From b6ccc9bd849a3389abf5f38a4dabaae94aa8452c Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Wed, 26 Jun 2024 13:59:55 +0100 Subject: [PATCH 001/430] Filter only external configured connections --- rust/agama-server/src/network/nm/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-server/src/network/nm/client.rs b/rust/agama-server/src/network/nm/client.rs index 9e409b5e64..5c178f46d5 100644 --- a/rust/agama-server/src/network/nm/client.rs +++ b/rust/agama-server/src/network/nm/client.rs @@ -173,7 +173,7 @@ impl<'a> NetworkManagerClient<'a> { .build() .await?; let flags = proxy.flags().await?; - if flags > 0 { + if flags >= 8 { log::warn!("Skipped connection because of flags: {}", flags); continue; } From 92e55f78b4149ec4e9b792316e4ea09f54c5b222 Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Wed, 26 Jun 2024 14:00:00 +0100 Subject: [PATCH 002/430] Added changelog --- rust/package/agama.changes | 8 ++++++++ web/package/agama-web-ui.changes | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index fa6a3bb7f6..fed08d18ff 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Wed Jun 26 12:56:31 UTC 2024 - Knut Anderssen + +- Filter only external configured connections + (gh#openSUSE/agama#1383). +- Expose more details about devices status in the API + (gh#openSUSE/agama#1365). + ------------------------------------------------------------------- Wed Jun 26 10:29:05 UTC 2024 - José Iván López González diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 3fa653e7ff..31004fffda 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jun 26 11:54:41 UTC 2024 - Knut Anderssen + +- Adapt the network page to the new UI guidelines + (gh#openSUSE/agama#1365). + ------------------------------------------------------------------- Wed Jun 26 08:31:31 UTC 2024 - Imobach Gonzalez Sosa From c526d6047a92cce82186491b0c30d912c8968eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Alejandro=20Anderssen=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 14:45:43 +0100 Subject: [PATCH 003/430] Update rust/agama-server/src/network/nm/client.rs Co-authored-by: Martin Vidner --- rust/agama-server/src/network/nm/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/agama-server/src/network/nm/client.rs b/rust/agama-server/src/network/nm/client.rs index 5c178f46d5..82b2fda3a0 100644 --- a/rust/agama-server/src/network/nm/client.rs +++ b/rust/agama-server/src/network/nm/client.rs @@ -173,7 +173,8 @@ impl<'a> NetworkManagerClient<'a> { .build() .await?; let flags = proxy.flags().await?; - if flags >= 8 { + # https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMSettingsConnectionFlags + if flags & 8 != 0 { log::warn!("Skipped connection because of flags: {}", flags); continue; } From 19acdfcbb4a88de5c8f058547b2cc13cf943b605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 14:55:58 +0100 Subject: [PATCH 004/430] fix: use proper Rust comments --- rust/agama-server/src/network/nm/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-server/src/network/nm/client.rs b/rust/agama-server/src/network/nm/client.rs index 82b2fda3a0..44278382f1 100644 --- a/rust/agama-server/src/network/nm/client.rs +++ b/rust/agama-server/src/network/nm/client.rs @@ -173,7 +173,7 @@ impl<'a> NetworkManagerClient<'a> { .build() .await?; let flags = proxy.flags().await?; - # https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMSettingsConnectionFlags + // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMSettingsConnectionFlags if flags & 8 != 0 { log::warn!("Skipped connection because of flags: {}", flags); continue; From 38243634a45215bdae034923f584c282c0d947ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 14:49:59 +0100 Subject: [PATCH 005/430] fix(storage): generate JSON using pretty format --- service/lib/agama/dbus/storage/manager.rb | 2 +- service/package/rubygem-agama-yast.changes | 6 ++++++ service/test/agama/dbus/storage/manager_test.rb | 8 ++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 9034066bd7..6019320ef5 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -115,7 +115,7 @@ def apply_storage_config(serialized_config) # # @return [String] def serialized_storage_config - @serialized_storage_config || generate_storage_config.to_json + @serialized_storage_config || JSON.pretty_generate(generate_storage_config) end def install diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 0d58f88f74..8d358c5ca1 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jun 26 13:54:28 UTC 2024 - José Iván López González + +- Generate JSON storage settings using pretty format + (gh#openSUSE/agama#1387). + ------------------------------------------------------------------- Wed Jun 26 10:32:08 UTC 2024 - José Iván López González diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index b0d79d6ff3..16a8896b03 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -522,10 +522,14 @@ end describe "#serialized_storage_config" do + def pretty_json(value) + JSON.pretty_generate(value) + end + context "if the storage config has not been set yet" do context "and a proposal has not been calculated" do it "returns serialized empty storage config" do - expect(subject.serialized_storage_config).to eq({}.to_json) + expect(subject.serialized_storage_config).to eq(pretty_json({})) end end @@ -547,7 +551,7 @@ } } - expect(subject.serialized_storage_config).to eq(expected_config.to_json) + expect(subject.serialized_storage_config).to eq(pretty_json(expected_config)) end end end From 472d38ecdf48e2080fbfba1001d85423899ff871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 09:27:20 +0100 Subject: [PATCH 006/430] feat(web): improve progress reporting --- web/src/components/core/ProgressReport.jsx | 35 ++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index 7b4cd47f08..c75f7627c8 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -28,7 +28,9 @@ import { ProgressStepper, ProgressStep, Spinner, - Stack, + Text, + TextVariants, + Flex, } from "@patternfly/react-core"; import { _ } from "~/i18n"; @@ -36,28 +38,36 @@ import { Center } from "~/components/layout"; import { useInstallerClient } from "~/context/installer"; const Progress = ({ steps, step, firstStep, detail }) => { - const variant = (index) => { - if (index < step.current) return "success"; - if (index === step.current) return "info"; - if (index > step.current) return "pending"; - }; - const stepProperties = (stepNumber) => { const properties = { - variant: variant(stepNumber), isCurrent: stepNumber === step.current, id: `step-${stepNumber}-id`, titleId: `step-${stepNumber}-title`, }; + if (stepNumber < step.current) { + properties.variant = "success"; + properties.description = {_("Finished")}; + } + if (properties.isCurrent) { - properties.icon = ; + properties.variant = "info"; if (detail && detail.message !== "") { const { message, current, total } = detail; - properties.description = `${message} (${current}/${total})`; + properties.description = ( + <> + {_("In progress")} + {`${message} (${current}/${total})`} + + ); } } + if (stepNumber > step.current) { + properties.variant = "pending"; + properties.description = {_("Pending")}; + } + return properties; }; @@ -126,12 +136,13 @@ function ProgressReport({ title, firstStep }) { - + +

{progressTitle}

-
+
From a4d2e86ced6a1227a48eb19d052f4e3bc4d75112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 13:44:59 +0100 Subject: [PATCH 007/430] web: adjust progress report styles --- web/src/assets/styles/patternfly-overrides.scss | 9 +++++++++ web/src/components/core/ProgressReport.jsx | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index aacae6db96..4218cdfd40 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -334,3 +334,12 @@ table td > .pf-v5-c-empty-state { 1em + var(--pf-v5-c-notification-drawer__list-item-header-icon--MarginRight) ); } + +.pf-v5-c-progress-stepper.progress-report { + .pf-v5-c-progress-stepper__step-main { + inline-size: 220px; + display: flex; + flex-direction: column; + row-gap: 1em; + } +} diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index c75f7627c8..455d27dc39 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -72,7 +72,7 @@ const Progress = ({ steps, step, firstStep, detail }) => { }; return ( - + {firstStep && ( {firstStep} @@ -136,7 +136,7 @@ function ProgressReport({ title, firstStep }) { - +

{progressTitle} From ca3a925d65257f1d2d79c55d87b8841500942c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 15:10:00 +0100 Subject: [PATCH 008/430] doc(web): update changes file --- web/package/agama-web-ui.changes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 81b1a57dcf..557b5e60d5 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Wed Jun 26 14:04:53 UTC 2024 - Imobach Gonzalez Sosa + +- Add status information to each steps in the progress report + and do not reset the progress icon animation + (gh#openSUSE/agama#1373 and gh#openSUSE/agama#1388). + ------------------------------------------------------------------- Wed Jun 26 13:45:36 UTC 2024 - David Diaz From dd107892b0bef955c7dc76d3f0fdf673143c0240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:21:54 +0100 Subject: [PATCH 009/430] fix(web): small UI adjustments in network page (#1389) Use the same look&feel when for wired and wireless sections when there are warnings or errors. --- web/package/agama-web-ui.changes | 6 ++++ web/src/components/network/NetworkPage.jsx | 42 +++++++++++----------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 557b5e60d5..f61555c349 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jun 26 14:17:30 UTC 2024 - David Diaz + +- Use similar look&feel for sections at network page + (gh#openSUSE/agama#1389). + ------------------------------------------------------------------- Wed Jun 26 14:04:53 UTC 2024 - Imobach Gonzalez Sosa diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index c5c60d1e66..c0380bbfd5 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -22,7 +22,7 @@ // @ts-check import React, { useCallback, useEffect, useState } from "react"; -import { Button, CardBody, Grid, GridItem, Split, Skeleton, Stack } from "@patternfly/react-core"; +import { CardBody, Grid, GridItem, Skeleton, Split, Stack } from "@patternfly/react-core"; import { useLoaderData } from "react-router-dom"; import { ButtonLink, CardField, EmptyState, Page } from "~/components/core"; import { ConnectionsTable } from "~/components/network"; @@ -31,17 +31,6 @@ import { useInstallerClient } from "~/context/installer"; import { _ } from "~/i18n"; import { formatIp } from "~/client/network/utils"; import { sprintf } from "sprintf-js"; -import { DeviceState } from "~/client/network/model"; - -/** - * Internal component for displaying info when none wire connection is found - * @component - */ -const NoWiredConnections = () => { - return ( -
{_("No wired connections found.")}
- ); -}; /** * Page component holding Network settings @@ -93,7 +82,7 @@ export default function NetworkPage() { return ( - + {_("The system does not support Wi-Fi connections, probably because of missing or disabled hardware.")} @@ -131,12 +120,27 @@ export default function NetworkPage() { ); }; + const SectionSkeleton = () => ( + + + + + + ); + const WiredConnections = () => { const wiredConnections = connections.filter(c => !c.wireless); + const total = wiredConnections.length; - if (wiredConnections.length === 0) return ; - - return ; + return ( + 0 && _("Wired")}> + + {!ready && } + {ready && total === 0 && } + {ready && total !== 0 && } + + + ); }; return ( @@ -148,11 +152,7 @@ export default function NetworkPage() { - - - {ready ? : } - - + From 48f3e737e2de775a16868684fcb1e5d68245368e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 16:25:48 +0100 Subject: [PATCH 010/430] fix(products): temporarily disable Leap 16.0 --- products.d/agama-products-opensuse.changes | 6 ++++++ products.d/{leap_160.yaml => leap_160.yaml.disabled} | 0 2 files changed, 6 insertions(+) rename products.d/{leap_160.yaml => leap_160.yaml.disabled} (100%) diff --git a/products.d/agama-products-opensuse.changes b/products.d/agama-products-opensuse.changes index 0aa654f7ca..0285283474 100644 --- a/products.d/agama-products-opensuse.changes +++ b/products.d/agama-products-opensuse.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jun 26 15:23:55 UTC 2024 - Imobach Gonzalez Sosa + +- Temporarily remove Leap 16.0 Alpha from the list of products + (gh#openSUSE/agama#1390). + ------------------------------------------------------------------- Wed Jun 26 08:25:00 UTC 2024 - Ladislav Slezák diff --git a/products.d/leap_160.yaml b/products.d/leap_160.yaml.disabled similarity index 100% rename from products.d/leap_160.yaml rename to products.d/leap_160.yaml.disabled From c8c4197db8bdc75e8b656678aa65c6089b894aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 16:31:48 +0100 Subject: [PATCH 011/430] fix(test): fix software manager tests --- service/test/agama/software/manager_test.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb index 34d6458b7c..6f29aa4af7 100644 --- a/service/test/agama/software/manager_test.rb +++ b/service/test/agama/software/manager_test.rb @@ -253,8 +253,7 @@ expect(products).to all(be_a(Agama::Software::Product)) expect(products).to contain_exactly( an_object_having_attributes(id: "Tumbleweed"), - an_object_having_attributes(id: "MicroOS"), - an_object_having_attributes(id: "Leap_16.0") + an_object_having_attributes(id: "MicroOS") ) end end From 218adbf9b5006031925a9dcaf2df31b48a1b95e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 17:18:18 +0100 Subject: [PATCH 012/430] fix(web): display the installation progress when connecting late (#1394) ## Problem If you connect when the installation has started, the software service cannot tell ou what you are installing, hence the progress screen is not shown. ## Solution Until we have a responsive software service, let's omit the product's name. --- web/package/agama-web-ui.changes | 6 ++++++ web/src/components/core/InstallationProgress.jsx | 12 +----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index f61555c349..096824119e 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jun 26 15:57:52 UTC 2024 - Imobach Gonzalez Sosa + +- Display the installation progress when connecting in the middle + of the process (gh#openSUSE/agama#1394). + ------------------------------------------------------------------- Wed Jun 26 14:17:30 UTC 2024 - David Diaz diff --git a/web/src/components/core/InstallationProgress.jsx b/web/src/components/core/InstallationProgress.jsx index bf5f3f08dc..d3c43d2555 100644 --- a/web/src/components/core/InstallationProgress.jsx +++ b/web/src/components/core/InstallationProgress.jsx @@ -20,24 +20,14 @@ */ import React from "react"; -import { useProduct } from "~/context/product"; -import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import ProgressReport from "./ProgressReport"; import SimpleLayout from "~/SimpleLayout"; function InstallationProgress() { - const { selectedProduct } = useProduct(); - - if (!selectedProduct) { - return; - } - - // TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) - const title = sprintf(_("Installing %s, please wait ..."), selectedProduct.name); return ( - + ); } From c56bd4ac8f8f3e7c0b599c7a9b62cf7b289953ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Wed, 26 Jun 2024 19:16:28 +0100 Subject: [PATCH 013/430] fix(web): reduce ProgressReport flickering (#1395) ## Problem ProgressReport component is using the PF/ProgressStepper in a dynamic way, which produces quite a few annoying UI flickering. See https://github.com/openSUSE/agama/issues/1373#issuecomment-2191899200 ## Solution To mitigate these flickering by forcing a fixed _inline-size_ for each step and making use of the PatternFly/Truncate component. A final solution needs more time to think about the whole component. --- web/package/agama-web-ui.changes | 5 ++++ .../assets/styles/patternfly-overrides.scss | 6 ++--- web/src/components/core/ProgressReport.jsx | 24 ++++++++++--------- .../components/core/ProgressReport.test.jsx | 9 +++++-- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 096824119e..3e681de84b 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Wed Jun 26 16:46:58 UTC 2024 - David Diaz + +- Reduce progress report flickering (gh#openSUSE/agama#1395). + ------------------------------------------------------------------- Wed Jun 26 15:57:52 UTC 2024 - Imobach Gonzalez Sosa diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index 4218cdfd40..da930f63e1 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -336,10 +336,8 @@ table td > .pf-v5-c-empty-state { } .pf-v5-c-progress-stepper.progress-report { + .pf-v5-c-progress-stepper__step-connector, .pf-v5-c-progress-stepper__step-main { - inline-size: 220px; - display: flex; - flex-direction: column; - row-gap: 1em; + inline-size: 250px; } } diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index 455d27dc39..d41a8be3c3 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -23,14 +23,14 @@ import React, { useEffect, useState } from "react"; import { Card, CardBody, + Flex, Grid, GridItem, - ProgressStepper, ProgressStep, + ProgressStepper, Spinner, - Text, - TextVariants, - Flex, + Stack, + Truncate } from "@patternfly/react-core"; import { _ } from "~/i18n"; @@ -47,7 +47,7 @@ const Progress = ({ steps, step, firstStep, detail }) => { if (stepNumber < step.current) { properties.variant = "success"; - properties.description = {_("Finished")}; + properties.description =
{_("Finished")}
; } if (properties.isCurrent) { @@ -55,17 +55,19 @@ const Progress = ({ steps, step, firstStep, detail }) => { if (detail && detail.message !== "") { const { message, current, total } = detail; properties.description = ( - <> - {_("In progress")} - {`${message} (${current}/${total})`} - + +
{_("In progress")}
+
+ +
+
); } } if (stepNumber > step.current) { properties.variant = "pending"; - properties.description = {_("Pending")}; + properties.description =
{_("Pending")}
; } return properties; @@ -133,7 +135,7 @@ function ProgressReport({ title, firstStep }) { return (
- + diff --git a/web/src/components/core/ProgressReport.test.jsx b/web/src/components/core/ProgressReport.test.jsx index d0fc0ed374..f90b083b5d 100644 --- a/web/src/components/core/ProgressReport.test.jsx +++ b/web/src/components/core/ProgressReport.test.jsx @@ -89,7 +89,9 @@ describe("ProgressReport", () => { }); }); - await screen.findByText("Doing some partitioning (1/10)"); + // NOTE: not finding the whole text because it is now split in two because of PF/Truncate + await screen.findByText(/Doing some/); + await screen.findByText(/\(1\/10\)/); }); it("shows the progress including the details from the software service", async () => { @@ -108,7 +110,10 @@ describe("ProgressReport", () => { }); }); - await screen.findByText("Installing packages (495/500)"); + // NOTE: not finding the whole "Intalling packages (495/500)" because it + // is now split in two because of PF/Truncate + await screen.findByText(/Installing/); + await screen.findByText(/.*\(495\/500\)/); }); }); }); From a1ce948b034f662491d76a3fdd011c59260db7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 20:56:17 +0100 Subject: [PATCH 014/430] fix(products): comment leap_160.yaml reference --- products.d/agama-products-opensuse.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products.d/agama-products-opensuse.spec b/products.d/agama-products-opensuse.spec index d82c58dfad..a26e225730 100644 --- a/products.d/agama-products-opensuse.spec +++ b/products.d/agama-products-opensuse.spec @@ -42,6 +42,6 @@ install -m 0644 *.yaml %{buildroot}%{_datadir}/agama/products.d %dir %{_datadir}/agama/products.d %{_datadir}/agama/products.d/microos.yaml %{_datadir}/agama/products.d/tumbleweed.yaml -%{_datadir}/agama/products.d/leap_160.yaml +# %{_datadir}/agama/products.d/leap_160.yaml %changelog From 6845bb3b76ef674181096fc467f43b49167cb758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 26 Jun 2024 22:14:22 +0100 Subject: [PATCH 015/430] chore: update README.md screenshots --- README.md | 82 ++++++++++--------- doc/images/screenshots/finished.png | Bin 37613 -> 29506 bytes doc/images/screenshots/installing.png | Bin 18174 -> 28592 bytes doc/images/screenshots/overview.png | Bin 54937 -> 69773 bytes doc/images/screenshots/product-selection.png | Bin 85948 -> 52655 bytes doc/images/screenshots/software-page.png | Bin 0 -> 94402 bytes doc/images/screenshots/storage-page.png | Bin 67049 -> 125831 bytes doc/images/screenshots/users-page.png | Bin 32046 -> 0 bytes 8 files changed, 44 insertions(+), 38 deletions(-) create mode 100644 doc/images/screenshots/software-page.png delete mode 100644 doc/images/screenshots/users-page.png diff --git a/README.md b/README.md index 1a4ff7d281..1c85faa613 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@ [![Coverage Status](https://coveralls.io/repos/github/openSUSE/agama/badge.svg?branch=master)](https://coveralls.io/github/openSUSE/agama?branch=master) [![Translation Status](https://l10n.opensuse.org/widgets/agama/-/agama-web/svg-badge.svg)](https://l10n.opensuse.org/engage/agama/) -Agama is a new Linux installer born in the core of the YaST team. It is designed to offer re-usability, integration with third party tools and the possibility of building advanced user interfaces over it. +Agama is a new Linux installer born in the core of the YaST team. It is designed to offer +re-usability, integration with third party tools and the possibility of building advanced user +interfaces over it. -||| -|-|-| +| | | +| -------------------------------------------------------------------- | --------------------------------------------------------------- | | ![Product selection](./doc/images/screenshots/product-selection.png) | ![Installation overview](./doc/images/screenshots/overview.png) |
@@ -20,43 +22,45 @@ Agama is a new Linux installer born in the core of the YaST team. It is designed --- +| | | +| ------------------------------------------------------------ | -------------------------------------------------------------- | +| ![Software page](./doc/images/screenshots/software-page.png) | ![Storage settings](./doc/images/screenshots/storage-page.png) | -||| -|-|-| -| ![Users page](./doc/images/screenshots/users-page.png) | ![Storage settings](./doc/images/screenshots/storage-page.png) | - -||| -|-|-| +| | | +| ------------------------------------------------------ | --------------------------------------------------------------- | | ![Installing](./doc/images/screenshots/installing.png) | ![Installation finished](./doc/images/screenshots/finished.png) | -*Note for developers: For updating the screenshots see the -[integration test documentation](playwright/README.md#updating-the-screenshots).* +_Note for developers: For updating the screenshots see the +[integration test documentation](playwright/README.md#updating-the-screenshots)._
## Why a New Installer -This new project follows two main motivations: to overcome some of the limitations of YaST and to serve as installer for new projects, like those based on SUSE Linux Framework One. +This new project follows two main motivations: to overcome some of the limitations of YaST and to +serve as installer for new projects, like those based on SUSE Linux Framework One. -YaST is a mature installer and control center for SUSE and openSUSE operating systems. With more than 20 years behind it, YaST is a competent and flexible installer able to cover uncountable use cases. But time goes by, and the good old YaST is starting to show its age in some aspects: +YaST is a mature installer and control center for SUSE and openSUSE operating systems. With more +than 20 years behind it, YaST is a competent and flexible installer able to cover uncountable use +cases. But time goes by, and the good old YaST is starting to show its age in some aspects: -* The architecture of YaST is complex and its code-base has too much technical debt. -* Designing and building rich and modern user interfaces is a real challenge. -* Sharing logic with other tools like Salt or Ansible is very difficult. -* Some in-house solutions like [libyui](https://github.com/libyui/libyui) make more difficult to contribute to the project. +- The architecture of YaST is complex and its code-base has too much technical debt. +- Designing and building rich and modern user interfaces is a real challenge. +- Sharing logic with other tools like Salt or Ansible is very difficult. +- Some in-house solutions like [libyui](https://github.com/libyui/libyui) make more difficult to + contribute to the project. ## Running Agama -The easiest way to give Agama a try is to grab a live ISO image and boot it in a virtual -machine. This is also the recommended way if you only want to play and see it in action. If you want -to have a closer look, then clone and configure the project as explained in the next section. +The easiest way to give Agama a try is to grab a live ISO image and boot it in a virtual machine. +This is also the recommended way if you only want to play and see it in action. If you want to have +a closer look, then clone and configure the project as explained in the next section. -You can download the ISO from the [openSUSE Build -Service](https://download.opensuse.org/repositories/systemsmanagement:/Agama:/Devel/images/iso/). +You can download the ISO from the +[openSUSE Build Service](https://download.opensuse.org/repositories/systemsmanagement:/Agama:/Devel/images/iso/). -> [!NOTE] -> Make sure to download the correct ISO file according to your system architecture (eg. you would -> need to choose a file including `x86_64` if you use an Intel or AMD 64-bit processor). +> [!NOTE] Make sure to download the correct ISO file according to your system architecture (eg. you +> would need to choose a file including `x86_64` if you use an Intel or AMD 64-bit processor). ## Remote access @@ -65,24 +69,26 @@ might want to access remotely to the installer. If you know the IP address of th need to point your browser to `https://$IP`. For the case you do not know the address, or just for convenience, the Live ISO is configured to use -mDNS (sometimes called Avahi, Zeroconf, Bonjour) for hostname resolution. Therefore, connecting to `https://agama.local` should do the trick. +mDNS (sometimes called Avahi, Zeroconf, Bonjour) for hostname resolution. Therefore, connecting to +`https://agama.local` should do the trick. ->[!WARNING] -> Do not use the `.local` hostnames in untrusted networks (like public WiFi networks, shared -> networks), it is a security risk. An attacker can easily send malicious responses for the `.local` -> hostname resolutions and point you to a wrong Agama instance which could for example steal your -> root password! +> [!WARNING] Do not use the `.local` hostnames in untrusted networks (like public WiFi networks, +> shared networks), it is a security risk. An attacker can easily send malicious responses for the +> `.local` hostname resolutions and point you to a wrong Agama instance which could for example +> steal your root password! -If you have troubles or you want to know more about this feature, check our [Avahi/mDNS](./doc/avahi.md) documentation. +If you have troubles or you want to know more about this feature, check our +[Avahi/mDNS](./doc/avahi.md) documentation. ## Other Resources -* If you want to know how Agama works, you should read about [Agama's architecture](/doc/architecture.md) -* If you would like to [contribute](#how-to-contribute), you might be interested in: - * [Running Agama from sources](./doc/running.md). - * [Working with Agama's web server](./rust/WEB-SERVER.md). - * [Working with Agama's web UI](./web/README.md). -* You can check the overall status of the project through the [status page](/STATUS.md). +- If you want to know how Agama works, you should read about + [Agama's architecture](/doc/architecture.md) +- If you would like to [contribute](#how-to-contribute), you might be interested in: + - [Running Agama from sources](./doc/running.md). + - [Working with Agama's web server](./rust/WEB-SERVER.md). + - [Working with Agama's web UI](./web/README.md). +- You can check the overall status of the project through the [status page](/STATUS.md). ## How to Contribute diff --git a/doc/images/screenshots/finished.png b/doc/images/screenshots/finished.png index 55c5837e221b2f033e53565c748093c7c6ea20e3..5cf71bc9e1bd151d99cd0d3adc57d5ed7541b577 100644 GIT binary patch literal 29506 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
    mT;ZRXr$xAE7Enz@F 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%0DT(D+W|e^X$IqOMmnLG2n>Ru1JKCU zKQYr^vMg29{m%v{uQsW1qH6|c*+9|3Y89af-jA*!Reij|SIZ3zEF+L9dV*7L9~0mC zyRw@))SMxU6!ZUrcrvF^2;>{O0!lr1|KFRTbKDFaN#n7f;kRt73m`pl=E)5ql4OEq z6{x;OFE*Sr^9DWB)Un-NxOQ9H1PWF-u7S7ET5CX<98}C?xB6`f2<YJ(&>UJQd+ zYL~~}f4H9iu@OduMm0; z2GIj@oFb^abgJ)Y&Z_VpEDpOFFMV4exxOk!c&bk|AtUAlcQlkDOo;JYdA zcbIwsN{%Rj>5H_r`L>$Wf7nphp~izdGmUnY^{DP7(dc3pgbIK}wVizEGnwxN;DpP# zquL>!EEE?I44y8~K1X*!lxpwxP8vjADl%77E>Ndm6DsmwfyLosLH$3-ZByLnK48rVBlOFV;a92>6yNl9^U7i z{7TFav{+l#AqkZNeLkqfS|8(LqW)5aV0gnt0hqh6-|qV;2DqHuWv|?E^t&qvV2<@- zFy{E-^^JcO=+FWgIvhVPH9UCRWOn3^dN64y7OY#XUz^?DxFH~4x?6xb^$YIz+-fdKdD4=-ex0C&c(TKD!ZmA zzGftVZ-Un<<{|ltkNDI0Dr-efH{@Q>qy)+d57Y%;2QsTJ;Ck%If~%qvi?Ww~9DXM> z&k_;Vg{!|k4(SW=`Deb{$wJ}lRqIa+49;9EzIl4)s_5H}A^?M@7bYXupd&5WB?HXE z!9XzoTYdMMm}4#oWL4+s0Q1k_SV(38CCP&m(oo`EFp${YFOj&pU@?P8MIv`>?gcI|ha(%}i9O61S=jHw%Q- z6~wnlLtR0X0?2(|)Kap^L#{u?&u4^0M zb_Qw6f`mcu-u>Up|E&qkH+m+?dW?wQcG95H*+elzyj3TRILaH*;@iSz6rWX^VX?Du zFZ)wW-vLs=HucE^xuR0BG#sji_;;|`S6gSaS(_(kaI9Wv$A%t5{6p|fJ^LnKgxMGZ zgh8yRI9})RnWU`V%(+4$l2*TP$H!dbS+Z&%zEIUX$=S z0?Toss41PG1*|kLioZ^YN!lmoMvKUkr|oAvb!8@8cGPh=>`lHVN4@U0t22#r-)2uo zm}VGf^EfdKgyP+AWnM>jQCV}Pl-^zWy$zV!uETy&h{~lRH^g1lV~nOfnvff? z!%KQRo-APhi(PfiC}DAM!V7*6DSEpqd&q6nBx~PKprkVhO$=YsA9v zxI9)&G&8>4Eo#u5j%abEs@E&e?IVRZO0u!yjA*Fp524Fu-RRXfavkyhppM>Cvy^Vp z3%dl!f#(**RjHIbM&GzeqTEur38FG`0j;{L7y+%F?C~nAET$ZyB?^qx@7y*6GMT{~ zy(UWD2snWC^nkF-qFo~P~}$e~UZKkCpMDP zd#aDo`A(AxXZCHyQYOS^re)U78VzQ7uNaZDwYfVu1-JB~z!pr=<%5r! z^Ao+Ov^;F%{;jXx^!w`Z+x#rim{$VIjOCH|=HJ5wTvT~Kk1z4g@(y}pYxiRw zbfjOu02_FOF!0-A%R7LOZd?7>Ura#KvIoYS> zXVIg`dzo4gXWf@C*vY4gwpTZ%EHzKLUy?lyQV_~KKF|;h*fA)$GV4RXY-z8uqPC4c z`8<2N*!y$F=<4`-kAbR^5|yhFdC_T|=`VxgCAH07sD%_udNsUY}U! z^O4O7gs{K-l&fU5>bTwxYjAS@_{1RR_sz*3KmMnt1L-=Qp3!`Dn8tu^gL!XnM7r4n za`cS?F74d)mhM_d`&TtogchA%3v}80V!LV|?=agg`8>&j_tkF?)B^X|MW3wqT8tfx zxEY`URrup&R})x9Q+xrHs#Qxy*&%N&-_Flpp2p-UmLvI1j=Y7@o!?lpUQ1B&+B>H< zb8WD19<~;3^OYoqh4_{w$bQfM_=u?920;v}q*x#;w?v9gP7DO67|cw zv&(bS+pR_6#$lQAFRC*79Bjl`+3N!owdMC>;1&t*+ZW?n?rLH+*|Odh8Md`t6%2Gj zQ1w^MO(uRz0NkHWb}E~%i! zYNYxASr9k4AHA2x?$|;1kl)RyD1q>*X(WCFre=933sr@+pIjAR-uY|TO*bqydVt7u zo0e5o50QpQD}XONX^kayGL&DYIos>q2wY~3LtC$53&ai=EOnJ$Cy%&2-0ba*xt06K%C-6UOM`N4WQAosale=7 z8Z1XhSv)*Q`uZ3b(`YE!-?wO$WFB;6#97j!D<*TpRLyE`_C(O!z0xX0ivv^ej({VA_@>*O?>-<)dPT zA2Vu~7ln1`y^FAs^!bOkyc!Py}|Cxk6fK^;@qiuAEqbyU6X4PF-8q7P9Wv* zLVH2B6?{HW%nlQZTSzps3g9h~i|3Iebw}Em^oHE&z0>FR--?Tgkyf{}UO(MhEEJR* zc|&@7Bo<=Z*9rYMgEZZ*cisoii~j{{s?hYsoZX1%IIftiIdGu;m!ntGaJrnW|8T%}%2sEsrZ zU>EFNfYJ1}_{JN~!#D29UPqzu9YhIE7bQY1`XZPWzw9kuW3D#_gZC54J42k@E`J_O zgLOyX0>h}DxatfhTJm5@G~4hTx3GBYJ}pg1fU}Bioni4De*AFBbSi#a_{-fow~wyO zA;qqn%-M52q%QcbJh=9dav951A-84!;8+!nuY>DO`J;AMgXyGepNcftq36o{E)uLa zgFHXyst+r;91k=hqo*FEEUMpJn(3}>YcE(|M{A9YK>F8m5H?O->CW4dhWY%<{sGId zaD5!EG@x7RR+z?c%yKE3ixjZDh&NI9s#xzS+#SBc+Vz~sJ3VQ~sm(6GIcx3LTIFMk z3<3$?`jN#)GanJsIls%dh%8KO^UZHCz1t>-`e7dJA%$rexz%`@zEpYwj-$^6`UFCH z#((eVkzlj$&l_P9g7=IYSFdcJbPOKRhuOB`^XN+tLA_3jQulE0i1a{kWrM zT+*n^u$iYlzTU?DIL_4bdCIIp=Bib}qj^TfL@Qd*eIhI2Ci~4{PvD0_Ce@COrg9TC z9k?1Xj7Ph+QiI^Jc;Iz!F6}@C7d)F{$Gd1|L&x>nAo_~ggFv*R-i#YkvoTZI7T#XG{Odt zf6dyIK8wn*wp^*1b!eA@^zV$%Rvkm5zBKo>d@B&@I-2P)ISa z%r^Bx4`hsNh3zE8;Q7V2hafeJ&Q8ifHB^;`jwpm&Rycz~E;k)&N{u2IT4sxl&wcC? z;FLpUE{E?SDV_CGgxn{m(;j*sb-XA8Yj#9CV0*gmm>d(X(}x(Bnxtsfh^w+TH<){> zT8_Oqws~$Ypv())J+J(#!Ym4}nb0@T+dfM&B`DX51yPWPADA)Z(X-oalrOV=Gfgyb zpCl+c1-q&y&$96l1Nb3NItw-hQ~_%!}q!Q2h6C;;w=&2 zJN>plqzK$*M0)_LtO<4@<84Kz*S|#^Vh83G7*C0J z$CD%U3{E6(t~8C#PoB;o8$S(aE1a?COa^`L+dP;YD)@RU@zFMMdOah`dy92tR_tpY zR3K-gFkC1l%EJ!>nz3JhM^Kvd(vQb3O5@N=TS3)v6<#Pw4x&GUHiNg{#z1$@mXSmT z*OX~!*0hJGM>w~-!TxCKsX(w;26y62?{v27@1LLlLKLyU=fC4SjX9VltkCm=(VXam zL!8RVer|Y?oU*?6X;;Jh&+3h_%XBlU0pU1Dl_h~itmZ*- zmKWLitAID0wvk%u_ayzk??$&LGA@u_R>p}LG7~4-^1N;n30%`cf-jr>|) z`>UL6S&rYz>y+agzkbX^;Wfn)vMdRaIkGmdXSe;Z*98l(qhj*Ca zqkFHk5tlOAS-!5$1mfPpaORZ^=bO}Nk16OcTbFy7a>>A5jNK~bh~eH& z7`?2|RP>84ENU-a;(S%|zsoG&gS-A)^p#M#>f8|>zdy)IOu6&vZEusTe86gOR9wPH zb#ZFGImR>h#AOz`b3G)alql567J{SScf>*de9j<5WpmYfs!zXcDe(_)%H~jWM6}c8 z3oOoz6H-3aYAj{kXiOQt_3SLwpd@3TGvuxDH8kt3XD5Ge<2t&X zAU4)UsdwQi5$F?&O`;1&y@R>7(}q4~jG={@X+W@6?vE957z>lW$4hJ*)ATP%miocG z$Z?s@)$IeO!*j9EcfXO5pg2T49ct~zBLrfXnW=aKPJHssnforAQq!nJ@8$`qW3-R$Q#$!GoZqnBj@@2_c->k*^b^1#}!Of zUyM=puy4+Pc^IC~?K$i=sU%8G#L=D0?rfIieg6Cc>qV~kESG(QIclGaz-$m0aMcb^ z8BgG5!3%LKd_Mch$IZbpOi2ZBaNu`8us^`L$%o+F^B?vWEDcV2E+L!?m-@U9P7`gE zN3B*nWpTpj2@3q^M9iK^4dK7Zj0>vywL${Jj|na z2C?8fl8XPSBE-S5Z=eTUyDml7C4a)XDbBbD#h!mEG9ivww8&WVaB`^p@Ni+OuU-C5 zkV`vby#C{BG->GPi_fPuHsJcjhgt``AN*8Y19S4l=l??kS3sZLTk)`OIQ`r#UyKv?NXZ>W}No;yT4}BFz-1m&LZ^D z3*cS}P_O3RL{0fNv&l|WOreNGKPe&kcoi#@^NQ`q;12pk*P1O`1FyWs!xNhys9 zCe#0U{f3fRYUB&tC&8{-H?prj-`;e8tGG?3SM!i{x*_I$R#h@?@tBHI>T}dr?!!PP zCG&q2jxxB0gutP_gvyO>rpJscH;SEz90>)$-F-pptMClAXHzX?x5c+;WeU0^1<8!Y zcHK3Sedc@jf>5D}I*cuuZ2mBX;Y&|tnyjCF7x&+Xn3b6ozC-o5lr39$F)izkmB8wFBSKh(vc)l6s_L`@vPW6sGLGO=$V3DRs$H^l$0 zblixhyul<91=FkgOjDNg7O8%VeRt@5uiSrkThDTL{-`?UE$)V`G>yGOYfXLV@5*++ zpm!7D9kC(mw09t=(AhDJnFTEY7WzI$%`wH~(Ua$M=c?lMuvyFJnb^62zI**x3m}FpIa_%JpDuc?rHW5Ji7J=E4toIF8-7A zw27MX2gb#iUey=9_arzY=Gbse!MJ+9*tuvmnDhphWC*FqNYgD>5BI_CHpZC0e;n%X z({v~ZI#zuBn|gZ#?JBex_{CbgA+nlZ8bdx)D$=-bbcAAKif-Rfk-sm@^8;bW!W-B_Q?SpUUD>vBd-l|O_89#Aso>YMGuS9%fakAu5NdB`sqb>De%gnVN-sjdg z9I1>OSu0l`av|q)_F7*DCEotH`fon!rn-@L(35apMK(TK(d!}YS3MAz)<&#BNY%Un zJo2Yz=l30TEGuj!^T*#f|B=t)@;Pshs!H%ySJfYMd>6y{1oawL62q|>$)sM6!qXPm zN=Qb|+>6H|t9^IHbK_CI62Bae)Nd;wn2AN$Um>gCx@F5~Wc98Zi!caF^n|S{UUwoo z(QJNLeKfPqZsUKvowYaCE$b0@DM-I{AL)N91WIxP5Mw57RvZM4`DGhQI{Z_)$&3$gBA}p^Maw7zP zbwz=NoSm`qd>z88m85d~mab;zD5YI}vU8Qko~8ZJ-}lEg(xdogzi-E3n+_>kqeEg_ z=dW)0PS<{(c!vrx^@$~_aV2V{cLnEcpD-1{Xx6gv@`7)WsF{z1gR5-Xk#{Hbo*GBK z8*<9PO`Q!Yv16y@qSr~pmdcIV_q`u2_5bYh3J*_n3elWhnYsUO7b_wt<~((${ZQt@ zznWXqCjy(653QHZojC@I0<||&d#9BwR$M9{dG5h@6)k$iOR_R~;{zF{HNC!vM`#XI zoY7NKV_s}c&vNXSW+#Vo7>fOkItbHv^Yp6l?>Zl^TcV>nXo{91-B|_sh?2cW6J&tt zE#q;OT~%YLbPGo3YtrIht9fKzae8d={5EAOl%u$Gd(P8yEr&@~{=*=@)n&R3S1F$x zovYuyy>GXXP-NwFU9%H?`G2+d9#Bn1-Tq)GQdF9tD7}hERp})ly(1tXB29XiUPG56 z9Rx%OC{=nV^ngf_-dljsLkYbTLMD9k-uutI_hzm6|JTf{`PTZfSnGs)bI(2d>~i=1 zoqhK~O&hB)0^^h+Y?O||?T=n-%RFAx+`f9_+-?|W&nMDjeF3{en>ek>B)G<3ZbZ^r z0XLXO&TGX=vPZ`}!!V5nSUwdN{W+k!$RN_*z>}bW!|*?Y?c!S;eATswFEY z>l4HL=<_&$x3{w)ds;z3GWi6=)z_R>GB|xT*`-^NVs4WC5i2gjhLFG%`aJwVPrPdA z+5Ps%44u8I^AiEvxjg~LsjG8@Lx%=zVUL4edtbl%C_C4`X_kRpc+ui~u(ed?f@R0SbpTT(h3rc#g&nLN?(;5Fpg#yKYG9O%4J^c|~(?dU9kt;-|C~4}Ii3X1(Fn?-a z7?hp}dTqpgkLfvh;1|cUIX&6;eGy;%Y2>GB`E5!><6XtlXz5lqQG?Tk8uTP$r@v>V z*cZ-VMQ0u-WqENhi$I{?BFy;>OCx6E=hC1|)=l7XrF=X)YKoZcvqxz~e7Cv!W9GE+ zt+{reQ_&mKd0z4#g^vEcsgV_qc-;BPp{$ z;6`yN^aenlsNulu8N%nnf7lir@G9Gu=^d##vW#;qXvlmc8P3+Lw$W!Sv4)tF+2kC4Bhw(e zlJNe%G?pkiAik{<1nx&x!gmQ+G@$d|rNl^5q_jEokqx3L=0(e)>kFTr?E_Jc)moIv zei@x6K$7e^p|&R4Gm2hZU7!gx(v^N1?8*#tK1f;p_R^kCCvLjRag}C@S}x(!6crmD zDuMaJZozvQIWiFegqk`DGlY{<gJMYXFoy*dN(U7um3<6OBWE55S>NpP)HDVgx zmfw4B-M=&G?GFIbl&H9XCLqUu7hLsgYQ|#8_+@@q65jb(b2%)FAHYE#*Jyj<`%7x& zGppc4R!qB(I+Z8hcQRdVzrBHP8ZG|XJKYSg!o&D!FXoZrYkT|`Rc4Pz=I8N&e1ljP zRI(@e3Vv-h_}#I3b6W28P zlS4A^fqs7I31d$!8PJc9!+&P|Kv@vgy1R3N*nLYc6@T|cUK1@Ol9-3jXjwsB10Od| ztA6l7u+`H6nt6V=L|QV$y+&x3J1_ z2}RdJH)j}CNOCY$wR}k}`5~rHf6vf}4>*V}X~pbXAt0xs8x4S52@6YZ)=&E=Q)oOBEG5Azpgmmzig| zX`DQsnZkM1*5fp(y5f~4D@?r4!$-!CkLlTrr0(i>)JQapYs_CXe+y9W*@u17Xo}t# zywh?aceqsuupAo5pZ3dcCjTy$?x#WZ=HU84y+6)O=ch%9(0Sm(H&_b)$6R`Rlhh#( zdV-+V6m!kLW-A>YE!Ue5Vwg!;6yurAyge8T2~Tz0HPKeATZOL{bu~nkWOy6^$Yv&@v#+BJt68qT*3&7G@)R7 zJf?=8Hj(Tfl#z$>oFxpi(yi1o6)M&nYc62Sk`dpI!-o{Lxk{2XsVfnAtt~YuS8|xL z*NekCT;pqbW(E@`Aw~S5T8GS_{Y|qo-9}>PkgyJfmLo;uFLbEw_YGkqVhW0_(A8!G zv>fx#SHnIk3I5`zN7IS&azkkafT+l8@GrRW?;EqNJo^J|XhtoK<=O3rTkb!;;7H#d z_#;F0?`QrQ53KEnTf=JR1L2{iu08}-!92|dr|l1f0wzvjgQ%l-CnG9%rJj6SxmsD; zDExMQ@#qZI*7g#^TRWMExmb0O04#OYZR6~7KgJke}_($gVaz`>7h7Dlwp7O(r4EZ~}%8*af^B0vcwDKV7ld z=XsVckStB%54oQ%{!axH;BJ9;*WLOL?sz2?ij~KnS-wZa0j=%~Qq7zpSCFZy?J^)~ zHB=kDlNNeM^Dsk-EB2Wryv}@K==y&dFRP*oG*`BLzr_1lAGNNmYcp1=W9`Z{e zZS1(n1oInc%zM9O4)toPx`4$S>}-Ct)gk1)fsXC4c$U~@tkLDk2&;Gj2s2LZ9DsE{ zm`fu=VLh9V0#j291q0qzX;~%unL9quVNq^e}qr=D#Qdm#I1hwW4#{UvZFgS8m7#xl7K2Mf=kA7^ zQyE}^8nNq%gwm8N@Ez_zd*M2Da+b6eVW$*VB>yBMY*62YJ zD@QY-rmc;Imjj9#NT1P`{pDDz z$-jHlI;VZC8z4U9OU+E*<$j}${k?4aA8)sJI0_V;G2tMzQ`9&M-@!t~f(TSH^X+~5>9SX<6C`5+lbwGvBiR!q-A%tu| zKkJ+GkO?-W{I|g1Fa&A}JUF;#HOd^gE>#iGe|;LFI8e9C$*HPEoMt z4L$_W)~S5g7Zjg*J=Nkos!+2%aW}B7^Lil=djjb2eIrVCqagoykln{`FqcDZ{JMIS z8@=B%fXc#la$(`;$J@6*lEpDFM!k`7d`#*w`FbNOjo}63#iW%R97`T{+}9_eMp#m@ z*B&Jpd_500cn@><&LJgSaujH}cs@hB(ZiuxH`%$tcZ_X#-F9;>H~6Ayn)m;l9o1u; z$}ihhZ-fLj1u_`ZoCMMa{096RTUzN=vtBTIU)-r#tGKzM;{lC+Xb^yxUL9q+YGQ9$ z9Dmac@(Bd>`+rbj)7A7z?**^Z7+w}+nxo*hfwPH8q-26W!F7qigm{7o~nd6n+by$}TB)YixkhpL~? zS8xnS@<}@I`mf^!N@@l;IR)OBxh?OLc~s3)T5TO44}OM9P5dOgY6E+TrJhz-oD~`Q z7Rkn=`bB{^oyi-eKAPY8Fqf3a(a8muT@jZXNsaO88Kjpm_u)XvkHCWr$N=8pjptn2 zd7dZ+diLGqTddV+#YToE)f7Y+!+U%g!6?M|?jXis%y@EGHNbxHyhJKpahT^gUi14o zdV}Y9obtvp;Ch*3gW8I(r-31E794mF_|n8c4F2H=6+bYn`MqG)zjyw+L~+0*O#u6k z%_FqkUb@HCQ9|QZRKy@*YMV`}(Dwq{0H@TpgUHMFd(GFd@Lo8vI!;n_mbEqKL)q@6 zfE0|wR|?^{Jk{r%v#$Wjk_59ZiwVjOkvIyEr{m&6!o{|S!GW}~wYn zjp#;q<=dC58PB*%c6<#K@P|fkhUY52FtY5AkRTL^5|_ z($3_5@^~eNzz(A_44SW!M2*GcH98?S8~1&$toG3~bQVDsDN{mA7F=Z<9hFZ&85?dm zu*;y`l7*?CWT^heXzY~zCkVv&09lZSn<-p?swlWKqujup-e0UzWaF|#NBAS!2$$#e#5tCOjf(8O*vljhM4*SyH63{c1xbG zqgQi#X&6H#0hRKJ9w7YeuPG2sPJzlEwySB*XI~n)+J(8R(041r4GsN=g8mAtcjh6? zM;_x;OpU#QOLJPWSZ6*J-Lf{&4`ghk_p?z`--r2M0$<+$Q4)HQLix?Zp4o8GxHCtc zAaUrvt26y{*V~jK2TxjWNdc4u-D%K*%b9!lKj4N~LSK3{cSC47epj*wIMfoRQskwe z)sjn$&`hqv8)P7BQ~gb}`wr7V%1h$#dwgQpa8-gq!cjKT)7{8#I9OZ7^J@*X<6fDQ z1P9-KfC0{u{SFMdzGQE8a8di%4pP3hMGr)p?dmwpeZRPFAk#F!=@*w!E^HiBI8kQ2 zzum&!=@j~iZ;%5tGJ(%bDmFADEJHW*e&@h{)Q^$iyyy(=RsaY6dFG3KXZ7Fq2#_%L zSNu?FT!lD6wWUhOl%PhO3pFk?okD8@|2sIQ0yM8H3fk?#F%sY&|V` zjZ_zvtLR-#(|q)iJQCgKU}YtDVoNW+rHH!lUCopM7eLc(oa(DUXc&m%$&R655dGVE$?Ce z?y8+T{ybPO>~(IYL&UrF4o0#xM3{dFjGHx%kJy+Reiq=#4%29z+32#$MZIDCRA7XQ zGv}^abq&%2Bt#pkbC$EdP&05BE}}1d!w4HX>M{E2pE>l0s|kGucp0ho*Gf-Cs4aY9 ztt=qrbiezRRBp7-G~%EkqaBFbY=(F#j`V5{euWo-g?&uDOe1ihFLswNLTurGKaTox^^$t4b56J6QQ z9eGXA#w`0|O19@PXa5_m^ACCyyQhEa$+CWbJHyMcrC;|0gv^nbr0Av%Udz)XmJsKx zfr*TqO0r_?YDC(#b&=oZYOveS9V^rQqWz{OLe$rw#q+`L1@^c8d>Nl#C84dOpif)( zN~oXzNJHmK$^099^VH8~f?QJhr<&tdI}Ct*q}|3gXK^+EP{%7RZ3+)cNz^kh;43-7xP^EF zGa4LH!MMgI@FQOb&p0fsS)0&X`k}K=>1PL1lf-@}cR!$CYEORgy9Aqb%6(XxHb4C)yT;L7)zQ@?F7b*)I^iD=5NE zr+%{QXJu7h96l>|$ftd?Gmw9A7O&@&J0%eTN7C1&T;%D;Ge3a;L|V#H@!{Bo3;p8H zT&1SI;NbYV;_tzMDw;u|wMC78gs>B0F9@V#%m4ER>c~~DIfuIRrnfA2-PSA%sfep? zC9%HRT)+-IDe8XECgW4e8)f3l<~azfrl8@Mk}9KQwUE&WAW{{YY1PTUbgUpem_@LS z8W9N-E*J1#V3k(za*aoAAf&_BoJUM&O4iM8k1!t%eyHv zMAYwK&G_@i^ME=DQ+M}iP3dF0 zXN4;|fu%JjRy{|Ujk=` zt~@@!rQ1K(mv7U+YEfqW_OPM`b^?U%-#ikS^i6KO4pqv~ zN%PAnAc9cpKOtKF@RAjK&l*gb#F22Vj5O17uG_b(PN$(%lIrD>`S!7EF^T(TYjw%# zKB1V>k2c#Dd!ky>9mlFOM|VugI(?4syMiUbS;%llsx3~K0d$Y}k+?#lm~2&4G%S%5 zOVPG6OXtgUK|CQRIYID#nV8f4m5LxY3oX>L8#2)wE?~|Mk16` z|Gc^!dx>aSYXEJ_MGUiCH(RpD`ki_+_x}nEN>+Nnd+>Z`i%oW>Idu|I@*zUbXaiHo!N%ulV#< zuV)=zo?P@eC&an1AY`%FU0atwp~^G6NV64pH2;@O$Pc-v8fE6IQz_hYGk0-J@5eHO zuI`iEK+VLuqcRzdaQ-c!Vga8Uo6*7Yk@+?a1p9fE17Nys{}~TFxKP)Z{v+1XpvS}J ze6TAa$U~94YUr5K(qKxG9dGgnWP9dZ9AYH?rai}c-hv1Gx_Ebob4;i4b6tjt=o$&@ zPWh~b(J1sBRdQJNkh@I9>MznI(-uS~J}n`?NS!_a25WftjI;Ns7@ST#@)sUjZWHMF z{4k(OOz&^1`saq??StA`qF5C+HyI&YEg=!T-$#lnL{L8pot2@R_p63n?@pYk+^7-c z^dtSQq*RrgTe|{ThMptutbjsK3SG|nPo2w6w{|?Njfwb)NMo5i-Jcr>$FLu9EdqDo zA3CbZx~o5Fi2XEi)H0!kFOqP-tI61KT7;NLWpFq}U%lU9V==7k^spf_!h#(LYi`5YV91VH*cma$5M{QXAx)Fv*-Q`K5}%*_B?Of@ZfZ z9z0m)sNW$E?5Dc~`=g^Szd)W8kgD;UA3#_n`XVoYy`PxPyAr(A0W$g(%rm3#31C`7 z;lSAQb(6@UU%&dW&ii8_`-6G7ZvaeC;0=6k&@Y_CPs#7`y=pT+_Y(7rY}-+*s$AP^ ztVU2XYwA*5R-|cbA-b)j+kM6@3i862LFWdkk0M^Uy(XD=zzf8a+MYzk^LV$$0kPX1+9?*JLb94Y4~;s~nkBFf;ck9NOn6 z2!tY^*!YnPYILGCyK_zY?p_Gse^Z zTDYbviHl`MuQ7b#qOl=$C&z9BF>v?-pKz9##35&8!F9}8olW4^wAJ>UG*98QYfi6P zv>9we`*}+g+w6bA2-j-0>i1lPlKOv>zV4_oCsB>&Ls%cz5IZB=28Y&*o_}1mqDIAg zAKnp-q|fNeBaRBAk+x(YyXjaeVB;%yT%QP_Q1bpDmV6_aJkzne3l|A@?{k@KIQ-1^ z%EOhV&hXwR5VD(=yxRk!VZJhXlQazBGxS=o+OCrnV2GHE_g6A~!=i&gB#iVN%;_{P zFv`wB21{NN-~9SooNFAi%EA9?P<8n!HQ@q-KXa5?W~%Dg$i4jliY=)-fW+vyh&ep< zFgSnc{J!|X9w-Iw;GN=_YnALWDDq&jw^KJ=9%+O5b@Rhs{8Zm6Nnk>Cc21B=Am4R3 zq&bT8=uOp@s2+61beSHJ^>FuR41;x@H>cmJ=-oKBS8j{v)oQY*JF!kh{LJ|Aek3m#Wnahp1QB^eH8nYx*#nIarEZ+exU?zSX+FFG=_F(?Y5 zA?fLkzPT}mP4wR^OsZx2O6la>w}XG^Jc>1J6bpDsOknz$+|(T;Ex^({q+H;jj$;^t zZ1LhwuR7ZXjiFV=V)zV8s9SN$FM!2hHf zl5}l7UFGS9ullqH1;Z?vg;)<-{jJn{E>ybIRaoMp(VD^j3fJ};yQbOpzIW24@lU;x zw25)@e2AULHZ?Y4^M+l}LxjY;WGT!t5pzcIX#z4;=@Z9*-(SL&nY!v zsQx>~O_Lm(DZLEy#W7sVS%;lII3M;2@)N(wXbuN2>SR|Y^8+JFOFD^2`HAQy!$op~ z=b*UtT6wW`TFcLS@Rmx}TC4RQl+%VT)MO;x{j@j}&@F|9VMzXd(9>0s?{BQz{27zw z-S4t{2XTQFtNZN=Yp0Y-B%(3)`m&5ui-SY-lQTNi*xU*{C+N%MjLbJ1&-c$1{@kpV@Mu-M2~6cYu31bzV1XqKHTA|R(d z+!L_+U$R!%jzfz^-xU$>kmUWXKLk z)GTi>v-9`m@ik^+W2L1%!{}-0;MV7kpOonIMUn`*AGGj0VJ(m2SZ@CeIkVJl8 z{B8!mA)WDXU&rV+ej<`Bt6JF3QFFsvO8zia!p@(3E#8MC^H)#0hRZErMdz@V83%XD z+WU|HYzg7CKT?pfdqiCHB+n5o*kA96c%#5u6)c2%xNN`G(kcL{&B{_;+;IgEvCZtY zGmIJ0p2i}Q#Uw(1N78L~t{$$4uY5fHB3TGOufzZNVHb}@snrSpGpt@GBk}WjtE7Ml zh*sJ2!1rp-3MiqjPQLQPeE$hEsY98EcheIu7*goK*9J%o_LT$>ZI0OC>`DzgqKl@t zzPs=BACDL|);q;BFKku@1Op^NdU9>rw@v}A7iUQtHHH7NMZqG~XV4qL9crliM$A^_ zTkVQxKP?p571RZPa=Kl|8{T&zw-brX6v#j>OL9Z`EZuvYdKgCJR$nk?2G~UtYEpg> zb5+{kd1U%;z)s{ipD=?xZgXzG&lKoE`)87;GC4|{`+!-3SSDQWqXjHPAP zrpDJ-nd|qzrqX;_EbA7!@_gVxaLO!86xX+8y`UyrWAZ94O!)3hy>Q4zYcShj%y}Sh z9zv*OzmnnX7U&60dqvUfH4rO264%>^T@~# zR04OD);_xBh0i5Zh*U)Vc9K5+sbV)QmTQBU+fiJe)s-IQ(8|lN%_gq@A6X@cI|9OQ zSLO`HFS*SsEZsX(m9{6}oLLbQ#^YW5`nP-;Hq0!heINh#3*lH}vQxz&qhEN|D)+PK zKRci%q~6+;bFRd`R8o?}6hgwtn(2T=R!~T*M&P2xt=Iy`nM`au15D0RBz@Zvh# zvGjS@sn)*z?1!@lWWc6y9!o)iriBT8M{6|3>)VKZH~A9)zFYz@fEzjA{whOd7UM!+ zN7%>tp*j3FD}}|v662fmCOW`-Tj1W$%ZBp-DQC^HwN4l^e|9JTvLRxwc-P@WUASY} zU$rZ1j1U#N)8aSTqhynfGYDk3v%@I=ZG&dr?z1s)FcKxpjH3UWR}WVm%NB~hF?+e3 z{WSqv(t7{jVHzZ~0>52fan=}vYGZAl)z((@9(xyPx*u_GOBI0oKN5S+eQ1$Y^T(3m z?j7fUJ*O(T@^TnarI8;VV?)Ho8M#6?oq2+zw2HhxW~*b%xskCihiEbhIAT`kLaco- zs982J(9wz2Y|Kc_zbuG4P%E;BI&zRA_!SQ=!sqL#AnlN{*Jw*)5Owx5All;lKzd_l zlj6(&Gep9{aZ?%$`tOhrKGPM5Bzr^@;q>8At}1`HNF<$Pc-ZnJh;9UBQ(Ba_Vov+S zG3usq2sCDBDCeT%^xXbf@8!Oc_|KoLO08l9ezVd%jP31b;NfTfzQhr6KE(G`+3XeK zFbsEczWvuQ{g1IKDFb+LB3$M!r2J>Y=)KU+NcHsY(nGDnVNV!1*@x*@B+qC0(tu%g zw;=Grrkty_CX>fIc$%ysUY91{&j#~5O_#E!L74bT$+z2>{qaB|?ubTu4Eao@nw3Uq zY=(ZdX()B}i?H5r7S-hv<4#r6vFtOtGMglS!?FV!86AbVlMPHu2 zeS5Vd)Ei#mil=(KbHE>#IIUr|K|%peo6NYGC@HBh`OmpRn{D%h3p$Nj2*3MV170Wm zsN(rB;nyuHerE1j`-hFiIR{omwl;_9a84Z{~vXUgz zBaip$g;rD@ss%0+GgGRR32Lt91$9WV8=Ne0JI$osa2H95I)(8z#7=mzkMa zn#rXE-~RtZ^j?l% zveA)hT8ZE4K}>E;%zfoCwSR1~wA49VgtCrv$HATm+UnUlj2B)b0$@wgVr$B_e(qdm zmX%fmD80R)ye)tjRK(Y|V{iDnL4(=o_)6Z^~B z9S#&n^AjcQOFXgyI^2KIMbOWA&Eb1fm}p<#^d4M>0gt_UhllOjl>ZB6i=94@?;m8E zlK;;(3F*lTfM$-or{YY?+sNId1go?iO%lqIa7H`* z(;~d1^?~VaO-*RnP~KH34PmaZuf{6|dN8>TdlLl5l8D#tn#>oIWWIiu>8B$n|rmeliNv+C{7JP0)k+%aKy z0EL^*dlJ0O9AD26;<>m$`+$K>X?H+mn-KW*55Z4r_I6Wb1{Y-OTVDi~A2y6)m`&{c znTr_ESVDQqyxrfPJ37oHRvyHsSvQ`#cYV*3QS&!U5}a$t@~1lvz>m(O0X((1J2K=& znm+-{zK@;lf~=tL$e>d0wC9r1lyP@wB$)+kp+y@*tJt~KkJ9D0>C>uy@UYP*Rs6`u zpMPfJT?L3RM#@4HF*L=kUUu=$Iwz??1N!|~7%&xkX62b%g_zYLRDA1BZt z!TNdwrSZOL8TLNy7c>xTsrJ?zR+qcia+yWaNmNAOQ7qN5&+(Ct0fJUeemO;wjj8#% zQ=sSE?|w|y!Qa?Z_b64d&2#FRz-(}sPQ*$Vbw_@iWv6kry}_IT z8mv^|R~;SPqiS!;)bjNzHTGQ|;(e-yAoO;w-fx+IC&iB2{p%TkpZ-graxoYpv0;xD z`UfS_>%m6KghAdNLva*^l_ITMI?jHQ%?0bRP?4KjFFmnc}e5>MUCuohvEf-`g7t41CIVR~ZmR5h6AQruK2}>BK z=-rYUFdKD8jFLrC5;bx)-K|xOq_+0}$yGDLMp&jH7vHRzV6u}@qAS#Od$s}ckc+)o z1Aur6&-eXUe}-D|+5)RqAt59a)=C6LdF2~PEw;()ikrM0$5`2!{iPJEyfj-M{)P;< z+>Uc@EssV`X$05jDZVcNT*~eyj{C2Qwr<}kr)BTTIulRFN=T#{-t(%vZ$CclWwc9M zpwwn^e_CX{_C8ew)c?_QjChXzz0t9wqg|f5qL_dj)GOGurU#Z$>ehJV0T= zsUErysrCk(b(jMeD0X0v51Wdv{IF(;aycjNdQD!?Q+3Wyu2ss{_#uR6RMO4lE$k06 z4p{rdiKk^2{gcK>Nflt@ZM_Nhyw#1p;?=OATa&SFTA6quyE}I|^t0C=Q1U4M8U-;IYzv~QXoP|W6a&3FLEyhyNVzVnIrRqd@c zcyOR(&5C88Yzw@s{G6>7&S8`&&{^i!mpyWoBm=UFbH5qT+Fdj#B$1;1WnwkhXygH# zvim%pYhVPOiiha$Wbo{6>lPgC8BDB+_7t9RC2WxRKK9sIuR8<+CKBgV3XYmcjlKD3w=F*Ma(SrmY!FVy$A zj63c9BS}#tNH0~D-QW(^@|yFu^?Xc&e7M0Dwje{WJ{-zf#M-%5LKF3Oo85#VMcSJi z?^BKKyM4eKOtC66o|7MFzcD zojN;lwES-U)Rt6MqserAYbgm_w3ydEDvR5&1TVFAjVo5{MWNELbBS(L+%Ffu+_(>y zwS5de?+l@V;FJ8q6E)r34JX4Aab9e}Lg$;J5Tn5FQ(e%n;eZhbM#hu(*QDawND04+ zeoBKdd&@?aTzpCj+*`xNXWfRK70$q4M!_TLuUW~>MIU;nR6woa25NpHOZt^H=DpJ7 zSZ%018Vy73X$o-sDJ}6=D+|6^yz1;h6L-eAB^^3vm}qWaeqOxuVu$3x6I)u{+e+3# z=0CT;!?nR|Th(*M@SF;NXJ|L@TSapZ$Duo$7HBMnFFf(tJ)9o%f+ji_gZ}n&b##o2GxV5{JlKQ)9J%uYV$5X z;ZB`ikGc$yMP-Ce5xq0uog6Y`X!OjHkv`di8mB2@RhM;#lFodA;4O{FNq_prDt^et zD2{a-$V}E1XQ|XtRas!C6g^_xFA9OT%nzJvPqm zzWrK9qg-D9J>p58bXJeL9l5kTAyJenW1M2-nRUD0F3)l~rxe9}gcC2o zUlcmi<5n|qTM92zzSWVI-K^cZIqki?T;6L5H1Qww7CEx@yYO`9w)rO00#7-NE~J$A z1?@)r5bi8(gf|<%Uu*Uad#G-YUrM315PYY~m(!g41j>c%oH}k&K*ce3Fm$BwIM7S0 z^?R;_eO%s+ul?Bvh?S~cjrauD@DGHh7pF6x*!)TQQ*m<64KB&((=&unk>zMI_JQ#M ztEQfsN`v4Y*6A2U9N+^4pwej3wT|2OrPT?`f-8?IS2UrRoZ5bF-=dlL|L+j%c7j`!K)!r6{UM)ih~oE~Ua zS$PFW+0ta8bhfE+7j zqd%TA#7MF^G%#H0cAd*lk>Kx=VtvCG4kI(LYr6rWcBS#ajmy2LS`gNoiDWJi{KUP5 zfMXwG$Qms7*_1(wrw#jhk_@PvIOKJX(~RhW$|uD1X}1Q6=as0BbKqVmpAhLOft~$q z&N`3b1EHTgCjpYijZ)<#c<0$O7>|YOzzarmF=Y_GX)0I2K|Jr8YH7#~E>yU_J${~6 zHwLR_e+3V&-sA#Ovbu=xc{g)VJ<*Ev*$}zw+0FZ|9 zSqJXA!Jg9S?f{hUlqZ^)R6{f1+5rDlkoEykyom#Dkld$IPxU!>|b3B_6;*^ zqU)+ZV8bJA!}Io)Y3a=vF~8;sbb#hSU1GgIo2qddM}cJU$dfxV))tnHV`6B^R{CrqYQ9aOiT>(J-5PxK-(QMg%8)XfhM<9*I#Fxqzev9Q|pK$*Lhp8uoQ(OApoF5s1sC>{k9O0 z&kj&Re@4F%KUczw22t|#(c6Kr+y`_s;+S433j*@|$-i~Zx&{8?6!O%3W_D<@WuSJ5 z-7pQHmQZ^KP$Y$qeEv*7-`YicIvzoajTZp`0#z8*4{xlISND|0a(T8D%?cRWY=2{q z9WsFb@EfiGS}h&B7a?{ltpFmx`0!VQ+Y~~K)ZsoYa2f;x zaqB*Q_#6ac2Y#~shw~5M>wx;Q?Ds!^P2CrqoSbt@MvK5-p@2s(10ILIcJd2y^Y`)l zkI8dgkgth~la8{ObmcJt_e@F42yu%%5rE0S@V6MRL zxjIhXPJVU)009qPzXk=l`2|RM*?HT!IQf-szC8e*;{U%sW$NVT@8;tTl9#?KeWyU! z>LUpBACT_D2QNZ0wikk5nXI+9Es-(l?2*V0UMC4z6W)uD@Ey-;sdW~F>L7EE=(_sJ z;aMcTwrEed83cNBlk*iYlJnnz|FY!2!th@; z_^)O7uTA{_l{a|h(zE6l$m(&FZL+HHTWH=&OXJm4OfP4h2UTjVLFU;&Z;WaJR5E)V zF*da~lb#G_C>0je7ub8PC0y*cl=s23xBKuRV^B3xyH+pX7to4s=21F}*0gJl6!m1^Tu4)#Qms{JICO;8N z{J3h#g%MJGJ>o4UhHNHO8KIEln*laiJjEwosq(dfD%G57W*lXeojdqqlBEz%0}yc$ z<=d%`uPWF-?1`!n(%U~cI4?SyS5#Ee9A+J85^-B5yym2SB| zH~VD?^$s)$bfc8e=`HCZjqs8Z_ujsX*n67?)~Z47=w{N;mHVLZty8Z+)9M97Vg5&| z#CcWbD1+lHb^e@=g3oN3(*cjaq0d@#phRE)PRMF@$yQpx4aR#JNV!3O^2dkv2%gIz z`)hB2&0bd8+%)$=k)}G>yFXtJop^A~uW4RKcdA*fi_bP`r|!~v|Wds%II~xeOrP>EWl5hn8XSA_Kcih zv$`PwQa}BB&g2{yzobRIXPmUPO7KSGwJ9iJA$Y1_oP#J=3ngN{M4f!YV*p}n3ySY` zd|o_p)*@6f9kNm$Dl}W~h3;do0xfp_J{_*kv0x0Oj7C%T`1zrFIQ4b%0 zfN+bK9e5S!?+XB@L7->6ZT}tkFH8PE2*bWQ5VBWlt(p#A)`(c&iq{KN+pVDm8!AwH zwt892@iiXQWB^t!g8Keia6hZD`>KH8b=XbC5yi(cA@qHXg+sqz!rjb8x(v5>>TzQ? zTbuSDmlw5qBtyIhxMHeBYqx`cE`hyRN{usPx8l?kz+OH1L8`koo$4=~9FtKYxXFfr z{>Yq86=4Jv_u^WdV@fP)GZ}|5jNa`r3I1~buq9iy=1nO!Oa3S+e}>nB09kc8@~m1i zYot^L`_9=JN92TWDUO_ds!FNw=rWWUjQYiD_NAYLDmNL+Mh+~e2uB1(c>N`hSKcpb zCtQ8QvFm%T4J4vT*w$f%u8k{*!qHc>&d!yt1;O8JC$KPKlh8{}TH~F1Ul!~pzqyVz z2PQsznaEXa>J_x_U1CKN3~%|vn6DbgT23kA7;lx;$YIjnJJ8%C8@O>q zsP7U3Zo17(WhoDw7y-c~F+`9jKR3@O^)fJFzStNiUGexr1AfY9*G9}hAe6_-TE%6y zwkf8+H1U^EGg+k3rJDr)q!~oP#~FqPP0d|m zVJhII{mG`~AtVR)_JX2M`+&4%XWGL z7zUT0lSB^%nKPkyd{URHn$w9&g*{j7CC#t z^Vqx(XP9;0WiQQYydFUkoPg(Ta+FkTkD-M_oo!71Lf7JYayc4?%QECb`!d8XW{1+e zAsmWK++2~@Ja1E=p0i=7KeKv2;90aV8wYW+>NSOmb$=H3KJLnx?@Ao3_@%sm-$1aQ zge<#P^P$kaS8>{`KgT^VnRnM>gU!kYFVt_%*Fae|girPNmbnF$QnqC2$4k~|xHN)J z^5w%-=&dG*X~n6!l1Zq^!w!DoFz*L9zPc{3dOE3!*vg>*isg90uytdO?4(~qtJcW? zA9GdmrjCtm7T3q1_u8@Blk$=A=69jy5xTgk$ryh4Cc4BaXq1b`kV;go6OD{7&5+x} zfSn5gnU3g=o!%`6O}qVhr!A;z>Boj6-o}Up?)@BmTNaOsm&8C;&4yW{x?nH;tiXdV z2E)iqLlI2+;G|g+3VVh{EP{1{B+iH;c2g}YT7BH=z3Xba82+Z5qHt^jStHNL_jcsd znvk)<6kA4{Ud(NOJS-C()V60URK2lfX;a2sykjdIFpVj%C$M%bm5uJf`;GbBafa5W zNV^$&J)6;S`*5p5PF{P7zlSr171!?ru{Sjj>(pWwIvJ}{HrXF+WcBS7J4b_ZK^17I zJaey20}icIjO5h}nmK4-tx$M14{{No1uUM6Xr!{o+3sid-x#u}vzrW+BH!Pk@n;3T zOgT9iU;6!^M*)hooBy0dV&kNEaR{>rA`vV9sHJ&QIN;WY0}4>;9GkHo9+vDnOBj)Z zGIRN8m&6kc&<0H_A}de)Md2=MS!U4u?yk<{@dIHd&6Y(ny>WqX@cz9{G_RkB^E`M4 z<`WjioaGk0wpu52+$6%Fr*i}ifUyRtHq?m%lawmmR5BL`J64iUD@{XykcFi zVjq5gB63#QIAwAInwt#ya!hT)s@-Ry&{jc&YvXzzN)^lT@C@iqFE?`;qvJgKGrdB< zI8Co!L)MJ(^QWcA!ZA(%BGGr`7B0hoE56=REd*?-^swNtPPO8g2F-$a41Pc{Uc_K@ z!rdGPGtovj2gPP1>S@pLZ1;?*edQ^1Y^IEKc?s0duA5;*+oTlocq6OgOEHkGf_@gI ztyB-sGSn(@f?aDp>Lh-_)Py={MC7=x9VppS0vEyA3^OT>!@$86h2yL6(x|1cw*(6xJ?qXE&#La8C?%$4 zoeh0Iq`a|cW007h`PbJ}+O&e1Q4#HdpT`YsFlm)nlX(Is;ybd#e#FDJ?P#cZV!EQ* z$-ki0`0J_AM+Pzb=S3quf2r~@ecOp5<}FMis5i+UirO915t#7d6s>Wd>P)eEP3phc z#H;yN>f;-DEWbEfFY+NUw7RFk+%RiEHC9Dvac+(klWl|gW9sZwpU7m~4KxpYd`aHR z;`?dQ$l0-5!L0Rn9yYd?sGeCjCB~t++5P6$h(z{BQN_IO$p*syxSx+wrOJYk4{s-x zK~Frnp4jUso1}kTpzCkKwyu$98J1TwY*LVizVyz+wzP=VE8)V#<8U_Ar=1%mf*TO% z@e&Io*P!D0VDd0sno3aP-ctIl&KgTZejCMXd4A1tQ5~T%(JPvdQH;ZJABC$OW#Tsm z0Jf$Pl(t6B_QqRNH()aK?w=<~f|`$044d#u{0mfPgK28PfOO_APQ)hUpoUApyDz>x z1h?q{JzD99Nq8K)a^f2v8@!iFuu<@sw;g8fm}@W(zHjE&iB2xc>=c(u9}_x>4d)b7 zteM9qQ1G!DYFPi*oRKZ-K*V-WlTxta_DWFcVFjHc5kWt|!!R#VqLBkzGjYt7=2Q7Z zYX~(H90A!IW}zM91FvkUvsxG>6o;cwY5QT$~}Tz>cWf+#=oju=k00z*wB#|`|hI)B5UBixLy1$5MM9>>ikZe8K)h94|cM@NUlO|`eBAby4na6D5^@>6Y zbNkRvD7bC!J_R8Z$kZT)Ze&LbHxrw=z7T9KYZgIh?dc-rs(#87Exz5h-q2jx$aq`7 z5hYdt{(F@}w|W3>4XaUYIZ9)Gm=$IfMibS1Sag6~`~e9sA_TmBg`BBDM{*3@usEd6 z$H_FU7o@|H+l985r*Hq37}|)G;K`?|_%Hq5nKoa!vWw=HTv&Uvsyj`z`cmjY`B&>0 z&7SfI0u?n+He6wUsY?2)gQ?&amB&M=BHqpTR)lxcp(;nM-iLTghORDI z8w01U$|pWl8ke5tW^}*qCU7!@_U@}AvgfMxV41L&euA z*^tTD42ac&_e2_nhe7&~g7#iIHPIh5@${7>(MY;*F4T8Wh>Y>aU(`^q*0bTvw(o=R3_gkG)pTC^jN#3dank9|h=KxRS zsVf`#l+mRJhxbPDme>f%9T5UJvwsj%A|Rvu(Lvs6XFhfuu^G~PiLJ_cA*yHoGiv`+ z8AZYP(vG(3Er;86pft3`{W3r;fmQkw@h5j@*&J~BT8}UAurRQQp#91$A&Z7voBkuk zEE=xn!=-565=_W$_LbP3@BL+#W|>@~*FUL{7W^CPrRIW!{Pt4|W3Nu5pe$tC&0tiPSh;23He?!& zQV-jG!(@-)nXYL*4pgNqV=F=)adcBOOd?Qdg7lc#1ie`KYUg`^dxD}+7k6@e#@rqF(V%byPsnl zw*WqSfBP_o>DT^DNC~1;T~U3TUz3xa047W#wMwTl$?EHPTeYyK!12=R`pZjBi{`n+ zL0-=`^J?0m;0$Jpt?+1JULgL9T69(j1~yF7pL@u;87u$ zIVLR8g#+76lk`;R@Nny0+G3~n89_zQjbdmEU5|*`>APZ#E6@T=b#z9UX57ibjsg`S z2z4~L2`c@};wl^}1EQWAGzfEoIjAU|>Yq8UxF#gu* z4<9_U`wLK^O?~M|-sfse+&d8@0|f=!KQNgDiXGUyYkR89uh?ev+gubi*7)pJbzNt4 zO=2=Lqu|y@C6zZdBDdoEovWL8{hTEF0cCtx z^F=J(#w7&|6`Hsb(wG53KtdN#40vwn(X%!65n%~$cohqw1Yv=IV+b!n$=@`s`S>nyv zUF^&N$Nb5U$%tSd8U|~Ypm(F_)zFIHflBEMtyw3>;;^;OK zdbc~)02Db*k`~{~)WJaHzV|+;UALlAGH2-=ll}g%A1(Pep%ALhTuAbOIl18mx+trIM^L9{Ekxt z(48S5t#E0WfSpP61n)l60=!Cf{|z;Ta`#Dpqn5@SKrj#aqFg)Pg}-u<%%Bs!8(sNF z68%428nN?^AD4RT;-k7PFYZTo-&6V)G7*LGao|v~HP>rG$pVSKYtve~%9?TIl{eCWb zOYLiN&h$NktL>(Uuy*@WwhVttP4CW87UwkTXuAy6Le0m+S+eDxU3j??tsbJ9}nOJ};8aWGp)b+pyrL4odrMnPj!ZIJID`)PjKgOc>d_{5%}qlaE6w zhW_FY_6b>Doppmlclp#c-n~dsYhI7KSeNbImzQEH6z2NA)49}Ho$R-Hq8`irKB#44m1TD;N>2r#%Uc9K3;Mvrde{ktrp}*)T`8!cKsE_P!HV9t( z2=-~-`)ckX;XDt)`HQo*ia~z57f>rZ1X2HPHin-F3e}iL~$mpVZq|Yjg<8S zAtO2}efY}!t3;QQsR}(D5PfwN#A1&^I7%}1J0|+xA9(I6YDX;)PIhMJzi- z(dVBuwdTAVLxYZH`Vo+buvc*>QK@O>g3>xBENeY%sLLZ0>FdqHM&(GzEt_Gu6(h^iBr3qc3GdwTGLT*k6ue()ZIjaeUgf%rR7u#%WVD4L{ETQFjj5%YuL)Ft@+{FgcY?hh z4~GRl`BimI@G2=@0;Up*7qL3At8345r_C@aHJB*V3Z61|f#49RD{>k_=cBx6fa=0U z#;?gl##Og-71dbc~s1wU-hE(vXSk&RiwV8NN zi!s(eGEmA$!V6AO%jR%~LYV;e8{@3{4{O=T?Xhm}Mu;|p8!VfjD6Mwzb2L{cPJM4q zX=vJpcCp6E$1se(*DV4Kr~CU0YTRs$=*?e|M%j1daMKRQ`(ZSk?G6lfN^~_)b)(HY zmT&?y#r?IR`z=R+UKH<`KaomP^WTzrnpyhYmo~|RByW@EXK@wxasLunCSuFz*s|)8 zzq#jYpU$;}WevdG<$ndvfEVku4QS@I+>7s^>zz z!C#V`+0-OeR?rHncY5( zicP^WE$?=&X$1d`^Ahc8<9^(a))<{ zdFxwlktxe&AJqM4xJsuC5tMnK7FqkBKPn!1;84`VJS9_S&BwLVEIpRoD^&teObuV3 z#nMTWv+-NhD3q{Y+Z4Z{O!#2~HiO=_%Luz*HVC87;}6izTAaN>(Gb$ltXP!OG$4B0 zq$aqK;*QyhnIpP2Vz3FDskpU~jZi8M!AJS7WZ72g{(ZxOpk)7QJjJ$nAW5AWUDMAa zSmiReoq*2Ds05Od9NjpK^>ZLIh^228lTm&Pp97s_=man}WIdjZyc6Hxj)^f+Q}QtY z@{8zB)c%ZGeRy=vJLCuAxZgly^{2^;qSw=SuFi|gL_VGN>A^f&$+B*!w+Bj(L0V*@ z2{bRPU#pc)0oyb$#g-iA=tKlgy}R~BXa;T_G*esHrLOEyn&J&-rOwuRWu7UOo4GhV zzaN(@H@jBYd`839G0vdDH)!)3^mx-r#^l3m?BM`6^j@o0SRY9Pah&yQCO+J6#z`ql z?T*hl`*ttr_!Ur88ZwdJDAk}W55#zXfgYE{hM-sMXMQx!G{x!^yP z1jE;VxyUpv_PM0wYfX;18EM@2VmwVt3EsHLw|$$siB9 zX&|U;)tAHX?=x{qbj434s%%MCAhIQNN$TLu%d_ zqDUB`#Ch{u_fFb#R@8EYEq#47F!OQ2crK3}Hb=?C zA`kMZGPSeB0lBl@Upaw{IjfMqjO&-7udw+`pryhHqX=8@@INvtw6=20m9iq%Tbd#? zfWBqLnO%vEX98ua_?yD_@{BMwiTH{-^#GQd;MK!SEPf(p$a1$vPZ->wg0$aiv@G~C zx2vV-^Xx@c^D=~qIS9Mz=SoI+XBj6-K^V+*TOLu?8^vznvE0{*cb(0otWATz1dyup z*Jehtoo5oN|82Y_OO~DzZQA?;=*+$cx}U1%Ig~{?8HDqqR&ZSCa?~bK+SWJ|os$RH z#yHk;vCd(IM%|s6(8DNHann%&2Mp@pTe5W$@q*9mLnefx&?(%CZ|$}TacKV(*n3F~ z`Qh_D>d%#ZB6XH{!y8rlw@*xtusD4m238AXsqv^Ogkr|_^`9ZsPJX@|xR0Z@9HCsW zE$fUUHl+oeC5jc=uF#;Nyp}mux5<;V!@i(x9Wt49jmR2)&u5!S@pdz*&@CNO;}8ls z#A!esvB{Pg8vmy-)-VyiDYvr&v|NbaLiPGf69!ebF|4*{h2eY0C%iCoe4z_GG<_m; z7;;TWbF}SQbnt>>es9pVh=5-vT0|6W>7xv)sj~Jj>**3bB19)*r*>Fm-b*XOC)Bw9 zDxIVUpYz#~OP8vY%(a>>M&y8gG&J9RbU!!H4=6EeM#+w8fP za$fUs{k>grs74rtEjSKBtqawgid+|JCO}52#9`qRZ-OkLh{{;Ozn;yr`cte_H%1Xe zVcPZpr5_!+d;~t!Sls0^+&DaplMZ?Hs1xoAAi#Lr3M2suRDB zd-izRRgKLOo7U}84X|sdqP(DQdpSzQzEt(1##h+`Z^}@xcUn5(9(7oW`5M-RXg;b# zWd&irk6Bf(;Fv^P+!<37PDQ8=>?X2l9KcpRwAd>3Qr`WlkA-Dh5t?4re4 zd{zuW-Aip!kh#Bq2tb=Z)^mu|khI)2vi+RK3kHm$Te5Q1RbSHxCMVxmPf9bV{(KXs zYgcHR8oWM~sftfcuV@JlVqA&e6f?30_a^;}^3wMm#8Exy4WXl76eObU-jHh~96=TlFYhZ4+bpR;(Kq5D)HL8Fnr@NzDoE+L{^f~}y`$g8gAaKKtNR)pC>;m-kR zHBdf4T#KN`P}Webkl7N?u*sH`tPC?&{=*uKxb33#gc4i>?tg z*xwkj$!vhaAAd~N8fw|5F0BwDi+lV9L*_4=6*JK-kOt(3i1kPrAnUpeyC-0woYQgE<HepXb}!j$RlW8=IP%T3ZWg-jtM-l$Q3Mt#z}rvjdjr6wNd= z13tj;ix(3z`erT#m~5tyXDh25)lpDfACa};ltg`}+DcIm<2T+_9~U8@6fk;N@@Z@L_93)yR<=E#d>~=&KX7_zzsQK3Mg{-ft-> zDM?9z+ta>ct9m#A?S?`Fdsp547;t{GywQY=!=>HL$|UqV zvePAyYP8mmw{h-+X$nqGPEk=&3qWz|_&9Hd4<51gNv!mePj$F%zadunFykCjM=^Z2 zya5B0a=aIs>pb38OU#Z*Hm?3U^pcZ}RbM-dC_ear#5uUuys@55c5dge2bNF!y?jYd z4$yiCKD0pU6vi^v*!VqdfR-k(I&1$emvE+H{nGnDBe>i9Cc@h|OV)mFX{jliqm zJVQL9_wR|*7Bf6Km(UzI?A_)c_cR79QW2}a0vN^ve*{^_4!z@PmjngLpEwhK9X|!w zrVIT1O5wXDr=W9v#wztMp-$gQVa+*9D}fTlmZIC5iZFIA^y=0T!zNh&LE-YxCZhr) zA>)G>-8$6w7vQb$Y5p+%;M;~)z$oWZ{gqVU+(<_9T+@#fs200R-`U&FioJOom$&1W z$xXDl>vr@rzcj_ujUr3{GY%w_7>*NB|MF33{*+b2c{Iy0rG_He=;eGP^0fu(1y=E6 zjCj8I0Cd=`jc?s0LiyH0gF(*D%Z=tJ{CD>49Dq2HG;`4Q(?2dBKL!A`K#W#d$^3eY z*xH&O1+FCq&fJ2^NV{V;I{w#gWMqau@M@EvAe&^#6Lf9qcM5h9>w`&6)%K;EW~RZ z6DM1Z7Tyk`T3GM&jGC4KG882|6+;>z0_9j60&RkY*m=|N;`U^|UP|UMUz<+d>!l6@ z=LfK zOa$uM@cZnbMn-5Et8U3x{B;z`0tgH;`_>lOOq@+^fv#5=4 zaEM6&7N26dEIipV-ar2kj>MoqN=5zng zSzs%Z8y@Y^4uH~r`>nJ&IXQGXeQOJ_uV!eCdpYEuW>vZw;ko~9oSH8%<#UH?`j>(Vm-xS8F)_Zyt~^WyEF)oB_bE1%~ikod>$sZ9G%{$@JHO zKkjh7F_JxT#?uJ!`P#T641ldkAx@$%*OloWo=$A#mQlaonFNf$3(B=y&N zFU;Z?;c5TN-!iAl%#EKtduC~gNOA&z_rGT=`LvE}v#Tcl5AS*X&0C@T|EGXqe&y@s z|FPiB|8eK8@wEK^12}w_MObjLx)8klW)Hh9EvX0pRP^G(-|QD(Tn}Og|2l6u@$-7x zDlEt?rQcebbe_*4tVZm}=G}ij{d4ixJ8pJF&P{_b>_RGwOF{8-T8{2IC1U3nq|-w5$Pb1$-g)LK%kPpe}mRRpcij{U;P1rivIWwUk8Dnvi~L(1p0CMH;^4F z?MI<#6{{X)NXn~ih@Ai}`g{PLoEQk-LVW)Z^!JDLW-<9Iva3b$ni$!rq2EiLLWqz5 zs*#ZxMLFIc{kmYIC;8l4s4pPI0v3NNW*;ANs{Dr}9SQ z@`*D=nN$Ar^GF(*xFD>=Xu9Omnt6DPj)sy|j*+fcA#>W=9~5OD^MvXCxV=9kqFujD z*=wiyzJ7e`@iCQ#T>l4JfkBWFqK50AAF)D7xseC!v^ySj(#`xUzF0$}dnE^P0_x1o z!mhZjb+~HP-KD3!YG!%QYyByGj7x+Y_gTvx!*Q;}Fxw-Zq#Umr=j+xjtSU$#(}V*T zaS56b%>P5SnF9#|3Cs6t`czg`RaUu8(SD)NdS6CHboLgeu`_OIT)o~fGz^)iJdk03 zQMmuwLc8vKkjVNA>kP=fYo*#Nm||% zEGQU(n6JZ={%F)5b5+p1*%1&j`=d}M7Hnv7IBHaw-dgXx{$|Vs7QV}%I;?#%VFbms z?7@7?ujb8q`5bvB6Z!mbI8v zq}_~2TO{e_e9Iq(l2KUkteZ+9+5vkvk$x(<%Z0sPVcYdOQ)3l(}!*#*CCrrt{QOidCjW}eM zg9D?R$2ei#N6pehWW;M9L5$_9OLRAcr|x5i?Z>N2&v08Fh3&&+sMqhyx=oFquAy7& zj|5*Tw~2v`iZW<2&gC<{lEbdpy#7d5i^ARcO~V>F+xbHZRKueBARx8greZYiutT>(|9*9f>-sDNSLy|s>%LF!s!On&bRLpy{YxzGY%uvf`EF=bwXZ-0d~DiZyc` zH|O*8U`10q$r>_Q;kUf@at2c`n#_jU62_Gy<1SCW;Ixz0w{3WZWBUK9F86Un*<~Wj z#6;cw#uF4hl@rXQ0*)|VG?EuSwA1A8^_)>ZAWnEBwO zE@$?IoivJdGwWxSD0_N^NSOgCx#?6iSKrU&VZ*IMgJ}Iv-u5L9+69Te#^{ed1s6HS zWs+}te|?`TcoiETavfD`fo_Cg?S$^*ttcSo08j-mdx0xp-tnS5LVX=I2>29#Fr}Z# z&p5`9r%_LOpjj@NKA#VLFz%C@y`ihaBQFnwaTUP<(S9dGXrWLZ4INT~< z9!j4h$Oz5bPOj&N5~EeuD#D$%Q3bW02j(J{zWjUv%uprGTZUo5`{IQsAx%LKz=dPA z<}wDYo9C4&%Y9^w+DO<`Go`#NMvm;LJZ&|{{?-3HCj3aJvqpniS`(8&`GwNxzfyTG zGzTRu1Hp=!F(8(i$K%GUdg*360@P`gdM!o+pZiFJsrrM_SGb+bAd^CM=I)a>5#1(wIan9x=TN6pbG>n95Gtu#Mn%DrJS8;LlvKgm z&G&iE5T?B{&Vz!xR%KOB)65Z_whgPk)_+0z3W1C_AB<^B1s%*u`H!4NhJKZEvJV`lboAN zlQDdwvicho-cyNJq!9jg!W|a-vnU_THZpX!xI|a!lPoYQ;_1uoouw>xJ)6rc*uu_T z7L^Kt&zlfYj1D=fm>%rZUvF9Vs~z)t)&4Oj12KH|_fzi497Wo)#BJQS@-F1Vty^Aa zA0Gca>#f-ns&ke`7CPv7+Fg=c>RkS1VQ%$fPrO~+r>$bk1#QEf$;a1L^(>wBH2}Cu z#9Z$G!~*cvr3%{f%y6gOA4C86wqq$PH*?q7Dl+$*FU_gBHW(5XW5{sWY6^R?W3KFy zUV&>YoNaXbbhaY3>E4X5pAok7RGF_EWA~-glnc0}>pmv;tu`k`^TQon#*mlVt1$`h z{Zwu_T(=JiT3x7P61LrOT)n$D{Mp^n+*i_$nRAa;=3b z>6;X|Dxg!VZ&S^9RoE8vCE-ry98w8gxbk?8uS(A6@&KlonE0H8@cnQOxIZ&~I{U?h z@--x{A2W5{FgI&s=V=p^g63Lpt`k(>V+lOrR~r7FRJj1P4-WPg|C4DSGX96Vi@FlA zsz=eQ!=iqN39T~afieeIUjexC5qW{VA#fZCKPCFO!D;iP7s};ELEgSof${U2>b6+x^zQqx10$Hls}eXHU| zi1|!?65e3c$mmO%&n@%j`p=(S^N?=E3Kr!zRnnS?0mZLRf9CXhO$yAn$EFOof}2^j zx3rK_k-Qd}QyJiUq!G$#YvelNU54_DIQ+&R+Q2o&mAh=($U8UY#{zDzgw#E^G;wgQ2oJP& zszrXPsvax)-Y;|Ua;7CW&_*^}aBPf5U(ye#azb7cK|F15fC+I@w{AYEV)-Ni(-cY$irqgVB3kk- zj@2g=Moi$N$|vQ?Q)_EB&5WbcQA>-P&bhkhfXld!c5eCTg(c$&WM{k$A)P{NG7);B ziszkD9vFPinQ46RPx7Tyj^Tx+@*J#bdFRVobQw#^&Dc4d(h-W`&)2O z>ixoFO4OCj`|tk9l#J}c^pF2?T;ZEC#PA-M&D%Ab2;QW%28J$+jt=t4;O-)5!(Zfqm zD7~@$m%4pa!?T}DtD8%J)1-OAaHnRiXQ~on{PqV)X`%DXfbs%nJh@epwv7+|5Me!4 zxw4VMB)!0eX>zW#%5>>Vjy;5`wN9?NxiPwpXw%0thh|iW<55i5Aq;NBS@_nMt9PtJ z^Sq{_ga!PZPfO5oEc_Bu(Rl~DFsDu@^4P|D2aJz&&8tb^q!ub^J;gB;_NMAeIgA_s zOTSH8U%DII$eMAKE5X8z*)R5sw9II*G%L)F;m>mWQ&v5;l=dqxaQI1I=wYJdMH_}j z?Wo@}=XiuhZs3l2D9c~zhab}ud``9KKy8AmM3sA@Q%D zlN#nCBf&3HV$+od>DQ-bRpm>98!lC-q?n$2J2zEuHRhk&3y=Y^@ajUt$AM>pI&UKg zpDS!{>z>zamIb!-sELN+Y=fNyvWl#!uBKK5jR23BXbcT`sc<^NWr)R&c`ba@dXL-TEt?McWm5AniwIJqq_j^U+fsajr5+my-H4C)D2XcI32c@o9v( zE?ZK-E-3lUq+zXw7X0nVN2Ml_R77KFo2MhREM`(}<>>5b(U*A<@rSoR~K+`l{r3ZWVDbN~vH$fV5X@usNxaEH5) z%g0B;Cj0tUEiU&jea8SL|oG<<7V>IVeWhAR2OVL|GVL-OYu&- zB+*{GU>;j}2x5L~{8OM!=lW- zlC@IdHz8dsn2qcCutII&v&lzag3L4qBpKTJjPRH3-A0hg^2g_%Jh0H*-+OLe9dr=T zFsXcxUZ8fD&bY;H*-9sWW50#dKU;AWri)wODLI!An;GjkgyloXM&L&e#J}KDz1UQO zxIA3ds};%@D2OnQHCViR;J8lQQ{|z~ z^y!19TyY6`VY_EM?6WxUIp69b<8g~&W-dZG(~`c9$Cg{czEvKUDrJTFb}>I;?FdWi ziJs}CO~IEn{(GNIRC!>7gAzUcD$U|SQz^D$&Zl6>C)xt7H1rEhi@)6{ekHGPA?wIGW4 z*jekMR%UH!)#A%A(~r8YXO0XNRrKB`nV#2l6|4u6314i4@^#s^C(R45EV#<6YKvN$ zZ=xEHzR3Nt8{D4C3kv>AIemDHo}hvDM(odir!r&0ts7U?mz0#;ul$9L9uT2OCVVAGK7) z%8+)apRDB%OdB!D-d`o%tSvN-+Ve~=E%!_o&qb-xCJ{|>Ek_-3avH?61}JdXsKxxo z_5PDQNa~R*7QDcE-`rDIZB>0h(n9oe4qS~!C%6xL)dUYWs3d`I(%*X5aXze{Wt%XlmvFL3V(&NPjhboyu z`n-=mZO4h9`4u=bwiTUoZ99^e(1jY2VT@ntR2?LJzI-po2Qfhjl>E{KS>&kEhv}!P z(*_&Q_qNOB(WbL8Q>bx)pOAdt(^uqY?2Pu486NB6DU(l57M?WF1jof5)>iU9b2#a( z|Ck!q_sJqVc+X40Z&gd!!q+FbEKSDSGPuiU!k@t?6U&|e0{N`WO45t|b8nXz2p!sI-rb$udEfhCcV}nbd1vnVbiSN3&pA&yzjpo)^Z+3d2RP7-yEXE)&oRFI{E?DP z1FvV>ScYD7LbB=H;u_|n?OxsU(k|8N2Y$AZ_g%_4!oTl;y$(d&JF6?>$_nvyl!axu zw`7~B^ia@3dVf>ZcGPVZsjXC}i_`AvBI4q~1Kl*!5+!~(PTTB~efQ-mXKon7@t^uE zZH_J2&bR8=nts=Zsrq`=x8>{Xa~llPL=h&e_Z0|kEoqNZo0=(MsN%7p%KReY@P+iP zVSgIP_c39r%J&k$`9tw^KyrijE5lFrM7A7h(wrr-;Nwo83ixeX)tTrB+*p^%P0@{x zXU*v3e9do8p}*>i(Ufjj$Vb!uI%{^<`mgAT)B+hSe-$31`ELl4gfKg58(F+gu@gB0 zcJc5dBf!}yd?(es|4|AihG%&4ab52#Tu55#?8wp5AAe%e49@`UdxQCt=irPGzy2I8 z-L|O+QxiN-NS!qwbDjr^2=d3mWJ?f4pR~=X=M6Qf+uOqiS`sAuH#^ zegD3!Q(I7V?w2Ea2dkvh7K=+X$9@+jL!r5z44g-=Qnf4+e;=~6iu_>L*oZ)mf`5Eu zfJ~_vtY+RHPd*pgL?2^{(-!h~x!02%;pE>A|MU>P+bN_7J>I>b73fH_|10=Ijx=-! zgS}hD4d)ouInIV|C4hfN|8Xv4)#MxvSt7>VDQ^rEF~xVU&cd&~r-$!=OBo-s{hPwS zUmnc4-I;7^kw)7nSyi&AHWVEoAa-|Q2lnZ}jDxnRBe)d+1?tHhu?42jOC26$$4Hw^ z2zy?}8QskAF^8Pvy0pXSOLdcF6kI%oadDXvV>Qxi@;*cqgj4&t}yFydk&8gjK~{T2uG}M%A3w3Dx$*>g-FM zY0x|3kd=@1RyylHIS0Le*HAB8&LBRgh;fgnhrNE3u-J7lnU1%XdotWvQ?eq>^`^_O zI9o*8s>bdp6E2VP1>5`*oRY0W(aAcNS9rgp;0z10d|d)R5@%((y5KTqDXbQs!Uqm^ zvXv@Nz>wB+p`6rC4A%B?S*A_kNR2xbN(qCx@RuW0)hMcPFgaEa`_a= zkjI%P!|EJGa9jV1%`a?%(nA&t*}~F43vu;rp{fa$JNjL3Pud{QzEzkll|d$q9p3Ex zhcGg*qDs;mdd{|*go%DFh{$tXiZq~?q845fv>SV0{dX$JMGE~iWRpNQ&D|jY4L;sz zPCjACI*TXK$~0}pN`PB8$a$2j3CwoiPIp}t0%UYsK^EV2IAUy4yaAZ;L0n~CTo!W+ zfB4sppf8}@GDEtc-@V-lLwLQHK$SE$n_7mc-Qz-}f8dBUfgI?so7&yvYKBvhZrny zrd;~a?fz+{U5tnZnBI^AYUUPJXlo<$0Lf&j(^6{JG4SPS7_(fAHP8-H(U?^8^e|Zh><|(2;dwP! z*~6LA4z47+!~AwWypJL;uY0Um_q;{ZpWfnlH}EXZZ)3ofatS1w#UCEOy*z1bAV)0S z+9n0+eDd(|t2q+e>3XmBH@x!rCQT?to903dTf!7}aB#^&l{DOe^Ze@m<8;Bv z)V$ZWv35}JLi*Ge>q;9ue1<6>RaJ@fuOScb`*D^Eqx&`~So8c+gw?g`y;n(E!YWlO zmJN{pXgU-LL(|+a2ffxD^f}4N@x`KFF_b4(0!&62yLvIXO_C# z+XgA*;%{CHq^lJ<)0oVoAN8U{ME9)f0)Lig-M$J4`*w#wv1Zu6Fw&4WMFvK2IE93e zj>DivW*urJofA!R+YlbmJs<|dY1nRb^t21NZV+(PD$!(HNmA=w-?gCG$IUTFh8}5E z2&+=9Lr-56|I%uP#vYhddQHHWQTT6=oHH-~N^Y>aS87}DDe)!9DK0b?_J+$3Pf|Vi zCF2FG(@suQ7}N8@gK14sX%c60FIkT986A1ggYxz}i#$bq<2ZWpkBzZ||R z!1|?;>gej=JDkkdgrvz9br35I{ga{}V&6wN?_1VuBPBNBHT^UA&t1T?iVj`i*Lff0 zWrw@A^7sqW6&_U#G@-Pbrf^%LU7?O%PpMT&%^zL6Act!fiRvA^KS9UP0 zjN5#;VGJr{J|8cnOBvjUo0HH1^B)b#{x&F-Q$k2oBJ|*JdR+}ZcS*s2n`tGf z(E(syakl@CAkak_j?d)#(KJvtm`s5s@kC~!kIVZzA_ol^s0ojb!XIcsL9)UpQ}Ky_ ziyFJpQ9D(vW}xfWqFT$?)+GId_9Hy}%bNNNpCt5sYwhnZ%I?iN11BM(Vei}~RcC@w!4VIH96Kk= z4+rNkvQ;wmI8dam3Rg@eG}3~zK?|GXwTfKl@fFZuA5M-zIaThiMt zYDQMV$MEAqAz_!$CGMNr_lHiN_bFru98X4^WG!3J@%oTqm)d+TS797zL<>JH0lCH0 zZo?T$p5y@0Zcw0cQ<@~HtV{#+)!WCbeoJ*FpARZ=;Nxm-FX|e*)@!VW(WD(kA`xDj z>jTI_a!1}n)L4=2$4Zj^=Z&XO9lDsQzD-i(xET+@za3_)og+F3u1`bwoM^(Q!W3m{ z>SR7q-mCm|0iE~i_^?C3Gz9K9&dF_ z?i%XvqeA|q$t|g}-5WxCu9kH{yV1yRL78#g#HK6B}#Cct)cb zH!HstE19h5jJiG%K)DZSYj8;Lf_>CxpPqsKBV{w^r2APJK1(-7Ckb0)CoA1-3932C z!%I9&XxT^kvJzsRgd%me?icj$+Ium3OOq`Nq@YW42M-l08XMrp^r8#ev(NWjafUx{ zJhdjdnnvxq0ScISa6RVNqiV6vSXYzo#K_e5iAGQ8rz+$Lg^a##@t=?;kL7r!S&tjw5te# zhq2OxLN%z=A99DprLao$@1a-cDvPa1M*;lY6NT<_dHc5uBv-%MYTKot94COu)hQIk zxR?h)ibp=<;XfuP^wiSCiyT+Z)2gr!8>cn}4c0GoW=rgBH#)eE$VlLNu2#NVy=1_> z1Txm$6Z`aO_0~i2{E9=fQo{=Sbj!iRj}OE$qn+j!D^v(R5qqM9Bzo#O3qd*zu(gE4QsDHt!U zNjuhKlesEiTf7d}p9ZbSI`p>4MDTtVjN3uV*#bQ4p7;|~i)^fn@72DgTBOWsB^~3; z(623Zr=5j4>komoht*`f(9p`^XP!|Z6O*mL{I{4DD*FC5M&wg*{4o{Z2qTDz_7@3~ z#k^dGr1zg~C;GYtaxFvH;Q}Nt&bfKFPLR$u4>00#?d2 zEqbMRoE|W-A|`edIaol)n_+9uc5LW33%H)3dw-tT<9iDEz6O+LE1I`2?+ zt)+BOeT}1NRmJ=|w|IDbrWF&%t(iO)wGOuM0a=E1ZY$!Q%QGyi?l~!m7JQ1gafv0D z*|`Qgbg#EyRT0U$^Eh&ztm| zSK2VxfRkN1;mi75oeA=`!-nl$5kUpm!m9gV&pOzU= zE%-&Jv+_pURF{$c01XXez^^?5z{gh;`s!j)u2il>=6bv1y3T*O_UU#hlJQyG+gjii z-pq9EDtz_+bS<|^pNoG(kFRlQ+G%KJ-iHoCedflTYj>jx~b=*%|wCH05*-T!&Wml`>GJ}qcLLpHP zw0AjgnV;#}jb%;+eXr=<&){({O5!q{vc=skD(qeeFn?K8kX#qO!!?Bo2(n!`XxOAr zxj$@}IWS6)tqRfaoSa`E7I46BuO;gxb93PWs6$R|h;yeXodd~?|BZpB^p|i5(fd4l zPY_6FaMHosy3lKF9-MOrb5UE10$z9V9?sQWXxj;bO7$%!u^TwnB2Z0%aeK7(S*d+! zP|{fR{t^*_y$lQUN9TQ{xGN;yC#_p4eeU`xlWQLu!yj%!O>v5{LG0w6NDE^IX0lj2 zK}|Yp1u7^8(66EQ7p*OSl9HNHCLti67Q7%jX!&d|KdnNnVV+qk(8QEVkNJVOf)Ajk zh-!4%_U=m^J{^wU6_^Fs?73TGvoCoosX;Cw!e ztIlm~w)BxJIIP$ES_OSr^94l%4$m8qX22B?<(G-~U8-!(7-(Fn448)eCc@ie?Ate6 z*AIqnz?xUR=KVSiN);g!uU?*HMoj#veu-geu2jM(OL46ko}FkC=sZ^Wnvl(Y%{2t) z?aP7of?P@z#=dW{CGUm_SLl_jf4fzH0hVzH|Wybg(&2((iR(W(!y{jgo%DZ|P z1S^MyGqbV+3hQfX>gscI%cV$akudkKO=Zbg%Rs3z%TvmBLOFqORcNj_NOECquc-cH zCY4w*?wz-Y{U*)Wdl~;sU#Kr^p?p znwLgH@0vzOUvWR*Hw>H5JYwka0N*I^&xdK9ZhWX%TfDqN&h;3n3K7JEK51?k)~ z^S3KYaM+g2spryhQ5Z!q7e(RuT(rGo&Yk<4lWv*`#Rbbd5`g4Q$2f{ zVB97C!U7Ajj9bY$_xsvkV3x18=s0jW{rl)>LIKkKIsu(krbYK-5gax&OEOaU$vOnJ zjEe;`!EFzFUD#+jwR7613bsKBTq<)Ca#J4l(ini*TLSjp zr(JX0W|f$7Q?1a88$3yvyU}#;<%^aj;MG2QZP_2~o%FYE(8*+5lZ91WZc!cK-AR}- zHDZ+DV4S=C_C@z}ciccVD42v2`5T$yr}aA%5%Fw!&k<$$(!z{;4IZmMbqp-gi?Qwb z-a=9mlgcA)XNx&rDQ@W$9y`{Ys;!KXE5P-U8cnpy*qIlyy`dz>?_dzAWv@za`?;^x z;rroLq>8mMuiHuhnnSuSqlv0J#?zaixsf0_!2F_KdUBKlCu{LYc>9iP@LFN|4XD&w zYx@Xhv!mW5@LoWUvvU3DRVH_=Jeo&#Bq?5cZ+P4Unf$@YqrcKt4Ssx82AE-9(Z9DW z$GcjG9Mkn&91QF?Zc<&;P@%e68vo{N(KMT(KIs(+STo)xNDSBs^{@bi=~FTxvd{Ei zWR1igd!%M6m%HB6nKKczUzcNI<+}gL`6(xRIMWA3Kj93CIvT zWCE>ye|eFn^w)w%J)Zn2N}bBo2`$~Cj0ysV zFxY}1voab21R4PuLWD>dLI?^Z%mG3YLZ*B8SG}rN^}cuCt5@Hxx>c`F)!7`vK6~%8 z_FBL7TkEXwh6B&_|Gq=o_QsFJvi~x%wE&O zL-85s0&Gym{%eSYn}$`AhE{SZfZf1nGk z#Dx6v-FH9yu!kFKff&g!jTp3WPrarWS?zyFdGl&!P0V7#{rnlD$Tn>3 zg^XgUe;MX$XHSkrlz&=zPOXo&(v70G zI|@q>Bm3y)jhkenCUJD{hOc#op_XfjU2^uq#QLA&ekZ4mkD2N0)VY30`p?-b)1AZR zUucRab7^Fa%$B-zd0^R7wJzzP`rx5^{j*qL034VTDZI&(lr2ZucAG~rv)SP7*(1V= zIy5V#gGo*==Q!YxX>k=3NKFe3Dh^vOBF7BO7^mBGPEHybkR}6k#8?iAlS8kJw$Ou3 zt$h(-U7zA~UWj*t#3mC6D^N4a{AULjnSO5xSnTJ{XQVUmvhIz!Xm8=h07e=aJ=@#U zq(WZX*M#?T6wX6gp9Iur-|)AgFwrQhU0Z;h6-Kx60)NX!%`wA+`- zs2U}`i;Zp*q>m!GcYtlsRG7fiG_Tq59)4b43ITzZOKD))uqOlK+vCk&Ddr2ra|8XTn+e2=+>1;G+)hT+-&xtE0T025Y z(_g$;q#k1MyLt|gkkRZU%B=cZ2|q!axtfVL6rwFQ-X-R-8^c2v~ROOif%4M!Ota70Ii*LGo3)^Ju-~!S`-MA@{q3AVQ{R<`HhE+5y`h2B91-sqq_2HNu8(kXAB> z>#gU412@Ay&3>3P553=fvVO$7O2jHqPQ>_ZkgEFh&F8%_2SDwrbyn%1z=8ovfq)1` zZ>%c!=dhott5JXz;Hz(|FEfrL$Ts0{`?B7dL&s=WHcLx*YH(*S5tjwg+rrVh#;L#knF}An18X_eR2cEA zpbWRsZ{Bq8a&9A7D(lYX zs0;f>!_b_zg{cX!0szc9G5R1^71W)K*`d;cu}r*OhPXe|DRc_215_;(tQMf(>MwGbvR>^x$k0 zhf>ov+O`^6lvpAeN}Q|0V$K=V+-6N`WE2^ag2~v~<4&dU1r}#Xtq>p6#ANBdw(*;K z{)cyAuLj1p0r_5QG;&hG3MAbWP4X3)4m{-uCIx+t6gAU0(8Br<*qrsNY{!yR+lchGdi4)KN4+5DYT*z=3040M2()FsJaqO~d=7g# z6})LPH5p#_wiTT-9f`%~Lg^kdX@j3`^b%rLE%r`YK*5;?=r-S(QYC0`M|Cm?LNRst zov%bQpO@(_a1g*ZB|B&2>+RP9RM3IM!WWsjfjmi4X()r?W+%;@ZQWa@8iRzdemy5B zo;Z%d6}G-x@kWw!b8&9@Vz(jj+mld;|H#5tJ#+D!-eG?Md^x)pn=d#vTOd-O%Sw$w z4uC+h_tRD7?o(g+W-PB=2NT1TC@Nq9;4`>Iiz4dc+fM$}+25FPM#wmyQ_b2oOwSx2 z?vymaDda{*;Kwj(L9Yfw#D3G9KYCOvozmXX<2OBhFnXzBFHX2r>Zmt|YpwvTH-jcgUcX-L`hqsR?f?ioDioV#pV-&Ud@7(M3f z;F=Syqxmt--mN;0*CZskY77yxVP>DOGn3ln&STG* zOQszn(F)U-6sK}Hk{MnM4+A2GdE`&ztdiT`WuhJ|^l$s4x7HW1>j$I7eUCi%$CqqjoGS;q z@v0T2K3x^$*;Xe!^CjQo=lKN%Y;N^!(fkES;qWjna&Gf@fpy_jYuIY9dP)DY#9+NY z96A9zevGe!cb!!t?$6ofA)9Lce9oBL}b$0=Sk{z%hVdV>H7Isr{=SB~j7-2_bF-ozr;(=K2idHUvHRsX!km=?rNWGg2HRoyM6Hnutf zL@b7^xdV&5n2ipV?7g+}_Liq)d# zd-u2N?E{#ePhsXL7x}dE-iKA^Htg_$7KtxW^j0CAd3m~l$s+i=#W9^UC( zR!RTa%fqGCd;}6Ki#-h&d)RtlgSB^2L%pWyh@(~1uE zOqRSCQCCP>s|?FHlWs2Dno^su9$UL!fQ|5wZ>*&6hsl=$|-LAGCC~zFav2x z1W0QPL-k-hOos7ncFupgLmUU%0(*`Kt4@0%)Qp4<8JT9m%~DxSVPO+nUW}{kr>5Ht zP9ISSguZeb5^rsirnk~9B086%w{guH(}8!MM%M&=Tv!uo$HCBRf&h zIR}ECATJhM4MB0gFWfd+oRt!!v#nx{l@N|kD6iE-GOrxEon3U4N8OwcUchd)ojeZu zmB7CbGT)e;Axsa{Q5zQj3}1QAHRA`X$rb7gIRM#I^{eDqT5LA*uLQQ0$`6NlXX^XO~VN*wq)C|>vEgSGV) z$QV_b5$nhS7-t*R2D6%m&{59VKWJ{YLRoB3ue{(s&mJ^VTZrN?S~mEHRy}UW zwkE}&7)oQct0Du6&jT5LBU#1$(hs0`3cO~|PPEae$${3!j;rF{a~g=UTES;dc1%F9 zeIYqV0yR7@QD9`T8w!wdkuD)qIBz`rV$itWI~ox(^f&_DFFXY^Ys;C)y9D_&sQvGG(z3Fn=%R zn1+iS5V!Ij{+oi=(DsQ21Z0aO6UgA)Q3bWww`7+LE!kdR3Yfyl2kqIiUXbs?mvCurXP)fo(e;GM_V)#QM?V48G=cD@V@d>T5kj_^ z>lSCjzKL(ZU97xNWjQz|LN+uUM>>pHU)Bxr$Vjcz+r)e5ZW7Kw^A`J?OBcJ9psl6M zIux{I5`0EoPqw!KCkJs<#GyBrhf21KsJFi94I>=emwUOPk5A^H1 zwi{L;7BkL%umZ|cY6`tJp*nh=krs@TkMX&smdkB_dLTdUnmp6=k`CD7l7KG^@GNY4 za9n+@0f*tdu?^HWCP}ey){uo(rU01ZX-G6`Mpg9}5U9)a0Zh?rw2J&{G{-ltj?$dp zy?_!{Ei|e>s!#308Tdg>Hwiks(-62b{?Zjh8i@6g6Xxbe*M`Z5>6r#@fky$wYm z*|;fGlY60eVMXW1%teJqv$M$CuY0XMLN}PBK2lKx1lDwM0=G;11Gg#>`$?(KKadCOQICvZt zWUk@ox4DKXGi`F2z}8*JQc1V=?J3WL)&&w*R5Mtq8A3g&3%sh5@x`p>g-gDT_G*E% zN5ybC+k~*Tsswe!`^@|^skfYwBDx#|XV7(4DD!O#h#L{CO8m2m!NmeTJFQQaYi?dO zcp}pTU-S|cY*}zSOBLeLG6!~cP%bDMRLc=6XCCEt`^~@fG+8Y69$rjI4jXyiJa?_Z zDeQ}p!d#>O2w`nf+dq`{VqoHhk1!=g6=OdkST5lE2>KbXFR***l-uXDLcF!CB zNEyzNZj^3eqJwm3yQJdT20pEh%)Oygu9P)Yfo8)BG1@iel54>WBiiJ)N$t*6qE3z~ z^x0sJ!@$GR(zzjIlWlg}Q{ho?XipJkZe>Byw_P-&xA_Nw*v66`>2bm@2vR;CAy*Hk z^pyH`knH+9OAW2*`_=lqC2qk@S=|c)Nkv#+d2U{`vmnnr@YX7>@nVIM0}ZBJ_?wHv z^#mm3xeJ`quW_V$aFmtW$D9=XP8&t!+!dJTY0#aEJ0oQ#`wI#P3kX||zAQ9-a(#9{ zGfLboZgPs5eI7x9_o*2Mg-Z78ZF~?Ng>Fw!@}n@lTJo?s}lUZOh*UFAAdfrTQTxcyuE;fuGJ}1 zzZjhk?V>a(s(?UkANgbx&`}S)tt3@{?%WwNdUko{f-d$Z!*dzkyPdUPZ|Su!6jl>r zdaJ>vC_kt)h&icSk2E!^zIK8Bnt9&JXYSQsP=Z1a^=0j;G7mgYIkVVs$>?0_1;z0% z!rcca_>`h_%0LUf#+G84>=frj$`cDRYtQtG&u8kEt}t2Kb?Mg<&K%5*AoF$;;d7sJ z2PW3#64C=c8?a9{Y1hGZY#s3?B(x!nxK^Imibj>_*lkEBZ3{<@!45$=#UW@%NwJmsE@Y|>gw))Y8G!` zu?Bw3QL~^N7r*72Y~M93E+2{=GDvS^Y%cTq+3>Hk);Q_c+RE|whBY-d%|RxmFHe#2 zDz(1-OD;8iiwBd#C~L`1t-_9p27mtX{JM&`T|fwjBz+SfbgSN$(vP&G^dsuN5<&>~ zKRiw`3wEuBphfC5_#opr`&CZNY@y{O&g~H7j7EU`+>5dpl(9`oDn%!~H{8N{UZVUl)06HI-D~;U=>ebW zeNPyeo*IQj1vRg*IQu~D$9pi5N}on_20w0&($1A#wU=kgt|cV-bZy86 z7Dtlne0a?R*l1|(ZJs@)Q_(sgXKow|6Yq0l#*x#OpFScfWgcmRHZ06Cki6^Z%8jPh zQFU1cCN*pmrS19U0a)7w4bS}inC>*ntD+coUT5Z^R+*cr<~B?T>^)v$y;V+75>dzf z$B4-0l|T+wl5{K3J$p2#hMY9I=sEhvYS1J&%VU3|5A-Ppz64$on}=txwz?#~PJO!w(~c`{7;cv4Si!N|)i*Fh~qHRoXO zUq&eu)V_Ct|3@vrA~M_#5019=2H-r5JsD0!z_SMna$W-%l7(&44Pr40VpxGq7fYap zlOcBLm4zWTgXWyiC-vsDZOcx+=m}PNVQ3vX+io5j!>LrmhNKDMIrYtJ+2VJL-QuOU z_GTotj-fc+_W{uE0xTLmse3+b%E3@)w`<*Y#X@Ajj2)$7{&}6lb!QqUmn5xd9#tKM zK$dGYjpEo}^NeDnEC>B@!9B49rbS8KVG@s~5^hCPsTHNXew(!*#1{^b7tuUOr@}XY zDzoAhEw-i;;=qq3&MPSgz~1gBQqabcdA0tvt=nCT?)vwF2Qh1Z9|&$p?*U$N5v@V7 z506rA`_1mo)MZy4v~Jz@|6Cp>?~b`GuNL`Ho%AD+RMGoSW~Y{!zf-TU4E}G*^X%kn z{-+fWq0>{Cgub+%<&~y*ofbbe;dqn)J~kpdf^2vZsB*_DAO|=hJ@6lBS7?yx1o;J&39wr zL6YBS+Jk>SYaO0VxrD2eyf7LZ{Df4=>`QZt%ZF%lBiD#kFHFr>%y@nCIRci>+QI-q zkasddx`J~Ooi}=m#&k0t*qp1JEb80^8au17x+J%pCX6i193ra@dP|hCkE%{)H1Q>8$nD z3m242qg#Ut8>MrL2hd)3Puy}|X27PM0;I5J)^h>fYU89WN)rEFN96LzgU!bfO15u zn(Pgkub-jt8@i%fuM)$9M|6W+>uvGJQmHMWM0RCCens$$+(yWIQ*e`P9P`Sqrh>a> z&`M=FA^$$bKo@54D7KRMo8N=tTbH?t@g_yX(ubGIFkaW}rOvl=^FE(dm&Mt!ke*g; z!FS88_%Vwr#1VToD$v2B{s*pPEmCIEAvtj{n1 zGD6q38vlF3wx#*P?Z~#bh#9DfSy7_EW`xi>EXCiBuyH95g_HS+_b#u|H*(yK?*>>Z znyFjm-^z0KbI+?NiQ8)VKk-OINnqbs#RT}mfB0hBqHC5jb7C`w!t9vX9+6pSgVCOO zbk9n=T0<_@;v74F6kKIr-goo(0S8`x#FC?-cK`Et385Q=R>ug-9Q`u4Q0n4h8Gb5E zSRz0$>tZGfL+rK1y_1!1V$yjv_IqF=I1cOlX$1JYEhdf z(Oo$6M98yir8Y{_Ai|?I$4eWp_UqHfVdU!SU^ln=Rw zO@wZ@O#)_9Dk&2KWlC9kl$8QLm{=O{>5Xk{!z%Ype(ru!bcm&e={dH?CykYuf=nAs z+r{LPkvLh^s!8;0vF1cerJrzlk}r_#NYH<{!DX-|%+mR@9WBQk9g^#AZei1y;?0&} zP}@*d#`*kFGC9x4LOQw!G)Cq39*&=IDTQATVw!KywzPnS-QiI}eo9`8|Erca@LQHO zi}~2NZ9>b&=4rxf7oxr>lz4#1X>&*WTs4JRTmo2ZwiSt?3*G*m<+AxvCEX&YsTA^D zj+0LLq`+F`-+Y*#UTR3?C_Pip^~hIBcj$kJ#b+lv1Z6ZHw|L&O-89*eO=IOi3ZKCf zR0lQc{NUY@7xd<`50`o7o~h84F`4#sIl_Fe-bw!FF_{W=ic8X`Zr5e0a<>_k(Y{(0q! zy8GU>_xiMywmB-H97>7r6qVvTrD$3@WK=znq9(9U5U}C1PY>fp0(>x%%>m7=@zjMi z{9(dZc7H}?-v_fEpTSE_tk>8W?HVz+N@*>%1{0nx9qxh-zw(fVR$MX+;H~$U?-*?b zn^BNS*@=+&{n18gl*-#`7*yS=YC+6}>W`f{s}n=GiBUu-HRZDIwAyOlW#+>3EQstk z?QJy#l~WD#>Da^xN4WuE^pQv(8p~Sp=r3GdEke3b2t*~9IO{-3p?Be}^_ojd2KI^NDF6Dp zmHHLD+atamyZflMFLBds?s27i{Y7G2@LW$y>2JPQ9f>MqEGYmSlSDst)GH$8j@3JF5<|)-R zo!omzf@Q%65r|Ant77_)8bg^Rl{7%HH+)a5cK1vQ|1sl^RYlIh)N)^xLE3clr<4kl zAC2zq|9n$v8}A*)dDBLGS6|}sh!GwnFPP5_wyN$s;^cX)&cX%W{smuNO!nVRt+{7e z((+a)Of2S>c^Kt~eR}0&D!UrghH^}=ue#iYA7>W;7&9H=3lNSY$72F5FMGRs>zJHK_wl)mR{idOHt=hcyu6y^? z1AEJ0^HUOecf6anIZFNTTSIQ?h|kv=7F=r903`*4)GE+YvBm{S;Y9I7xRid$g!?eW z|7lWvS@S8uA>(_ahPOCR^%2hpgxJw@{(1jZ&|ym>aZ%1jrl-iL@UMXxp9cpUcEf#q zeP!k&?8iZ0y}Ob#b?^2q55&dYrjbaR$s0M=8J4ZpgFFXVo=uycSt_vSH!G?MGtE-} z0w3PuZD{M=kX0p2NO@CB&xF1xd9ciG=D?2;FlWe({E9ZGd;@YbYxwSIZl^Z=>aVRy zJ~NLPrW3<2N8T;a%2)Al4{qld3Aca0pU<{(9QA(pWTm?&K6x;=c}}>*tg+Ls==(D> zcz}bM6cx$j7aMH3b_s|gis--cDrGsxU1TZ1`!LmA%(txnpa*+kbIT>3VL})#M~@;<%{;jAkdzk z_@k86{;Q`geI`Swp^_mw0@G4lwgbh{%$Sn9R(cL=@Zl4sGy<-R90Pvm5h>K;%L z$Ly~25FTMYq{*#knf}W9K{(6Qw2wSRPkgZ1c7AR|-`kt{y35VCaeA05+Rb;}O~o7C zo0E%#gFp#DU8Gb`!w4>bwl$)@i+Z1vYx1j8Ju5R z&-qg;PpJ&P*GP|xQD7@B)6)1|1i$$%TD|Yyyk60KBs^yFMqTHT@JyfH7A0Jr zX>WG}PVcV!xe_b+ywGd4o9Q@M%oQ|$GF z1GlPl2gh-3NQ;CP9us{_UgC3?rjE}xi(7e~=P{yt&=G%?%fh}FmqCf)W5x!@^KTlh zB2f+_zTUba{=+ZJ9prD>lgEI7CTZeK895``NiSro{7eH*R-uF=nOe7Y147e1nw14q zPGNi!>pgfq1JDMEBI*_D?zu@b?ISF$!Nmwf-Myidl$M5b6-0jTxEWX@DzbiHJkHct zTGZtCwITw+bI}>t?A5a{e;4U8qCF^f(Vz-HSNqd{=`APVcvuUvV}80&(`Aq%9$6Xw zvgYs(3REt5k<50zWmO&5{gO=&8)-ghEy-RGN|g1s*3S6j!UmU~?ekWcd$C|u%rp?p zZOiTE_7fi!_q3cIt{ifqt|)s@Syf=;7pVXf%nzfkRO1Lm+@Gy8c>SgD`Ik2de)fgt zv5QuMDMRr_?PM5KFyP^dPpSdZKan2zg4B$YI{~5Yshxn(_WqDDc>-nE`L?{%3>8$%xz9ErC&w8l#LA3jt3vKmhw;{46I_ zCycS~vy!Uy|Gr?t>ai=;;Qeid)cuDbE07} zPp)Z_z?j8ism8mj372eEPiAIP?qq$oaTMm5l*(EOk%~aIWO2uN7gnMZvNvz;-E-ea z&HO?LCg?%;PIbB{{xlG(-gqimJ>vz`VsH-c{Px{WO~c;*rI$~43;4>nQ08zXX>YqK zZ}~KYK6)AS(yOPz3|$k<+?o5A$3PSR5GXADzq|Y>rxv@`p&&v7Re0^}&i{8e{XdTS zZ-@Vx%>R|ne<_`58Iptfea4)#Cr$RD`G$&ZB>(M8ms_tEJMJ>b9VeK9*JiY}%6is+ z&S{AIb}TK7`Rny$-&*C)TJYxgZichZ-yc`|zJH(A2bgn-O@N`Z0dOwzV zD1Md0sF4H3vpYBBWbN(0_Kd`P0K?ymyZQ}))={1jiee&3@xXx()gfV?^^<&d%Bmdbr-Kf=BIn}-{kj$}wtpFfHNRbCS|xumMr~coyMK~#Vlxq3e5O-1}@pJTG zzi)PK#62mjGF^j_>7|342JH;6RIoBy=!quP=xtNUZtHzzqeh!;k?PDPlQquHa(DV2 zy~OAlu1S>#z6BtOIpq|uJtYaW@e_$|YBzqJxTja0MgBz|5rSU6L8;gUNc@~1^26tU zR_-5INj}}Rm2}=pvS{#8Me7m0hS)hRe!V|rZo6SCd_z*;N3HO`Vzx5yf$}w>EZtd* zsgKfMVPl}ElQMS`HMWk&V?ro~@pyb-8glCE*q76Iv>FJsL>~hEa@;M1KREKjpS(KT zvnihJ9ajc6v5AH1zwD*wvZtAnM~Yh{m|)mD8W4~8t=s{bKdF;0mC0|l-g5fm z)T!L6n2_~VKjox&#Zrsa7xXDa0Oi`v59?n?jE&nMQ^o=+KPDS?HtIyRt;f=q@ShK6 z752wZoKM>7T`-!tGIM`9NPO{>v**Y>s?LOpiCbQKW~Xya9^@6Xat)N1M-YFYY$I>A z8Xn78Ql~>)5I+EtEpSR~j!=B)pr2MvkN|Ua)yXC;mZmuOrt@_fu_txZPgsqr6sP}i zbrQE7kw*&uInBV9DhG;E4T=GoD<;SuKMsq5Eo~8gtn7Mr&)nWnN1y^?u#T}t^Z#+o zx5FL_A6S9SBjvcj(0@INL@hoA3dQfzw|{!e_FYputdflJbFB*rWrd1puOw3CLo_qW zi%5sEXqpy88jue#J{XdI4CLVbj)OXX-0h}CU;~LFen-Mu30~0@ODvK&2AI$9Kzu05 z3)U(@SLdbc9f2eR>UCu2R&ToQ-uop6bWAR?X|ZxUYQ^mEkg%Rvx&RpEivO=xN$bXr zDLu9b!@e$OeK*1OL=UjaC*4D7W($Bht#^NLm;uxYFwzh&82AuP0Le`D0tAJc?0VDO z#Rg2oRMp+k@dr0kDIQ3jFnR?v#wt{T-38h*9nS{6Xd!OrqwLdF7NcGAFYE%ecKKsM z2ThNfo0=R6K~MzD<;*8ci->b=VEy{9sF#4U0JB~KhgE=Is-90W#%zt0wrw>zYAwdi zZ7yjX(|75Cb=e)=({n=F@(-8fz-V#9rjx z1oX9vy#mfr_>JVCJN1STD!CdyD$xUh(@v|JU>`X zYQym_#}XR&w;!q>mhW<;`P_ah3cVL`eVrw|F?EY!Xq*rkf)6`zU%!-qrsdS3cf&qaF zwA>Wq4N55P8u%5STxrjtF08auxFg4qU=9LsGOyeNy8z(h8;;!mL%j?Q8k^hh-TAZG ziJwkiJ=}8$f_WGmajbQ;vETbk@?U?oSrnQ5(1@^)JG!gCP~`!aKhU_W42oCx>vxJT z|09-%CM*hg6L(^gC>vcQb$kxpWmoyt&Mq&l#fEo51iSf-u8$+{G)ec~jdVyQwSmmD zrOfl%{R+Iph{C_s&L{EX+n6z27(u#q*N35aVaZuVW*JN(jGVUwfy_PVp~|27EQ8rl zVA+@IYvPnY;b?&|zhZzVS)aknn!kb>)Z|BP!cl~UiGs za5g6e8+yW#hqF4zjybnduz<26PCoGju%fAn1bF3>fa)T^59#r{K+BbXyatY{<(ID4 z+{#9~zlKVDL~H;Fk^%1m*k|ZfkS3NxXswCJ`%>sn?(W6)tI%tBUhf|F>`%@~>_l_M zlSICR#mn`lOiQ;nw__kK2TJfwU$dNQ1e4gBt`5MzVs6;FVpYS~Lr-SIwD2AP1kge@;&^=MLQcO>@hB2I)A zi8J|pri@NX1{2i^C0g=`cWZ0?TjGwHt{O6}^-k25ulW}DUQgqSx3JsmGs0V?hZ@L8 zhA(;kkkAlB>ZCNGIBA_attVxI5tnrsNmiIfk}Py2@LigQ?ip*_w$$ANw+C1ze*q*r z(?pWBc*77YA~M!|XcWv-qzQUDwx#dK^Pul)AmFG^VQpB5;w}+wl>t0N505DpHTMij z2rx~}5BMJY!1*|UocJxtt)+GVL}48d5l4z5_bZZ8z{Et_$p^3&5!6=89P@Y#vSMhPu+uRy;OQc>AiQVEu~ui7l7D#H#y^t5q?!_?uVT0bg+Y@=IOU z16DJyFR#z!w5gHALY?gsVi(Uz?(Zit3v*jLYr1&x?P7b&i7nX%n{+2~9VrkUB5j7Y zyl4NlMRloA+y>ZHR0w5%h%mxuTzOy>(tM*r_fA@O$KLE6oYViO7 diff --git a/doc/images/screenshots/overview.png b/doc/images/screenshots/overview.png index a1108cb2b235adb7572dc6aeee4b446f56568598..d9d344719729fd1427c57666ff3f590a08e8f04f 100644 GIT binary patch literal 69773 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∈n^-R1?j_|?+lT3`mdOT?d#yXUTR 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 Y2QO%}8|bS3o$^vs(t1&$VDs*O0I`2~jsO4v diff --git a/doc/images/screenshots/software-page.png b/doc/images/screenshots/software-page.png new file mode 100644 index 0000000000000000000000000000000000000000..8dc972812b1c8f21c182f983dc296457f1f0049f GIT binary patch literal 94402 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 z%0D1Wc_tdJVLf~%|@V7-#4klFD%H)ncEwQtg#1!6!AKfs)8It^9H@Y=YAxWd9I|m|1>%Ljc%HrCshI zI|pYUG;q}uCU~>IS*Ddfi4U-yQ?1x=mU~lN|M&$SMQS#>LDa)jqT5>1(2@pfs21}Z zRTK=V1H!v^sjv5ex8MZ&LsLWaf4G`Jq}`NzyQaOcPe~*bHB67~j$CF(w_Q~fnLH~! zqmF<2B$GPPp|>s@$`;g=(pSB=cX=e_4WbL!&wA%UT$m=lcaN1dSGRnoMuc?JEz!^( zK8$dwg%C?1EG#UZe14)}dTGtwI>}qGfdlK?B+}X>Q|LoK4&kaeeXG9W1w@l^fQF zkZK^x0zS3m(S36|*npHM)P;vaY0%O&DsrW~@pY$<1!m0(afgTXJ6SDqMdPz-w^Kr9J{Wi%G7BiXS6FsV}>&xVCcF_)l*1 z)2K}0mgaZ763L6W0=348U&Nta>VjLb${hsmg~wwo$+{)V#5Im0x#!gvGD6oaVzt!F zaZx9$9#lodj4_#{MJ>rC_Nw1DV>P`Ai__z@!y?-h7zy?^v%%u1Weo0wjW*z=BA?gl zqc>ZWzmg%BPXc^h#c&W>Za(x&AsJgrOsyRt_=Vi6VwqZh1_kA7cU|bwI-5a@D1R&(#O`@~m;HRY_)VQPQzvx1|eZkGOEj`3yLEKWX{0dwJy{OO@M zk%92hV$P&+^%JY~Y^M*X`z>zyIDS?_dW*QzOC$#PZ2+z-inxG;=fdix%b@#$28t22 zS)6BgHFEOUWD)Gzwe1s_Ms#@-;l0KSLow?T{Mma9c&TRxEg+<FRp?wP1&-E?~SX9hSg95 z3Chs%k2|@Q+KPs(E2d|109eFnQ=8Axn1{3vfWZq<;o)>JZ0f>-^V{Bh(ricyLo{WmG=+|cH4JhT5Ye~3L zgomw7db!nYB1MBCRJ^iw{5<5!1X%^*zw-(3Ns_ql7ja}q?CqwOI2}FhLj4(l#sr7J z>LT_YEAIK!F{SZNhvxu-I*;)vAOp^9#GLpLaH2wtC7u(8{MJWwxQIEArx3=xguPc8 zG^`ZV)$suzKYm>H`KWYr`dV=Qlr&BjuC--jv`%;0HDfHwRM~{9eOb5PbIYJ;Cx{j- z8NB%eAdrNE#c^`cOt|z0nSnVF(rhaTYQ&!hF8Ky>jclnZUcEeQXPtKNba=0OOtm*- zW&*fJ0*9Sf19OSeOoaOVU@|?{za~eeoc7*b9@h*yU41w>PoNm{CaX6ah+N$xX);bm zl(|BmA655)nrU#UQ7Ue7yIj;}$ow<)LQmonWGV(`@v*a&Bf+VXc7Lw=(g;_XyzLml#V#&ZM>ztH0@&LI7xm>Hhd;>6PUUz z*$~^BEfuEP1{PhY$sRJW+GC++^QC8GgRu>QLtoqm{K9aMYVHTz2RWZ!?=ai}+xbL0 zPk7PX%8svkgZrG^*Q2fOhcHk-biiEomJ3d-} z3*8F0!@hgBm6bR}2qW#4FifzZt(b%S@3eup1)Gg^x~#V{O`e7IvO;dVtOp|1OiHCY zyoHTOruUzQVg_Z;yT$pg>ip|>@^xV~TdM(;?&x&WaarV(1rzis)u-}M;=Ffw#h{-S z?d0ACrz^kKG?04>jFqZ)o)ELzY#R#9L4vJ@%GGHK)xjf(I|F)qr#ooq>g)*0V@z=d z=mCvA&L?4F-th|K(Np*vHJ-1NGOquu9Y63TAU}H$M7OLus4z)T%biS$S>=J65L9aKT{~ zwuM25UwP}wtOyCOC@=GC`1!OEG$1CaZNx@$`=s&LufR%6JB6YllZdV9m)DOPRb&Zn zK`v1W#(-#spOh`9<&LX9`jr%AYMF}Oy_P{v2ajbkuRPz-p^GIEa9Q}mkL>i{Tw^>w zAHDK9(}C2R%KXfCf^rY-V^L1{aUEX7t=Tl>CcIxS5BjG^GZj#DQa=I&=8`&^iL=xP zj}0q}X3Ou=1|?c`h8G5YXaE4hK@WW>(17fa!=KkpBeGxRUS9dqkhc2C*;ujgP|nh2 zhYt$}ywQA4kcPlTakR)MU-y?%2I1;QPfx@8y@DZbUpR8}s1O3Q^f|LeZ26XhbL$CZ z+OUD89x?_aY$P-Ql$MjZ7!ziO_p6}WhUu30A?_s=(3G96dl;@Zx&C?5(dckWK&@zM ztBo-K{Xy-q2To%--%TztV5A8*jm_pVlBn-MF6|}H`b5J=H+|%ItYOm=qN`lD@AQOz zNoC3v`O6jGhj+}N4Ga{z9Fc@nsXoe-@V+BR&Mb5vF3+3gh2Qm4u0u?^^`BmG7W&V0 zNH&kTEkJpgK&6B?)}7H1g8D=`dkB;O5Em9y1Qzi9I|;%q(Q}0wHMDZ3G7T90afB44 zjEHGrQiMk_4{;IL`hGj2LVg2rCkDB6$hl~@Sw%0FVH`nYKiWnfSKBBLgZ#577p=K$ z3k>1i_|D_L}qR_HM=2? zN3Iar8}6v@5U*HnC<|XJjQb9aw8%FKH3U6Wo|M-(U_!3vT-Qx*sn>OcBjV}_T*#Oc z_{w+^7_@s(4cNcD$J%P-f2Zr5R<{QL%?SY%t*`B)<~T$GrF57Y9oE{nc#vJ);;r~L z3lIbp$QLG7YwRvG$yvfcpf|%4=e(8zzwzm^q{N;m>yP(iJjiD>3C2<;CQXIO82TXyQU4 zjqc>+WN5YFUyZ?GF6@-?-5C-J@<8e)$Jc2bI_JLt`2T*sUuGs6uGWU{96X89ae+x0f#fW9T#1v_#~)kLNg6$W0dXLDH! zs;^v9C~J`KqQVf1(>5f$?>5b@h-3(&6nEP1X06ET%nRZ8VUHYcmuq6E1uCUaGg( zx>!nod+BN^k0Mw*LY#UhbvY$$=a*=K@`n`qn+$G6+3la#e+v=Wz0 zdjVxzrOh|M8TmS|z}qA`U3jM z6m@Qy7H%HSQ+_zn!A~*+`Y-yC^6cQL;0Xt7r$44bP_sx*ET5O|E0?y-->_Z5cSq@) zU#QJ%fz`2CO3^A}o3WSn5o_^Hb!~(Q#Cv;KjmeGSHLA3T*=8`Qg&=TdSW~ynsSO2$ zET#rodxI~28;F;%@_mbOR&@Cm8A&X-bx$7OBS*ln>u0z8eUaNcKx@_5hlF5%y93YTh< ze5eTH%5Kd_k^(saVh_!6Bz~D5ye1dlalZDR(`g;G;pIi~;c*jQQ6sT3eI zDGdzpWUk#j!BdswjR^H5GvHF`yfC*#t7QC+a}`SU!*nWPS$S)+=NQqYqK&Y<@m@XU znvsVT?HI%h1xQ{iw_-)HYPDrnJ$7y{Ewi%m2GFa#FM`-%h9pLvAw{{!W)aF;u{v}t zjMCfcEzP#`0;nl1`j)QZWc?h;;Hm04f3lNAS(T1BO6Nl2$Qj^|=h~rBLBG$)5zC@O{+c`Qk z9l(kzu``m-IrZY+{bDS(tO!Zjq{dH8r3FkL%G+<;pB%Z&*nj9b#jWxcWg;W*lY1Zj zT#bzE8*3WW1hWqoyo3*ILv!{P0<}q1RnL?+>p|rMGNEP{IqCn z>5an$G;!jKoVUne!diQ1Fs=|x`rW;|gx;B&WJOvgVG5tRv~JPw?GG2w7nc&k>Anq> zH7RjB>+vjUi8S%5H>f4!64KtiH5%MK**nperTfb@{&s9@`yN42{5ghJR*wj6E6=6) zD*Fb;+4RrUyT%j`ZevgY#s&aEURkCNAC3y`oJmuM01q+~w?;TSZyUGW6aI}m!pIlfZcPxd`D~`?c#;nO@0e~*7z!kh>H7ueb-P=r?g8ugaEQ2_iZa9WhglU7654=b~ zS4d0+nCoTI8SIvjbmpyqIu@&Vb#5W6P3k)F?GkN@~eD{WfdAB(CUNQBKWu1 z_4nxxtQEXle9xr|ICKfsQfL3!hwZ)&Wz;|R>?10?m#S#>-ADTv9w4>DM9}zqz6-;{OIC$NM5!*Q#dNSb zyZB&~9UZVAk-`1vwKAVCP3Ja^tkb)EFS0&;?1_|}o~q!NVar`(Rwc!=*`hyu@C?e` z)A7h>a(8tGt=ep0P}REIZuJ0<&?>FW*F*JXl)2^^^S6%_=9{{6Ty&Sc)H?7lZhTHb zjeDr{ChhKU&0Z@Ts9=PIrsB}bkro&QB+f;Zy^g5@akLO6_p9%o$8%WB=_ec@&#e$4 zegp&7Kxtgyao??mjwK=LbR+0TIA45`5l<}*XpWblj6u)bggTQ@!9<@&3gSYcYbK=* zs@8q!E@nf(n3EphTrq}ChXsrS7D_18K5G!t-Im=n*5EF`$e{!NTsm7c&RZ{g(t4j^ z|0G@Mb3N?5qBvkZ`!zs*^W@^6kra28J}``MIJ$l~@a*DpTw|q@f}Y{NM3=_5q6}43 z%b4zGzzN$_w^RCw+4LzVS$YIT;hQ}ltQ&3WNp3gX`P@%{?`Mlsj9h(3URK@<<88l1 z>R7tU#WBm+f%^-5slliGgtF<#ry?j_M0S=7Hy4lM=?h;-FAob3U< z%n&wdaN7s_hX5tPSKv3~&N?NoPW{e zN$$;m4r9eM?iD50td%S|8`kb@F@N(y+zAP?mU*75 zzn83GGa*EL!@ySF#dReH3NmvaE5TkS3H>J-?ZZUP;h6D8-kbt2c#0n(c~@L)b_cqI z1yJ8DC?g4a*8)F3T3zqWVm`d&eEwHw`&nhKtr^XWjFpzDy|@A^a_|@wbXIG%n$C8w zAuF)0qtQ5-6IN6C92_DUd$}HkUE%HQ*u%;XwY+ri*6Aps|nl&v-^eo{ZdaTIwx{G&Fh4%W&Eriq!s&1PX zON$QL*jfSi{QSmKSir51f5o+J0{)Xxcnu4tv@0S}V*lPQZ0#v*uV#V97`UP1iL38O z#AFp)_?|>a)goB!ZaC=xhD^(zYa9La&uJ-XIJClV%b)G`!%H++XXCxAiM5$r1xcI; z$@=E~*nB`sXUT=ma1HQOQV`^p^+aL)3%5fy`}RdD`N^(FP0Dv<$HEGpC@{7ZU$gzH z&N;MBmnJA}FNh^sAQtmSLEE=nY8KNL@-jP;eEdvRlhVYy$`A3|JGcNkoH=rxzVc*6 zxwNe*G@O9HnF%grWb%!YRodh6=>!EOuNdrI|8n+otEtt0i=wLF1bmGS+4w`q|1W>3 z$N$3|9~eHrSpWFvV(Br?{esO)YIfe>m_0Mr7M@F_p>2M;U2+zWd3ypm6layHCopfb?W%u z)#_;dBQgwNtUL|(+DpGez;CjTeP@N_*q^YPG%A_gVYeOrOm3a{UE+)3(8$P?N24)v z#FD|1d&d{(wNdCjSR%GNyfVt~NrnZz{5$H&E{(PO?Od)Z#W54EY|cX<2TGl?2qry< zp}Ww$>#1yu7$in)r)6_>4ZaP%o>$XJdcKxTF#yi5F09lxK|sRxEm6oBiTEditU!JM zoR@KBeOTfzn+fByC`{p8M!8saMs8d^JDYB=uzn|Hc)ywLgIP6(Z}0^;Icj8q$Q=`_BaQnqf`F>mXJ-t}Qjoi&YIta9!fMLDJ4){$Er9w%76oh6REooNpz)=$m9vC-L7`m~ zu&7@Q4)QL9G{AoR)AxZqY2YB&ra5KVJw7cj0|x?}3F)F~SF4j`LH^o}*DG}84` z+KRLWL8kV_LF$3yfxkf+MFcAwD`O3Aqv=F_8w!#*_Uhsu-@3i8-$qxAH4So6!jCL$ zjKjjJJc|y-%IHVdkr5iYYsW{fV=>C&-}gJDaO$H`qn~s5-4fKocpP%?DkYbk06$U) z_uFla#N1yN(mn_-|N0;86a}wdLRr~~2V(j}zG67NqrwOc)Z5(CpxSatHI`9RZ%re( zx%04M7Zd2jC(-s#klZP0`VibG3ifNc3}+yI)MI717(Dn!)S6REbVs%}CXf|I+#ABc3likCJm+tBC+vD|?EcY!PN#nWjhAK=0xNzoacZHff$bHJ@q2L*l zx;DDB^(5bT>pBkKrUDJ9k8+;Ss6J8`I;#O0a~CI(Tc?1|&qtKCnI{XxTTxa^tY784 z`eQQoKJvfsLsBwIm{ZzGke=HyUWURpEq{yoVsV9g5UZUme-5L)Cz&AHz;yWA^lQ?4V zG&{-*BQp>EX5!=@U#MIn4%9~ftMX=$({fuLFBXEs(l3ORih|<*0p0x%uUn%m?!)^h z>PbF*sB@p%0eN#&0x1GhtJf(j+Z|Z1ts`Qu*x#DlAN=sYrUTs-Tn7W-)%#0w^d<(i zLC*_KR4iTMNgK!bp7-YwQ#27U>evH@n_N?ZMd`QAUpYE{W?$;N1h&7Y!1z0iweEey z73N+DNQ1fo0B}1)W9l=Y5PXR#9>%9Rc7_x79O~FSG?HmD5oMc{`Fmr-0tHFI5J}p9 z<9>UG$3~z{2J~u9{cC9|2gf)iuq#w5?`{+C&XE`?)r7~PWd~l-=}nNoAMy) z#Y?Y1S#8c0`GhsvsK1$vE?N@aa8%Fu+6XCGoh5d8ssY6S*+-k8lVzNk=dVTSpYzy$ z8$3McWSsxAHdU0!M{&UzhIm3cNtB>=GN|#YTAnv1$=A;W=xdoR&Br!3lRG!No{l?l z`&~pq=rgr6$+23GYSNiU)}@1@t1E`Ulk!p!9a)CdP~~1RlA}(CXL)Zw9Y4!0wvJ#A#=Y4G;Q}5 z(wtgu>?Z$!1|pg-II7})IQt-um`zWoJ9^DnIS}%)C2|^2{&crkKWe^F30s;#5?VSU z&R_SWJ9*!*Kc||GtHuIlA9c&Qh`b`#W$R3Y0ke59U#-2B zBeBSZwTLiq2Qj(La;~lWXH<{p!uK`{+V(^9Kacn3N?xSU(06=rg=UOQF*#*`ceN4IY`P1|9T6sKY_@m)x#!XTUX=grE=sGpae`E#Y{)MWloh4^*#H zawq`OLCutG@F}U4d#(Czh$Kn_8(O7UNK3RRl)z0zF^J3a<(L)=LFUO7mO{zAsZ#aC zj7si7uaqvvVaM=WWO~6sXadV)42QoLyVlnNsQZL5LfDdz4NKw@+ih;5BPGtp&;Z8x zfy!hPF)ei-=i(*XBwvkZXzU6{?D38dfU~Mh7IUwV<7zLpSwx~>6wgvyZ=qbORx7JP_!};s!Uh3| z=2iWb-sk6dOcHniUs;cFKLJV+Sip}nPFg@Z9W^Ow&%5jg;d^`rOoOoCM!R57uUkWj zl)>no5v{RbfbbT@e6IJsnG%^Lq;LQ!wt<3m`a?u_ftLi9w^i@QTH=;E+)+s!oyaED zyiU`mcgOf!r4S6w3eUfiWJdY$8)oo9=|0Wa2qX4GIMDGwYrog0*THjA5~^YA;3 zhm%KB`GrH^0ls0jBDRAN3a?O9X}|C zf4afNRN&hxU()m<0?Zd^Y{bOhz%hs-iI>&)VqwDYFdr^W_FMOa@_H*D!AZam7anTvutRo-3r9&MSl@>G zGz;s`f&<@CwC?#M`^gK)@eJ+s&|tK10RopFT`?)XQqfV7lZVUo1&EM|{q?n^{hDhF zZ#Fdl=)@#9Q&QQ^U<0q!s=2RSP&yKhR=c>;U@^#cv$|c_ogWgqpz^K3GGp`Bc?8Wy z2pJXWJ-~knEiC1;50!E6HGgLd@`q`PpDqk!+XDMLt#~kOQd2VA{qqj189{^CXX)l% zz2~m6t@w{Oe=D|6bd209V|J3aA3IbeY7+uSojA}f_d+lk+)TvGiOR|%BCuQO)$mP1 zU4Ox}I$7T~CjLL6UKCqnF`B_z#`SYwmN!IGS-Qctbj^?V1qA`1Cu*9s=P*Jtte z-kIkC>!y|QuzPAvb>&dOo!#9zAr}fj-^iE^fFDoo;&v2)Ec3eve{4;98Q7Jr>4OSx zUYJ`S=2uo=`&fTY&=TIMx*M0f%|$))fu8gCFN-D+e4epYZN-#$*?GgTdhx_vNo8gI z8K7DDK?Xw1!=QVr!Y4k<3KStrf6F5+eo882t0d$ZR6n6}?)BKABl&>NMq!$Ojl#s-)=Q8n8>-`=#E zasASB30LXL!;OvI+($!w(1yGnAH3KMMO&mb9JKnKYxbPu#=js1;9bPPB^}Ejw$?6G zRpS3mVV@iR-^HX(Lz$}k$MKll?))d-h1?l6K?7%P zhWUNCu5y9)gejAzYqO%4vVh=_3&k z9qkU?o!+!33hI&Dhgvs8nIAkR@NCZ2IBwu9PeKp|jEE}1_^~bEEF#$Z_x?hIK)<j4nc?=_3I;#Lo1>6I$}lfjJ==~j)6*vFUF?(vZ!w{e4=fCwDe2fIet z0hkBjc~HUZle8vqpdAP|^4k_RaswBj;1>SI?tYbMfW{+FRhilyFP(Yn^&a3`!DKW- zv0K;`Ou7 ziHD!sWl!-4b#6(yPY%wet#a4AgzJ+OZeX0J9iu-pp4?@V6MF9t8`?- zw7DbMr60k=T-h#pzZsvyTqyI7g3aPs$?tK0s|$^` zsB}m%e1eL@WrrLR1{Qss&X8+zO;?yll^CRsH_uN?E1@MYp; zuAqb}4t#WvB`2fqD(}Fu%UxV_sP)6du-#ZEmQi=XpznNgc%}clvx8XyIlLE>W3x?X zr>`R301b!!?sK-D{MU3_UxPnx&%MkTF_G>?e$N?`HU!%oeLv)t{L5T2=VfAEhg)KB z$93N=HOUH&wGxPa(MKmXPKrsWq84KbeI=ud>js39LD@b`y>5*!PuL-2^fT$*$XP&q zT_#2PM3j)!pspxR&7Q~9bttGxHH3VW$^HRm21(9PR;Bk*%9$@)|2Zl8Uy<-CYAkNL zw?H?PH=7oZ$;A}|i&m=*irV>M8r8`I%N22K+qjLpQJOz)0^Z?UpYf>i&A**f)8!O$ zWPPcv&CMP5=1EF@ri#=1hl~Khg3TI%CZo`ojop9I*BKv>*LJv*N8DMFi4nh)V{HdNdn;BB4mju|6!xG^zcv%` zu9&OTIlPsOG?rh2ud9|OIj}wzPxd%oI_70Ld;J5_{LK$^a;&0XxqXJ&#ejGZ*IoA; zB|YPoj#GdBIF6-#@IldeEm%mz{rINZ(Ya*wSGJoO^6bc!L{#@d1Kv69PrJ#_yQ)gqf36~AC*BxL z(P;l|RsJpZs(k;$9{*kg$hZFxfy=!dv*j7EjgKp?$P4kx+!QMSfCOM|(;+_ES`ZN0 z>7nebb^m)vfd7QFqk%CHNL^aXs=Hp%o4-Rx-p@u0<}pd}QZhJ5Lsa_Xn6;;uFB42t zZK)TmYLrX;T%zUEMf51L6G=VYiun^4iIDQZT*2ozXW{*+rZN=mouma&Z@l|{%2Ff& zM_bU>x$;;}NiofuqV5k#cw0y?7-ok-}!*_pl`Ta}P|+IvX_qD1R0 zw0XqaQ@QT=o2OKZv=h)Qt*tvK%6^yDbKfjs;n@Y!BCpz5u~`LT!wVQuoC5E@{TOjB zkD*1zZB4TukU6suLjijF^Y99%z1-BD2zP{V#S0*>^8SIe3n0?$&>#Qh89ZG5zzoO* zBm2_eb8(|U@-Bc&lB5NNhX`CbcfOZx?c*IkDS8zNFLloLa$4K#gQvU| z30c?DiWWSyq^bk<%T|po2c!czIa$n#HYaZSe27u3Mz%R*&J-~n8YHt%_t+O|Lg883rBx@sa1^V%i7FT z)rW@HcoF3c;4L>x-<$^(eEqcG@WjSBl$H)ypz$cQIJPdYL&0(^;u=X>ROT99d2&FHZ134c65sZOmukk_^_m_5jq+`u_PJP8`v0@SD2y((D2;^U_-P4L}k zx0Jj?YF4>(jcO#+)n&BJZ#^3tz%umy6;zX%VnSQRB(0_Ho6+H?v2TG6$UZa{w#*DX z8LX5lZhfk90`|!95VIri6kdMWLpV}=6!whQQFgTrPO{A!D=siMPgvuf@F z->;+fRuQyKKYzy4W6(vj?pyYvjjlCM7GJ|J+tuTK6=xzZLa!4)!_UoObCd9O%JIBT z3vVTE%7yj{Vcd)mn!P4wqb%v5CH%xowPPl^@pONZclsgjO@O*MIWk6f=yGyKsET#S z?W19S-7ihCRxwdugx2~op&GqgYXryd#wo{ z&H2`ZA173>5|`rZ{k%;Anv>Dv3g_eWI)BiNQ7W%a7Q548$e>q|N58U7v*!Kc{m-$* zAPql%0_k3c!dAS7j@xbKle7C0FS6^t(2_NF^XoJ#!gP;mVJnq)$C_5;j$WWAr$p7= zI_eqAk6JW(EBSAQ*rvo7C zMYomHTEPE_6&^e)S9nCbGBvi8bo(g{lR472Hh3+L%`APcyo_XzHQ$8 zFka7i>^9IH6dv4qdYgUpFB0Pb0OXI}&sVtU8Hka02(`NJL!=Pru+FL4H)qK&E-f$A z!3U@0R=Ky$H1ms-w)ieCwkzAUS1KxR(fhs`O>Rq;zt|FaB536uwcY>fGluT25YaTy@m>j+V zHmrCw2y>SQx?($l70OnUMAl@NAPy4;D>K2wn`ka%bcd*b4x6pbJ-spb-(A&77X3Or z!^9)a^{#Ye>B4EUi3FQPuNeS{?=>gpGj7dinq(eT zD0teeu-^z-d)9Pn2JAn{7%z_Qj}*{_EBe01+shn}d}9QFj~9c0(7Sk$HvwWZu_WHi zM~p|;mp687a(A<`NIv0|B6Ev1NuACj&P}6zlHLCySJHaxz5~+S*7@|{v1?65X4lP9 zasedPXvmK#+U!&H-Ve|rhv;jwDtSCFSCL)C#7mlU9t(Mdjg3t#-jjC{W6P-_Ly~%^ zdvj$oH`{|ztm&1GYpSuy7MJ1q0^<_P(0gWzi&%9H^uc+L$R}5H zjI5{aI4;kW49G}l+MhRCkN)#a1z)Ba^vZ1&J%bQz8$FLBybof+u3Wy-W#tc1wy8-* z-*=*4`Sar&I#-n6-|dTFoU6-yS)$>uYWSP<8$8Un+r&r2hhAL%4QdU(qrni%34!N$ z)IYGdrU?VCK%nZUCSv^(v_XLiolsNYqOcZb1<1*NhNA7qD(fxPrf8`T3Y38&Z)b6~Ml&G7E zYCIA3(}9d1K%spx7B$1Atb&I}>#SA9GKO*f9?AJ|r8F}7e`0JXwtrqLUDki8I{5%P z+HY}q*wk<^C9{4GiHi@cfvrsC&BQ%2Yf5kE%yOpW-^&C@qL~#({TW&VRi55G3o=RN z)jzcEUG_ijx}7+THQqKRKJunNm){+;=)uww5R_Y=^qC+}u640gF_35YuCV~J87h2x?mR*rTnhMoshSDg zOGK>#idYB$4AxNIWKntFcD|m~xqC-Ju+G8IR{Rz^^9rX5wq8UHHR_BdEC*r%iaf!w zPGf9xzziR9b7~1gujsjyX($4C3FrNKqti17HEEzqzgsGHcprU7ph8}6-dFKj@1K=i zcFQAqDz4IrG@8xA`A=QhJ6Y>)JdjlXqQv;u3HDsw?>5@1Y6r;Cq=-w#BdJov(DgFY zDvkez+|px6rC)lwC->WdjWu6IE&#CqX)4B4o8&G23BHgrKq>7R%pW5m;=z6uZfNsH z7dBr1z^T}kdj{?bdz`n)OF%HETjenL;{QuS4*zo#w`n(9#9+#gnyo~HV3F?6<(1}* z4u%xYO~1-${1XGw&+1*e%k7H_%@1XmxKpre9m;+t2ax#4%yr*kwbWUpih1G0j7DW_ z9;fV9?W6>H?Xu=D3gzA5BI)$5(~t=^JfH2K_Q=-}@^d^1^h8*B86SiskiXL!&_{Nx zA{d04U4NoU_TXiSR+K6X;wNVotmAoEnC1Bd5xeK{tyy=3XZl;* zutpHk`R(zA%9%}Q)OP)4ml^7Do{0{R-gNByYxMaU2k`U$ctV2}JUsGHb3nhLT+Mih z=R|gdORQgO9R`47P*u@jq1n3FL>I#UgBB^5Hr^rl-nZ2CwLEBpAfHoIBqq)E*bM<{ zir>ia%qK9n2L+%Z(0u*6kg`<8~t4DzDQXM>}eOj;Q1ouTv(iKL4C_>+&_R(*((46f_%N zs`?TXj+y)s0#Y^ay@3;dD8^F`u?mh2ZV@(i%SW&hVKZGmbNtW8%i30;!O(&=66%%X z%=W7z_jX~1&c@D+s2g)4t@i8HbK{-{4ov>(O0{Yt>EOCSFSyl3TxKt=IStOmupDY?XtsncH31k{In)atsdRoIIqq`LkBN^bnH9fbuD4Rh2^Mph%!hT) zwvjE? zjgph|VYe(_nRUqYDYJuimKAHY3! zDeT{gb>1dEysV+%-5-^7^%BPzn(x@a0r}(e4i7^(~=6;4~=_N5R0XP+x#EFksB@X6c)LW z0Vjy_X}s10bz((B4+f^^c#2?Lb0JS4=v*byfC&po88S_{4FhQ5YU+A4L`87q+@m$W z-y_#{g#e!!<*>qIm-RmvZ#KL^B0qxg~MEPk)f5YokJOj+!8Qi4DA`3YD^rP-j{hTl8Ee&o67+)|htVx`h)>~bU z(L@qJ|H+ibF{!F07#FH&`K6U6Vr@pCg`pzo9Uv1S6k)n|WDY*kJVhyVth>}*_>~(j z_D$z7;DdgCuyoK*b9j6+I%xy%+W9<6tp!8mS%@a@Y`c0?vJ8ZCUF)7s52V+W8!iE< zr2g&j2+YGTxmo%d8}&lxp8*$0{+SICm0@lFfU$@krVzZBSm*JZhtjf;Zh*RojN)U} zdyYm=!_C&`mtWBp!|N+DcP_VY63c@gdIaAB=`_C1u^Yi3_QMOiypO1cd)t7H-ifn( z7423@V7u-n10Bul0qs=hJfdC<4LSofc(n#}5ss2j2^s`s-n@^-7U^*p;dS zc|deV+IkdWqoup8-mOXkL~IDz3$l0dw++7Id=%7zC|O&i+=x%ywr7Ub8ed{oJ5ROqxy$ zBVc5-6YQIb4kh@{Z&#xl+I+yKa{vRGZq@b8H4Ku>pFv4U)QO}jyEbtDaAL;%o@Vo% zz)?oPEAn<8l#uOySU|!Ekk~HoIA^L)G&LuYf!<$7I0;gON_Jq(lQBr5(O0%V&#`g9 zuHUH-5hOk1Nw5l_%VU!$U2QD2y2OX4PUZaM273J(X>@=2)dHew7JPC9L}-VIzJ|q` zJa1I~NAj%Wt5;L8b~OQ?G7ik684=vWGcPRY@O~_g0!S|`3_4Xtlmh&#BkL;J=79yL z?*V|6!n_>N!Ws>(RG+eqEvR|^6rqE|)pg%h3kFJHD?I#NEHEQf;c?sCuyHAMko5Pf zFR{xTw)TW3l-V9$9JE^7w633s8JL>qIv6wJfu+K6&tCyQ(73VG@b{pfs?BS<5=V88nH1Nq-43_zc&BA+P@*%*>8mwgYOf9O zh+zNBzJRLKyCIrtD=0ugQL^hKM%TO+d`o!CQT5vm-(NLuD4NK^YX!G_rKO#{ z3W?rq7)IMnB<%`E?q_W!p`~^Db}_ZM5o;os@q}q*8d_Rr8vGv|_x?2NI=R)osIUJy zVGJ1A$L|q;&38Rm_MEO5ID#VG&;9!D8nsyJkT&rKMWcT%7lt+eOnrZG|5{cQBqNnl90F-_kfHw>QxF-gTceYqgs!}jq&m0UHQemD3uDY z)I8GL`2 z>0e~NiS#FZA2K-7I_|$R`1JK3tk(Uz9BhYAFU`aNQDCPo1}0{3P|(@g+0#kg8{OlR z00x60y0%wMa>`qQ$9V79#8_H1ddU4x@p-#PAN-+px105_`*aypX_O@-B(${&A1)&x zm)2xXAK>HTL)^?dbshPiL!ane&#QwnGo|V?+NewOkZy2Ftr)#-awlU_U#+Rl`Bymc zFg~SSffemod#61*YzacmBAiKuaVBhC(l)C~EFr3}R8M_-Qsm=3ZX7$!R9?IFP*jIR zM%H=lRh#7!@^LOEcF!Gf>48{6ktgTZpNn}Sp9?S|(sc^2NN*>dB5%idS4|#KxzBAz zdoE)!iQi9gI`R_p`-$e*+1gGVILgY7y zmQlIhGUJMcyroDYddYSyZ}UB3vu|=szgx57ogDPO_O^3Q}m7UK}h?-tOGkUAOgLDVjQ8vLjUr&44f8cxi|?JSb-K) zNA2sLjgAuuFH-<^K}TeLv!bi0tN1x~Mi%cpu|RxpdibG)w|Ua2uSmpJ0WMnkXUKQAB~(c5jn)JM!n_ zdFSZeywg@OoI(6=?$%{ezSSVl5wpKuF`oTbWXV3x>Cfo?vs0BGbh!wesqYtHF8beAUDXqJ*E;O$m|Z9UC64WgYe3VyH7 z2~}ejD+=8laRQUH0_~(M#vL`Rzm3lp=cmA<%_Cvp@9hUWQy4_R;82P)`?^})BzdEiX3O}8 z0fk%C)a0;(1RS930mw#>BpWQ_^~WCi(HRRk=nXCAKsRjyA)ZF3poLctD>w5b-5wUfun@hD39-QCYu zOnbHnDXP@MtTvl0uP@dno-fxywcU-hUB}gsMGK(Mm@lT`ai`^DGi6&x$>6QTtZ8!w zei%{MjZ3cL-;&&}Tnwp6Wr+L+rL^1JZKD~F0zsk2GJ_mn1TK5Q=(R^-;Uy)Brtf}RUw*Y{<<^2V?60K;L(?4a( zu9Y?(3*=Rx{=rm>O*Jm(oO=e}rh*r0FGa%yxb-FvUfW7&waFUZ7cSfTVu}P%bRi!5 z_j+*4awXS3t3^#3wG$W*YfT0eOOmt-(VGqD`j6y?WXbQ@edmZ_-g`{|QwVDPE zC_`-LLdqWbu)Rl+y10n|se^1nfbIAvSNQTOc6L>gsQnk1t<7^Ps_H3X0x4tidZx6J z?yyb1#dpw>F$!Md-n4TxC@}NCH|_X1m!wA>Z_8VhLTA-tJ1ESGKnGVJHp#KmG;(!x zQ?37rD2JZ+b2ZR}b{MvUnAJK3!Q?1>FPxy*{5rVQ7YCPAECRpCyyaf+@=tj1aw*Ey zwWAZV@zFSLoqk22Uj1%ku@Amoqv^~DtU;a{c6f8ereBf&x~Qgi01Uv^Tsn=tj{ZYh zPW?$kbm}Q{HP@e5S9f7U|KwwNS%9gh`;L27O1EF-51Ozs@(Ol!NOihzdr;GrtXW8P z`a5`liqCS9`nP)hwTU>Z(np@~h6EYFMnz6`mFQBk%jnh`na)Aqn&#l zeT;OgV>I(iwc!3|N@fE#1e}c8`DVnyy*zG&3gG|?o0zh^4Piu-e1an1hssJ1SL6+? zgMK>(DiIe>ZT-lma6fzsl%f4%f$J9dsjRB(Y7J)Fb%=QiNu1RT7XZ*Q-W|+MMmawW z_tId~FXSM)#P1*9*hc@T>-0DlUrb9?j24Ay;Klb5dTXmRrb{1s>4eT-mK2YR%-13Y z5~a#M3{F4+%tJ#Ko_@p}!xO|BeKYEL{L6Z2&DJ`p+PgJn@$NwYsd-b-Bu9IB+*nMr z4EL~n9?5?kX9t!x@^#9H%X&^k1mKIs-;Vp)>Gu55zJ-oH@<{IEPhdI>7IG0$r+co@Mb<%y4w z$p#5nFMQSga+*zx&23qmF6`SMej^le5jA^XN^V1RWWcfqr|9EM2|3kP*M)w6q}2L= zo`SZ)A@}F6H#G%0vnz_HOQ`Sr?rN=27B;>26s7C{ElkwJ7k$IU54RO-b||dar1U+% zuz1~{P8=Q*{r%C0F72s@`He9@#eghl*E1GfIIBYdrZmGrp+O$=&PQ|>cRUqttu_^Q z>Rvn#gO7ey7h7^gj3zlh4!yZr2RWyfeq4198U%}k3(W4U@!8)5q~`T*oNRj!Kmrw! z<53MCxdrDRaN#t17fCf3aJIG098C`~v+etWgtr(-{CnAVW@ENt4%jN7%UB~uNw)HHC@k@V=ik#YW<-VDAy(l)CutBD?5x5-WR zmwHsT{g8Zs#q*Vg;%?>-b2_ZjyTHucr=ZFbml5pFcX!DzD08-Zym&axH?r*Z6}_&H zg^SjU2OLpQ1cZVo0J&z2S<@ynQz>nHbQaZv95dpykt5i$ZS3kdHQ{z8WRQipp zaggCce!T&Q{3y84;ZKlHmA?vwe7R#tum1VP#iw`Y|49nJ95d$Tw$(~<`PIzEy@NA}w(_i_UNR!qOY@bN7zGh}yuKvhyvQVQ(*T2nYIEk08I zx>~l+Rz-6?7E5yB!VY@XoKKV^ZLYd`HUA;>ZzEH`C$Y`m@$W8l8P-J=d0Cu&uG!VM zgTGhW$?5$1jskDBtAQ{YJkLQn(Tx^A5nus^o73Dy-$seilkuv0dKT`l0tp*~3x+^E z1y*4t<^*yeITv+v+IID4gKsg7o5KXo{0DQtY#OR%T~b~2_kKxAIb3YjNl#8{6D&{M zFDIL{c#&w&$^(QIrtEGHb|0FW-18Nc@x#Us83vKm9`3f);T^O_C*9{{wTbDe_)po> zea3xu3=NJI)Zsu_mhBI9<ddl+&J?j!ks2o1<;^G7Fj26HD#gBMtMr0zUNYLQP9lLR1O^kxjrRIyW`(A^?7!eB%GNAY=Qsq^RhA zRpxHIlV7B1Jn8iH`;298d51%1U@L*xjOMc2zW4oj502r@0p~Po`-_ToLhzAkyTNTJ z&}Qb~M~d^SGz$ThFO)pyt=v)Q%ghsM+5G#6lj2LAZ_ z50r!ktTiUPVL1!*a`YLs=c0UTv-1P8$ZhgACz@H$e!7$g-T4H~6I*TzI9@|E~>o@S3NRIVBJdO_$(jxc_+foV($XvK} zp<7O-;&!nS?+FmH7F{`rTytUS@)eo}nR|LZr2+NewJ@q=OI5v2#2$1DL#U}O+4zrI z#$iCrtDo_ReD;j*HsiUD2UnkEmaccE&#p!i&cb#rut5q;4<=juTwd;5;CrF} zs@v8p3JVLH9^lbU2O_^*lCqtOCnW9pm|n+KcJCPNV9bv0TJqt6^XTuGvj|=`j|1t1 zDhNeC@6_XBg`T4TYr1+1iC_RhU4C84S#VNo=7T@*V9N;$(Z_>=ikWK}n5X)ZG zu%Pe&%g7$Ao{_RxyxG{@ug!r*%WIKcUf5uDKM$X_BXIB7Jc%gRqS zis`i{uZ142WE_gUZpS}s+eosj)T`&mV!S>%^3_IDMRL1UuIMH=AInRioeP2zO4A4M z9JYQa2cAV_Wm3If>`(S6-`g{BZwaUx>8KGyYOX;9Y+gBg$noRP2-yC;y`!U}%Ow@y z+n*+!73;K;fH7V9nxd0B;@E5+OkY$tLO5(r8EMYe>T6sKpoGhtk4{`W1}q)(SSaB z#+sp1`jE{M9x9!+HKBJU6c=i_A||4~P3zsmneS)FyfRc1)GHBY5?a#jM0`Z!zqZ)? z(0#XxBJj_O0R;)UKYqj{CW?LZ>F+j{nbuS+i-I&|hBTLW3!Z{8Uj;wP@W*_mBqhD1 zuPeU+`2)`@4ttbP`lt|G-0Q_Wo6zo)rga~`-bZBN|Np&XUgtvYoko?_Qes&PW+vMZ zH7EYY)n&cojzwU{rG0f)y1qGaSM%iV4GV(B)1)=xT&^?kBMj`K)hNgKdr&Pi#zO8G z^yd#Bb3|H__6fVg@jZLq-;pc#%eGjQYft57`f_ZE3hCCrCN^hjVKJf!cN=yQ!&vGA zm%6S?on|E$BV^I+vg>XdgHiH8@a9hL(jtM6VvW0a0xHDA9&xD!q`6h#?#&5NKDq0% zT76flS>N%-X(prdU7x=wceWt4-bJUVRo**QH)#b4-_K z_T$#82H#K0EZCACzaRl7&hMlceAnjn4Wf#YSjKZ?v{G?h`*ME1L@VYVKse(G+}=j& zEy?^U5g+5t=Lrp>-?N4qd3EP5k%TH#vQr<;y@TgGW=t2KDk2xms`n1Prnx7gPRR zv$r^MT><1VbXRy}oX(I{@RBeuOQ!@y3-h3hcSo;Dy~!*1hl+K@&83~!8N_7XLn7yx z)uKBWWBso|`W)tIuRNvK3Pvn^7PRN&y8eOyEq5+4v)sR? z^jo{LfA?~Z0&l?DEFwRA85`Y_umQ6N5(@{1FkpQ)+C`^lA;!|m#{R*Q_LD0_6k`=7 zUF_Ba$^D~?qSnf(uhN4;p!?3uz=q!Rj_Ap;wlpn1v6@c_r5rE0*^H}CP+|_#c!v}O z;}&ZVscChoL;Tz}=2Z_o(LEh_6_SFgi+!^uMdf!HsYW9IrVZ^x@bK_yvU}UxPa1P_ z=2(Dw;dU828!5ob#kxDkV}2ZxQt7>;TT)`N^BMTnsc974{JFsf^TQ3!u=jn7veD+`H5D{$;{5pKBC}!7 zXVT_TcySXTQui(VIMD#$i^F%`gm-^GKLjohPC1l&(Ax5TaWjrL2TIeZVR&seKIX?# z8wFtrT1&AXMSWN`{%*`eeRTTQBrpFRw>R+efflX!1-%zCCH~r<@`{|2X;1SJawz^m z1^hI-BN=uG$LP(fyBmn1A@wO*-K74*3;^J6@gUc;MPVcp)p{QwB(*YMP3W!-nH2CO z(64oQ%t=LJ`BF4_JjVj}VCB@d2Kn~B0p-H^+D&aA9}SCZDAh6ZppuCi?PhWGcTWl_ z{ToAw@asCO1jxfyZqx&^I$EBD78b>du`u9_Qvn;iq;^$~(crx)Ruul3-BzhgcF{K@ALkSr)WHv~cFKiA-YGDDP zAi+PZBBNB-TA2;%J1r)QF~6JvxQw$^BpRdz>5$OZO{I52%9p2|P!7vo4?ENebtL` zYHBKNbI0Oby9>qn8+yeeXXU@Q`aZN`zf=mU+f=6&splPXqT4v*{Or$!!d7%bF?Qz^ zwxmcnWW8{lx*rJhvBd{deMpJYKVvuaZHKYL70ZXE$E`D($@xTe+-Z#2$JG;%e1*(c zSkT87@Wz+>jl(t-HEFhCu}8GK3zjUR2hZiejCxiDnA1*%t~jZ!wf*7S?y)x!rt?&T zL=CNEm-=+P7?o-ac7MOXV7LB}0UCUdL~37V*-d0L?BY>$^M(@=@0C}ic^zE_qXveL zG=6>&?6+nif-~Garl#~uxGX?MGt^COoplUkbz*XC8#d%e(uj{#A<$TjicP2hYnw2HEA9$rx;G!nhfGnZd&mA+nF(%i&Mt>#{(cu zZg{yHY*!78BC8n@f?Y^&7dI5dD%#9=LqEYzEOyMF;vo;0ae1VJ577fb)Mu$dFj(d? zSW6$kRB@!$<6LmHBXDwTn!$+Ox)&Ur%8BH}=e4ob7C&d5KA?MfwK8($j}gaIYDJKs+j(WdrM-|A1f!Q)0m8?<|9+=+eI7R33FCH2dD z>>Dh8!#}lCC;bUnuVwjX2%b2-A$ADwZ5@?4%OWjx8eXY#PehEwwk1B$89!D4LkKd%Gtg8uIJ)I6ydKk>?nyEN?NNrp-VXYZF?60z0#mYQu7 zNP2$58N)d0oxdw@Nr4`aYQuNF);4DY>iQIuu!t9Hm3Y5X)09Wn|3vGcH`(hR`a=#; za;ZK@wg6_5(Jb;GoN1LM4jnL%?MJdtOFQYkV2#x*jn0Z+0cz)EGAhqL+Zk{n&9{or zhZ2ynR}Cw?CQcsDbF9O0s`#5kMoBxD)$CV1_Rx2Jis3)_1rJvO;M-S_NXmzz;W*%! z`33Gv9^L_fjuMVcRz2D%onW$p*pBf?QQc5pNfG;ThM1x$^_eWAPe|~P{P>&Ubpfpgqf(w0 z@`?!NlTOMoJvn&@d2V|z%5ntM7ruOxIYf%#Yx4&>hJ*#(XG;HTa+%f>zQIGwl_y4Y z;brCC8OQdPkJNEdeA zd=g-L?q*L0LdHs>U{9da=|!h0GS^3)=Al=>zq`P9_x#^aO_~=N*JwA)I6J`x4Tg2f z1yFRYk-yHPNMC+o<(qR1_ykk_-;m3*Kd$I39uU5<9z`2zCt$!-1*Sd1X`<(AZ}LSx z@c9+(c+JYiO3?dTeG-R@;O}OTgg2Jlhl0-RtGLmG^4ishyZVXWy~1BO+k*gtZ~%b9 zPx-Jn^~{%cybS9QSbia3p=X!Nys`EalQ9F_65-c2bezhT4^qzCww5|wIbgdb9tgDo z?AiXL2HH#utF@sLzsV3u&>MSUL`3qF?llFhM?~(oOr@ZQ!KHFO@AGES_W-gf+H2+k z(aM-Jyo>z(&v`EotU#54v$)?J%UTfABxRjeH{jLo+zv0tQPa@l_E}V~P*af0(oe=x zC5u2+HI(d>osa#Qe>WXvvY;sY=Q0cmSI4z-zaUx}23UWaImvBl^` zkPa2#6M5Ygj27-C2KFzuA?>dceUnsy+Qdw+>usKpaxcBux4Mfr`I#Pc@mPQPddnv_ zkn)0YiMQ6u^K>qGsW#6Ms1tT<`TLgQx}j04_a~2~=}GgGA{VQ@p5FhD;B55&8#t@i zsQRSUb<94C!Gf{E|LH0-v1muUub>9mGqb!CG98u9nK;u{dl}@({3VnU5?I+1uoCO7 zn9~M(%!h`+XT@7n^g`}tF`&}f5E4|eHoaJ1l@=b4BX^MMGUIB>S^|XOkiyY7S->xw z$`cDexqMj)h+~2-OkqzHI$@(F7kYbz0vFXKzY?tC4Kq|uZ~RJtnpL-&R|NmXF^HpjF33#SI)5_5nqDuKzDRQn-%vXYccFcKjgeyakn zV_4J2zZt1lSA37zaH{4vits!)>y9UO&JyQ=@V|hvElPGtEOEgI;Bjnv>}n6O?ct}X z>=BN3T4JRKW~(-)Sk}al=|4(igoHN)>7`aKspm~gDJdzq&TeCu!(v@icVdM%1u%dY zI@UCU%y;8=-)O$|b#G(sCtVyBhtOkR zOwV%;^WNC0SFsG)LYuH$I`vYl+kn%|btEGv8- zi%kh@Czl}zclhzg5>lN_RrT45QoG5mB+x1^T>8bEbQE8ci^ZQzKx&z}d)r6>=Jg9` zmRn2oZxYJu2tn&+N9p2b%=a#TQ40zOOvgFzlmXM~$zMP$Y|)GBjJm_1*zLr64zZM! z3y)H^;JxUYE6z&$?LE1xx{D>nc8^m;pw|vZ;9tMat!Jd`-wby*#l40%vu7W;=ybA` z{`mRNU*L~B&Ha(KYmAUz3&L3FI@z`d@mYx!HL&Y(7f{84?O8Bulq1B`lj%Q zixFCx?;09h6l|4U1LD3WGgDrNA5J;h#vLI@`X|MSLV|c?+qYjL*aweB-|?N8(AI=1HHo>5p;q*U7~t}cu-E8 z{`VO3A#e;Vz*hw;PkW`t!vQnEPBKh=i%}upOMXoR)5_zcD&q8QImgGek_QY{kvQY zF}cM`y+1tuaA_I8Cwd1Fq%nbmFst>;YQe=EcSnxc7iIVFlDfSwF7&8y)COpl_4DfG zgk7%f2iZ2HL?Al2i5U4^$!93zl^?HI9)r@NaN;dt6%D z=zX_tYqP7%h&%h~+ zFQ{qAd}Gt(y62$@<8kL&@9)~iZ;ic1v)Y5dfD)BU~xt9*M@fuucex1A|fJ@asL1H&idj*30JRc z0cPhv}WS8WZ#w|w-_wz(0=cCn63YSOm z@>k2#O)Z`bvTgjd566NOFi#o&OmEEjKs&(5R{`#=7Z!Zh|KK_x!UY0L||b7;!<*vZElAuD*qV8Kt2R=q&?K)Y!EEh;d8m_4sHcVeWvGeJnOhA1NxXb zy}v-3#mdji-JNr;OjxS;Zk%C1Gx3-I<`2MkiQIb{1v&r3YhgCTdi_lrbRu3hTZy98 zLeQ)(rL=yj4Z2NSaUECGsXdJO&${*|C_{qTKmZ*dSz6s3-^RwOM8&Es)UVsLq#gf0 zcjo``S;+2wc25Nh#XRk=F|r2Ecm^5opAo;{b7IRy`dt~G*f z$)-&`R*bzM&S@0*<=2!3~~$0Cps0KClrBje*bY6^=F@x?<_-rg@dDx??enZx^|3^6v^1L12nU*G{7 z_eBV1y>Xc>eY$H;VPTqVin;AEE!ZW`OE}lxPIcOcOLASm!BY&}Y?*q;(@02(kCHb$ zhRes7j(WxqQ+BG{Js7Z_UEyVs%~y438+e=RyEFEZa{o|DdCqR+!jxcoby|94`gp`_ zo5`XxZ376?eON^~D6gRR zpW%WNeM6N7GC){x9Uo$1=E~K0C51qOKRyA%rY7X&u^kzYuyTDE(P$IH31h`VrVgd1 z%TLwYpGDa^CtX%zSVPMK5t(p@)3sOX)=s^N2eGSYMs)7B>C9uAYc2bXhwmwKczCd| zZ}#$w9X|Pc&Zl`QU%NPRz0U4GUzv4-9t8h-1ZGKRu4;R-45D=+X=-D5_Ls zu&am38j4sg5~PViN6GoGWIzi|?r2Vg6(_}ZZgM`w-3Tk8;R1SPKdjf@Pt0O{laU)? zdbfRT($p-2M;Fw&M^@>U_h^S@ggFNT(AD(n8@pZOP)feVlhU6v5+Pc|a;4qT!uiIc zTT>RlpXfohH9fe9jyf}n%Ezmc4kc_SdG;cdbzX5{k=#{9+9#9DKL7m}7EH+Vjk0aW z{r+}XDx#!-@~U~~P@}av(;Qq+eFoU}=~R8v>n{uFMWC%&(}%@r5sYM+Eqj%R(&Dh(lXV*I6iAL}hbOFef1 z?(|LX-f=fG7x>4-#6SrM3Gwpsg7i&YBUO5SI;=5a-pFD*FeVbSdNQbRBJkjz^w$;l z($%OXsSfdaPEL9)OXZL7C_-8#S8MW-J|vA#g}InHF4I~B@~2xdT$`0`2-LR4v+kq5 z+-48)?@C#`Fzfx6-?_w#K2J{cT7`&ouqYh>(GG(KlXV_D*gif4tCLxzBPFFfz8+wI@v_Iv#6(cI%o%R8go6O`0D1Ne@Nn;9O92j!OCjSf3 z_P7$EU!+UfH>8(;HRPA|Ob5Dp&i*($LooSf7cK$Pfz|ZfLkTTgQ0XxL zQ%09^v!yIWf$FF38ZmK$M!8QJhN7oxsq3y+6QzO~%$*qA+(3<9_k?_5A!Ey)vUxj? z)jOl_b7nr@-jQ9B%#WPQ>Y8yj(N0BWWAR6FdDEZ-{vxRV$`??oy{l5#XzSnZZbNmd zwlZpBCX$cXaut#!q!ePmo(3!?Nv zv%6Y&$#YYkh{kzdex~cFCS5VM@u=YO=wwrIBd)sJ3D5CJ9HC#ps`32#VTR}2LCuj# zXUw@RveHk$O8F_d+l9JFhp&JKLhR2&2=FJCs=BH_o#VqtcM$5i7f!nk)nNFg1k8k< zZW}%mxBGOH??b}#s;58r<+$@{|54bxhu$;pZ&j83#mvDkL2^dqZU!2L{cVWWmzo*~ zFEXBTa7M@6Y9cBe`>yvJtEP-XYBk-S#1I}^Zysl-D9745eI+)0)x|-uqFi9g!7aof zR(^4BzMK;c)8o}h5Ju2<;>$H!)sy@U*SQ)WZt7LVm+I3D?!|3KS4kjFUV|7df6KVn zOQ~CVb7TYf3_9KE*qjPg{`Kyv^0$2m>&VVP5%1xu^5b2`iY2>%a3&oiQ(H~mvLBgs z4Kd4!>=mP!+n8X&oTPLIbGeR(KMX((+krD0g=Y3V!Cr?(q^d$IeONh^so6Pu5S1uj3NQcjr$R-(R<^^FzqUk$VpDiQ8+ zS{Y~p_5AOq(-PE&E0kYVgAJ|k>;qTjnw&Q9PD-Z<_RMLz$x72 z+?S;-27d74JfLIr^Ws0yndVHS^-a=Dvk<{{wetn4j0JagC{-hfThOMfE_Y~bTG&;4 zC4rJ_DUB9J;q~DPnlvYXn20Omq=uOP{%9_5x)iyFbw=zUWtskh*Nwj!6@Z)ZC_~6@ zcM5vgouy7G{NF+V5=e!7cNqbmEKkLus^uzB3NIfRHd(vkS`tfe%E!ZOJJy13)4unc zcnx-(wd$f}wQ>!jf)(Ym*lMf#DS8|pufDrXg)%TOgak zH_L+kvdcI=c&RS*cAr3(w$7GfZCy^8^Bsi+p8JDlLU) zx&Eo|XTUSI-hkufR-Jsd=KzgPXkfAwS#lueuA^wdO7~@Lwc(~1E$9eOA@Y!+vl0~E z)0TS9g5Xeq@C>{}SwW07!3))uzdV{Ov3a@aWR4+BQ0OROHOq!pnU*SjLB~fHVbJB5 zbf3I<44?D(T`WCq7H}ReuV@IE!Q75_<_-M}xvHBa<*C8$S%a&B5jA2=PP$xBXeXIy zyuxLf$oBQk;VPyIt@_UF?(f^Zn<3(;&B6hLh zf3fwJVR3V7*ysSoy|@;N7c1^oin|vt?(R-YaVzfb?(XjHGPt`BFvvIWyZ8RiIoEaa zZzhwhB$H&V_1tzS%lGoEuetYYIO>7V_5C&s)tOGN&5t@24W}DW90sO&5c9M24Tfmu zp87)(o5CRSBTlsePj>ximzN!aZSn!5@}P(Byp1g9%dFAni5XMJ#l^yI3+a9I zO&bhWAfH;S0Ot8Lz9VD<)u#-`a8N$PZK$rVqdo7b5X`W6 zM7bngqo1iKXtf*Z-M;(ut>)8H#r=EncBjL)F8K?ilUaGj%vRd1hUS&o^P#Drblxo% zu@oPpV0|4HSat&s4%7yb+=)-S(|65;pZ?)eEQ{a;mSfSYXMu$QO{;fOzcmyF5*y1> zzO=TQEzOCLZnG#64I#euFpsoWvIG@>B98=s-}sm6W2t|pF%YqZJigu7&=5S}lz&mSy3Q*hF){w- z%BoHji)Tt%Cdu;ji=&{&hPS38DhZ!7E%Sp13nDh3fFZa&B*VK_+zQR+IyIU7RnAvT zbCFu&65i%n3tx0KaWy&1-r1#h1~r$$%$Bd!XIq6Ndd-|*PCqJ#-J>G_i3UPG1Xk&v zZZgQCiH|jm8GnWH&qauZ=4?xa{)vQe<-I>!cny)y6JZgUm3fg}drnSRDID5Ek{wH9 zs4aN)%I-hWr&gPv(&!&|eE(pfs5(|j-sz(I*V>f>kzEfnM!oOeyyFk7h2p&8fGxm0 zYK~x>Ij=gISQY??eHlF~RQ0eR2$c%GI}snk?s$&?!n;RhG`m}@#dcSCv!-Ih|36}e&HGx%uoF`!-`XE9eQW5=k5gP#e8agfpOtrR74SE!rRak= zUejn9Xsr`Ej7GE>HCNeOb_g()TnYsRK2~s+_~7GG81BC7_NZ%Ifus}&4(Nf_K{ti~ z-nya)-Y%YPD%*UZs{RGLm{dz=2rIf$S&f4xH0<~PCtQb38yPIn-t-@jg{zma_g6cODnB7kEd}P@N5a{ zuxF{$O)a{tH=}ushn%n4qvnBCN-89fJZ)+`JT5M{^#BxIVk3$rYQf6-OBzZOb85dc zPw$eF2KzEEzR&L}oCU^ZZ*fhus+u}SeLb$>1D*6rA&AkLmjbOVe^Nnqw;Q5SgWmE@Wo+(_><~eMni}q6qvH)y<%JbQ z{R4S-f1&&0-7p`thvkd-x$fnGxn5O6S#h>nuhnjfeWMwsXNvF_Q;9+0LNGE#8B%Sy zCDE5JR;RJdL279Xy4iF+p->q`(Lo6rt>$3e0eARlHr0y(|Df8l_OPJQFeW%RGJ$Yv zUiY(RYPh`pwmEGvtW!p581u(F@~VA1O`q#DypocV6xfUiNYExA!md6thC7lWq{Vrs zcfsw0(cgyiYEHlMkZ22n1;$MtwKrCdV2F(I&m)&#=yZSoz>#obhu?dFLPF`dzZ$jb*A%I2skNR;pM=fYoib7t7+C zQAT@|PO~p)60NQZRi1h*Y)sOVdB&B6OYVni^5T`?tr3lp-&b2w+lbN%5%dX&Lhq%; zQb;faMUcSpR82~w(*{CPsWa zOKU?z4x{swd1)j7VeKWU=J`(+b*K2s{dQdk4{nT4ed|SDVo9AsZNsbUqsQ)qXsTi- z9#bov)PvN8jq;r_Dd(ggdLa~@+AEZndXvnC4fBbbMJDhLG6F=TLz=mIWNehpA-y&~ z!v~4x3sxubah_Z9z2E;GfmJ7ml6HK0!b?0wNl1F(^uzl^PJWqnZt4>MD5_@V2?0L) zi;8G_dO50d{SbbX+Xi8RrW^-{QSCr$ToHdEp;h!Ou~plnxip@|RiXKU@_#rv_8Erd zc>;^xgM%SW5I69sw)HFLq{xll>X#{xb_f~g;Gi?K0l?Ukn1f$&I&t;V2F8o#nqFLN z%B*`;m%rUSh#LCj&oX^IO}8{N0kNTX6@wk2p`AVu1WdWdj|xFb31|R@NRwY)XJ9sf zfAJf>A4{VSP*nvV1`w991IdC&nF+d2RL&4Txa-6v3i#b5r+)M|qq~mT<9Xwmlhlf^|M2~Sh`EvW5#mXeb-2c z22|a?+Lm@UK4H^gxFU=#gz|%}Pe(yQd&&J~_O&MK1PVa-=`mG)#f`&wf)a+&CptN* zZxNzvNvd@|Titss3A3WVFLx8Pl}=a?TDQ#Mpx7drxl^xC>-pZr-hQx(#CR$&=!+2*GHi@q6GvQz>DKDOf z!MpVN$#lV7qTVPK6!%ks#FUWZt?hV6nRYNu6)1`!_ayTyvyI8)ulA3fy!l`*`R3wl z7ad=~?nQz5w$rux%X!)%Nr;iHM5gmAH|!1HjHlHpoda~HtFPZ4SPRfC6N|=_xxK}0 zwb0U}8_j4?@*`zzwj`EFB*{^diGy#}FCN5GKy;?h|K2y0LpYAGtO_Ec zGI!>M6JA(y;iUGT(bw0Hg1SeKw(Iz-E&^ar6cpNZ+0x^f2IHrqJYDN?x2DHJ<6R$I z*D+_X+>5;%UGn#({%8ewyT<*U{^%KlHVYg7MHi!hZi49{Y8m-qqvIj0!%YlI$(ro5 zq{>;yXEL%5EX!EnB|NUQ;eIK|X=CV6kCV^qVEilv$irr48s5dqDvG1gaFxa}sTZ%J ztSWC$sw)VJJ>a~|vgmXT`Z8a6y=383vY9r@O|C$wf zv{pInPR$rS5!Ba0h@r3E6Z`#bt6^?!em>$;AHoOd>)>t=CkFe+cpU9UyJRlUz0~AJ z%<73N#|_hf!p=(n6xmuP-_0K4@R2gE$y>$BmO9sy#HIuKDgEenPfC9RUO!P)su6nM z>-x^Mc$g)lyr@W=t{P8mRh-I`Cfg-_cxKl)4H#x z=&-3?V#}rdl*>6BwasP62goWUh?atJF>LB@ z*v}e%uKn<7olF*uX|e^jx|}XfIH^Q7qY}qr*Csdmwc_aZ*qz=Vur+rb!&q zTfOh3u;gKl)hfaDKxbzJl5CU3a_H%Ch z^GJg=%Fp9BgE|ufrnm(1)b%SDmp*Y!7QL;lAFU42OchvwZfEUQ*vMBrTGYM}mNN2* zwWrU;rDwe23QQv zVKp;6EF5QcfKf8rsq@ckcTUR3KbA)2py`7&$l6!e+j=BEm7tQCGQ$CcmF5+xZZ{g1 zWrIu!-R(1~pn--5DPFq`DIUfLCW^{2=5kdii^*`)CtThLd9-?64||kJM$&(@Kn6~$wOUKUR{bv zd(&YSamfTtMr)fE?7h9Bv zhBFou*RxWkoT=f=WyaYaF4X0tVU1PqKJ;__kFHbWBHHjalw3} zBYt6?Su;@E*+7rXt-Px)>buI@t~TDJ*lwRZDM3ZfDO>+d@AF(4r)%oWrerP?-^F^& znBCXF4Z2cAd#C%Z&<*F6nx<(y+y|)){Vv3 zpo&BJUl1Yf4Ig-fa|a(o%#A{s^7(>jR_r%Y_Rnqe&mM0bTc4SacGu3AORAIliinhM zD605p?@|gR0!_`zMvTsbCt<`Q6MYFb`(e+aG<9ai^BaJZuY8kdgbp`X|Pf z;pEKmYYcKhBi!rYb<7kr2u}vw`*>RSO4^F*#+fHUIq3*`lVQ;s?tFH!<|H@gjZP!Q zeAZg>>j*ByN6Y>x;9GBVluAGHt8ftj@a)roM^)W(GtpHZCeh5=YQ#LoA1f?69;5(m z%5uzj6DDMHQwnoXp6P8!qOQbyp;LK9Ocx@Z{e?IT0!RG}(bdQSN)mW%&$C=BI*Jzi zxjU=^66joUIncCx(RYG^cj7t^`An(junqI=3)vq6!<@qP3)+V@juxdhbq-`kU+$f& zVakh&+Kdh_nBBX~6fyw*zP^Hpp;CyUp+@4-_4V}->#%SBQ4f`h$|GTf@2dXijB4#KX(;)9Vs zGYYI(WJ?R>)Im<$0CBYA9Htv5+w9_}dK)WRB2cCt_XCD#Pq#yNeg(ZDwXD~2r*$ll zUpE$AUx&v)hAN4?%!tFhFKng|1#oT7&#`vPAL2IK1R`lOAotlhn8KTN79YQ_ z;OnxPENtf`=h0pBx0#dpafPVVYoeP-VrqFQKezB0JR(BJTM;b-0nLYzwUGSrEc-b%{%4_%;dtDH z-|0I=9o4jsNVUe6N^%h(h+7O9K{YZlAaZsy~C0hbfokSoAumvMiHaB6SmKcO2M+1|D3ONJ0O*1;A8 z>v|I+PRaS8Nq4)8Ih$ALn>Cqy&8e=|24*an+L9NwejyPfXe4w}f;>^!*uHH=3-({H zOu6;V@n4vkpK3Oa9FbyCEvP0aM6oqtEjoQ9YhP?7!*Vw7T<56$Ysxd)Z1}N2wkUjG zM0z};+dO-=VL&%vs|^`bUhCN$q|6u#Z?a{4FlJF?CHT1>F1jd6x1OK&M&DJ^fKYcDn<2A*NYi^6D(8AbF;P^t-z|R%j3AAR0S$3 zsxDa>8A;g_dpQh0TDLEa+S$b1<)N8gvSt=rhxabWQXD2=dUiZ>vmPwNsJd z-+P{w*0OU>d|;CAra5*<@tDJ>oKYV>DR*|RVeG9#g85N9a{ld1#gQoOtLNzjIa3Ag z)KPvDt^03RWKeLDgY{XyHr#DuMS3oy)H=#ZOPddAvkr|%Y9qL{1IDq!2Ja51jAX!S#E6KbLGdQ ziVLd;C>94?tWtU_guk}?SV#=4z`-gm8`~oiqGKzyD5s2k7CYl&CRoK|ufkKJwIy7I8-wZFFg-Hz86i-Deg>>#pA>lJ+T zaSAcD%VtP^s#B0WsR|%G{Sr8#FWKsG>KbhI`*Cc$v-9IkGv*)0=liEse=5jZj|}5o z=qJOie?Uyr=U&@&G|j_-OE!qXd$Z#^?3}FQgDl#zyWoSSxa-5|d~qDTxQ85f%!+3V z&0ziQ`ABCwc)iY%3DCsLCwMj(o;p~bl+rsWKvGW?*i41~$;3C^7yDzbsELTLi;27| zOI2KWmC52UiN@yq6@VM(cqV5i<-DuS@k>l;tD>*C^V-IIZX}qpz`cm3app9y6rM_gz7L;a^!~ILgK^4n3bhs?n^?6yc z`%f>3aBoT)TM$ka+~Cl-N|krAswpFg+Lk;>{Y&HiY<(afGH#)MyWdR4sin2ou&DA- zvHpWp_s`-at$nFQmtRqpczb5T2vBr6V`w%YTGIDb8n2}Dc689S|J`0x#1ff%GAr@E zGGnu}smkfF;(?B4*QscFe##JxRR97p0WlKk+}DVMJU-#+^r3-8}+% z8QCas;ilss7(_)BB&vmE7VfVW<@xZuX{vsGkhi$m&U0Wux04Yh(rOw$T}ZHxYO7*- zCxemgU2L&>7=QuoXJqU-{B(A@(6is(@(Bv4^KIi4H=7=reSvoMcxYqxey^o6-j7Sp z?-h^LxaBqW`_WQTR0sVg;5d9!AKo{ktuMbR?_-9!ul#%WiFb zHqlDWszSfC)S5iWKx~qB{igmqs zb`}?1JZm&|lj&4A*=o&3tMTQr49Wr*oAQW1;g84BW6SQ##JXH98p+Lz)r{aFmuK%V z+k=Zqo%(J@^?H@fh2XE6>DbTv*JA@snh%PMyIeiN;v&@OO_b7bH}n0 zkMDx^f#EgEHy!|hiMen>%XUy{A&rk|A>^2|;$JLFOWc%Nn^dHL=S9({x4uk8ArDm?c~D)mo<@(5P}{vg4AT ziW_X}*mgIgUj{y25s<#RiW*f~^zhM6Vc-6=>~zG$cG9X}cS$FGNw*w4Zd2!KrzdnK zNt*U0&uX{H7c80A=-~eC#Ft3HGWe4aX07YZ;O|oEu;%u{8sPjng;#y}vXy7`V4p84 zK-{1@{>~=IT8r~r8q)L?H?K&)sm~#^JF9AiK^iA!nw$Kc&IC0+5_P7Gt1LSn9L8D4 z5|v(9>@6ZLx6N; zw(tsy@M58~PRhDNpVV+T{c`G1$es5JPG3^gMb4EKmw1Gt1dHvRiz73u^pH1gA}Xai za%j$v$Ytr*pLZcE5p!DoD1Q3&+Bg4}C-_m5P;g9W{-s;cZvl+~>hkqhb+U+}$$vZqK_am}&@8=lW;z8mA~m`C&X<2@;O?&bEn8ZF+Pu zr_>8^$xC7ZIbJG;Ze}*5ujTi+$#>9F%)h0vI2y~X-ik*w=twVr{Vv<&ENI^x?E-zCs}#bN8K(tLoI>Vs3*g!^#pa+Ng|M0+y%D z`p3}p`9S?%g;SHmREzFuDjG{ud{27oR@#ryV^OcPbpKv6)W0wh{~r5}5JDBVrPp-S z;kvqza@!=@%HjC#4Iule>)-b30X+X}xJ_|BL;P;B(1*JhCn6b}Cs+xKEn&3Yd;B|; zf4Ohk9c@wO;>fae;4LR(T{?!!?2LvruDEm>O+U=+WPLwAccWK8k_c6-)Di&Iq4Sxqg3WV8m z)&IFiUf$<^B=@PieZu_eq56q}PETnz2iB@G0R!+)47D68Z=&jQw<5d4&6PbXHhnwM z$2c&|^;~+y9P;mnp@T)`0+-gdJIAkyrSg#>BQJ3jH7x}SOg5Pu?NxOZ2@L_K-TrDr zySGGsP0E{SG|W$LR-7x|-98O|s&-S^SF9faAv;4XQ=@|A7&JFi3T{rQ+)tIFwy>H6 z9fr#{M})NlcI!iF;MK`ib+FOL(3i0~aa+09!Z5oq&7Gs#6DWzF(0UHNIk0){UaPK6 zP9&^W3fAOJx*spn=E~HAJIg7chFiG|h@jzH__Vb7JfgK-=pyABrN&3XH+YPy)8g`s zrUWVQNMxSsH`lm~KLF$crOY_T@edZ}CP!=A@(!m`{~K}& zvFy6x^RJ4&hCry{RXq42c_d$DP)6$C1yEFmrxH90Fo)=!z=~G-%H3r}Lw#a30itMo zm~Lq?xTUG|Vz}gReZY0%pl;e*cpanRgr_Gew@-ZBrsMj2x!3>H;1n%C{{-SoX>U(! zLpL0&aOtOo6m4rCh`Qt0O$`9XYU|r1DMr?j`3`{Y0$BlgG)jg}q4W&&ez28UrVDa1 z^M*lP?E2lvOPYhqzIa^I&gDnH93O&81*!F>CKB2r| zr&-9y!7_SJM~mHBShnE+vqERg8hk-X!|~8!%t2CR#H!immaQ4y#%F~b;)ugpdv-&foRVW^(aRsBrmLzr4l`qhp2Y=V zI^=Zy>w4=UJ8%Ekyn$D!O9z&h(<>4zAs3nV4C!LQ+Pwz&SMCUopG%Lntbbwzb|EU^ zx7w=2?(~a#iCb{-@e`&#Cz9^^DKmh8Yi>GESsItxWDwag!a%e{a`Q;`3iX8FZCH4i zwk(LXzN2vJD4DEC-*?x*A9M09q5CHKf)g}f0 zeaiGt^rhpwe)iF93gA`s54xKt20F+Ta$6`t8GAjoWws_J>&m5$`C5LzugWqTS!JwN z&xY}LM4!6QHlr?b@WG+o{EG>?zgF|wz#kW=E$BK4u6A~@p-Ah*N_}rksUI!mfx!Lp zMhM{{io)OLY{%V~s=NQ>xBD{@Vi)Ly0u{K;iSk)_{Eh6h{4!w_HuOd#D>7vjRttJxUf5deT~0mC8^?HciAey2vEKnY6n zH<`b6dBr~T)~q8d;|qFfdM*lV=NuIEsnw}ucM2;jBXtfPt0OD--rEh`)^qwF?V2j0 zy$P>}uUnT?SB`dn1ad(yW`AA1G@@wv*&NI*r(j2w)|m38qWHa?)0Gz&bhV?ucU4dw zJ01ia!lAMl8p?TqqAmonAGTcR=!|v*te)6uT3-H&Ie`WeHI$1ygXeHwIX^@%l=-;& zt~Iiu-w=MqG-;c;HTAG)(ovm|TiX?C%HQOE&Y+n8EvzTIWM z&+X(pw>7b0Sq%s4U+nH~4J@ofcO*(lbxkykyxQ)JQ?m&#PwX_=8Xw0rbA6=Sf^-2O z4OE?A=Fa9}E&SoIBbkvP-?=?|b591c6tRVl4yz`857qC|ry%x&Yhj4{v(GENGF|B_ z7s*rhm!h+kg6X2L$}toz0sz8Ii8|u=4(P{j)I#xLcZC~cF6IGyfW{e43}H!I60M)P~@RO;=9 zkh@lFEx)};hd<^CTlr&lA=PpUy6bJ-@%)-Qluc=|-zd!prqGW^(CeMe(bVYhNmO~w z0f{6Ikm@v3a+fAQ-iYb{Qo;AL1ovhS$S4R@ypH&yYGQU6^*pDEZI)qZra1mQ9^LvA z#=O7z%hQ)cZ{)PT{TY1)!-I>9QP1aV$pL!gixmv>>1-G*See&;pD2dtdMZKyaY0DK zH<#9OHyx;CvPl(~MoOEPR^i;46p3Nr>oujqV$ZzvwL30V}V?o$tx{ zL@9dFq85fWgBenN^9P+yAt#&D_Pq27X*EH4lbR-X?;x-!$SGx zdWYZnC-4r%seJohU48;XbZ5Nv(UJ@s#ps$RaQiZ)r*_Xh;kBUZ))TaN z;G1KYUI;RVYxwH zQb3h^PHDL*aHKkiWx`F#9-NPr2Io-83Ag!Q`~WrIx@(V5l< z`Ct@8f41#;~A~D`B&9zXa0=rFA$ygW9y$^Y@mi6?c3*ETta8%@=s46{@EIwrIM z+hmDu4ql5ENkwcP0()|_dn7D13S2wd>UmHea?NOq;tNYcFi#?Zlfk*h0L%GGQE0Gv zT`_w}<(sHYX#VoNxa_L!`q}owiJ@xi?~L#id-2uaihe<>g=1tpArTB%w@S*fM_E#H zS`paN_>t;s+xVj~P^!Ss|015MrN;k2o`D2*Yv@CmmsR?{OJ@}y2mS>+OOF`!{PUyhSSW(m2o%|Wpw8>XJZJz&&HP7P@MW;SB~q916*cQl!(w*#KUgPq z?#a>5X`LJ9MWG2(1LN@uqq)7RsZ^I68yStv{~?{98n72~dX$tjIPX2Xcz){AJCV~H zEy3F|9kpD~0{_6zBrPuEW(qhq?f;G1iU52?$rj@@7CTZUV*6(~%}%ocHSBbvke-|! z9sN99rrG)~2I}!a5v;5!3<(XNuUUb9mA3W#eq^QlPDh$*$Stl>O)9k7xU-tDcR&J6 zc)?{Nxv<>SRcIX8V;9f)Hqkael!C+0!l8m?!aG=dY@vnY_l}B3m?cg{i-uUu+~vj` z4evq+B9(rypDjd!_(~)ybOR_X$V;0WH?Q={@oJ>@-y`3WoBHxEuUd>0(_mFg442XSgepo`~ zBf95|v6RS+!j!KO%6;bBiN5Sjou}_7V;2t`^4DtwEOoa#waZ6Y-rN&kd6-{?k6;%t zfC)HZ<=iABG1>Ghl*k>0<58`p=aX|6Mi&ugS*R13asu58kF!d@JhhNt^O3XYCfGWl z^i;^{=gaz>ogr7GkGGcYFXacOruOY61q6eg@+l&AgI)&QO$RC+F;Scyy+SVkbcC!q zf0^Mi4uxW@1QQ$`*Nuzr>j@_hW6{8T!kO=}4VQ+ZqPBvP^yM`4b@3S_5~ zItddkc&!6i(Eu(|b zp5tK^EmU(u=PK>7l}t`&D9)J~I-iVxb@UDKR>~nn8uOK>?G;Y~04_!TA~B?E@`bJA zY? zx|7}3CeQsMof7$fXi3Ye$-#uiNaVG`krsDl+SI?*W1TaIL&E_O-aHW+ zBY#DtLv+{Cl=C02BH$8#`8p4}+Qd-sg6; zyFQtI6Ssf~89sFZ-MB2``T4Ou-ep7#29=Y@Tq-7b$tC}!*UTXGxwbSEr^3TiXTQC> zWsNC>7FIDZTuvC4Xi?)QmqJyU^1T>L;Nm2S9;+e`ND_Fy;{V)Cj2SM4+}?2gd*_A+bRzu{;+7t;xjrHvg~sv;(-O4 z4cp&l*RAph;KRE~sxuK<>qMv2k)l(&Nx?E-Yfla-_w}~8+@IgE z$lXoa9Jf}M*YnZwSpO_txw{3iee_xrpCS*EtGd)f^1G@nPRy5aYdix^O-Lrft>1-@ zeGBZUptsZY(<|UIE?GEr(daIbndqBuM^f$?+0=eAaes{K9raB0jE17{vP3ZYzL2(D zeTtsBuWBzAKmLqDXile4;p1vLG>|{)=?xw&MrS7f-Is98VH2iJQ;eI;d|{~SBJ|*5 z5%*UoNsl6Exuia9U^hXa!w5#w8<{mF}c$P&*_sC{f`Ua<#PCm zQ#x_r?1?Nm&GY9mjHW`uIs}td2ajsnsjsna7>PCY<)=5WHJFx_={kdtCzlZ6C*u+^;>!xeKc zc3|;7W2G*Ol}@g_YY78mMrr^@~CcDP%XvDsk=-AUVXizKy;!0TR;Jd)D9cTr?mhTEP4 zRdmj*)$98-ytgO&YFp%*2S@-UCT^Li1!r;n`7R$8CeOv`Z?Ze8nZ-%z<)WabB z95RMuYhA~qhl%v>O2PM@s$@uAJ!CCnum#N#>DWkBueTy=b7a2A{I2q|30@u_sclcN zhl2-`mh6u2?TPk3mr=%Gb)KKm1AFS!38mvqO2~uJN&8JFj#j*85iSZz?8=Me>dXuP z?H~yGNBVg0Q}yWgSPlDk#j6o*R+V)=4#772t#6@E&DjpE47MX>OKR-trRO|-ZSKB* z?(zrpCc@R56%;1ghN=9^C3)*WxW}AK=B;1yIOz|De+8Hk)QJ-!!(b##k>Y?p)fy-B z$2hov-(;HZ58carJ+qZ~z2Ks@ZP)eYt!6$r8IaL`j7In?s$QTI&EjyvC9S0QG$*mn zV!HHJSM%J0CnnB(TqFZUMvvrA>k&70m(^^^&hh?`bAD)dbd1JMxv7UJ7&l`w-(It7 z7;jbTROWU-k-*^uWXN@%YY5Oxbg3_v;rB@?ux75&stMMEmlr#A_ z=SNzS=0eF~@|&mfJu}lKKf%XMkJ!BUnZJlQfv3uCvRSLzr0{@RY6sBh4<^L#kgT%b zH;nGFr9wY) z$|VEMEk@4$JeOFLmjd>woM!16l)^H3hNikJ8=cft^jMQ5A>=wk%+AUM8XaN$R}t2` zus70mPQ~H@hY^YexZ)%mZab}mwNM#r$vYCOKy@-lDEG_FJ#F;Ny4FDQlKFy6KKqhY zPWv8r4qov7zE3by?EyO1+e;z!X{IjkM=fJP1B2|SA3o~y$6E&u&*HuhKUX_s`U{#h zI{1iK0PZcJp@CWgjQQ1t(Y+zRlVO76@_mLXYE|=VoH7p;ON0{#vR1v)4oJ8+)k&GX z+SheubNN~Zm*o8 ze~2DN&0&(4#YWk-F>!pJ7C{1+8pmc0G};7lFhBli@LwDpY0X;q*n503`d;=)m)riv zxPDNFYe4;6vLNbxH*l~Ep-Q_!#PYcAAt)HiyTCxY!GgK>{e6R3%x15qKjvi;VWp}a zY`;H5QF;xb3@!2j<`@nn3dilPVblh#hriX4BWu5~FS||RQF;q$5Ki`tUmzV znx2_+i!HD>g5NM+Js)q~+w}=-qHD%s(8SRgzZ_I!oBV;jA9C1#LoOUpSY_qOHh-aY zQ^9e_{me#pb`{Ey)By%+g&M6sJ*>C6f_P2~U?TIgdHIfQdB;Ws-%SduuT|k~q{wZh z*7IaZogpkoa&lf(+&v8q*3vX=HC=DU=uaCC4OV}H8@ZKYeBbptQ_JE^PmfG&O<#a0 z8bUT&4(o45h{_jh6U8lSvn{+%o~|(_7;4dRdNooUTkw(66lG)d+{B?_S}X+W;cVE1 zsAXlv=39&Gs>mbW*5mFpcy{3w;D^9_or}2?rQ4nH9!dG=K(vp`T-@FN5l4=Hq(@1H`NjFtBRktoctynb4=aogERDLc{01zrv77KG}OYc#){z5|58Bo z5ZjU$t9jm^RVKlms6~KVWW%Y6CDOPkbJ;s#(GJ3d9NTz!Y!!v~vWhxq zQfTjL0-2}(o%o1FF1vF|0Hx-16t*%&UG+(b!mAfS!--KdC#W|G#6*)2fV6l4C1y`} za(ZGSuUzzT1r{6StLCQ>R(2Z%+;@t3z&I(4&7}$Zp=Sit~%^5BcD|P z^0ujf07|6T%g&=BQwH_d$H$ztt@`<>`}g;=*UJDnI*hK5wQt)afyjxIs9N>r3bFws zTO%78#{fXGBv(~TrRm^v>A0Iz=mKvgsjl&#uMtdh6LTURWOOV=a$UcYGUDT_PNs)` z#3Bi6oLyKez8*=DR|zt{Atrm>&BkRL;)}{}{s1E*`;3Im!`meTfh1~T4%s1A+@8tX z3ZL4o^{z#g>aN)}uzMU>7_neN!$7d8(D0XCwD55kg2o|vEjdw0r(-s#8rAkSDtLK2 zDEqcSNcVwHYTxHh`|}AqHaZ#x9J>E3q?62V2zb&xT3t5(Hql0`z`|%5k@Ap66U4ni zLCKRk@oYY-i^J0QL%3COB)B+TR@2u|!iE8qhQ-9NquM7X7BWbbJ877}!QIKPYUP?- zBDeU56195XL);Y$6$?-z8v>D>kh?IZ9tONEcqcD(_l~&LW~b}fD!z8*FJuhb6Q6`{ znBr6;2BjC8w)x18Jl8)C<(FtHkl^Cj8x&krHpZr)E6X7ICyG;L4pdQxj?#<}t=})yH=#qa@Q0;) z;69^mEWe=BvIV=7T;Lk9DotBK7Z)}!q=d-Lmh|Dk4jn~+k1fXxJyRnNAV+YGP9$gD zT7M*s*@7N-8&Be0jveTb&8pVsdfZPYLrUGAQ0>r=hzFf5?dDXUE#Q@WkE-hwv9K_p zq!Am0IFg!Le1&u?gW!A9^?X3nrFRSPwt*>{v!IObqk=+U3Wj$!fWra9jcgj$Y?>_K zkIheLDkvl1#j>1R-yz~`%ep-w9HfQt}9mtHm?L?H1$l6}TK3-Li*S`rTZbV0m z_yAEb3QJl|CmLzLs{Hht+ih4vFx$3R6K}q^CurYJ82LT%s}Lo1#2=BfrEM@GgO$R33`UHe;thtkIU+I` z^>AlTj|t{b$9-Y`1{*jI%pmCKt|*$?>x*8_s#3htYO#rbY*-PTjADXG>@SmFYw4D{ z(dNhfe7U;s_TZbM@n_?w9 zUk>n@9`SFouMZ41f{HT;bWJaQtji#Q6C)u11JG`Fq{FDe@!O(2>Cw!Zk?@ufW`b^! zZPr5G`G(axo);Y7>e|{GABdpi2Clhk1JisI;2$(JgZP;fEu-t|d!8lgT78MaYiMYu z0I?U?ZuF~mtx;)9_12xSsG959B%&Z`rcaO!vx7FvfI2?ltCY4`-idTV=7b)9URpS# z<`?6`d<9rQ4_s&{n`5;6$?n-%-BqVy_ko9i?+fy3h(5fh$Y)hj<2K<>X`o)`0HA1jK1&toui?bwMsYQg;<(kIxitMI2rK+%?Q8i$l~-`g&*5*V`H z`Oa8uruDoA97pQ<)N-DQ7moVSBbNQ0o>Q)(R zH>=Y}f1$sEiR?S-D)ZBmx**|4O>LDvD_X?Ou6t1>Zfh3JSkYdIp%`NgHB1Ja5l(b7 zkq6<0BU|!14wi!T`7bobHGeJqnL1jOAaY_QHBGs=SX+)Z;o36GkWvZqHGMzxUy(XN z2Ou02$mKzZ1>_cXyi|Sj6`g20$g@#&S?^$P(p`U6pu{PT%@B~_rfam+`%UE+KHwXz z<_Cv`g%{Ae+PZiyxn=ar4->&02-jul?oD*Cw2BPfgO?-EnU}Z{VF|A5{TtvQs`b`A zZJ*UK=HpuSU7VsZo#^`5{LcU2-Dvhfgy!;t&$I4&^?PiPvcN5q=B>|_xG|(U~yr}knhJYwzyr>XKhoN#3fj8BI=VVnX)8 z2TEv&%)1CcZy{~M zsxQi7PmAJDA4FR5xk4ME8OEUeo zJXBina*wp2xGJ&Nz{|6ltZGB{|?4XWq6WFf63ZbGBk5jcgT8Cm)VA!oN6++s$BYh9wO{% zRk~7RSP*RQoqqg>tP7OE^?#~z(3056WoT9T4(6IbEwHaS>p25ZZJ!@*N9p}tw;f6!tzC3TSrG%QA^9ONKJ#P zxq;xOrY7cXtYX3w<((l8GWtaIfncsy)M(-Qt9do(mX3$h8#o_DN*9u}5u$^V`K;UP z?%YPlQ>_de=GI?7-{!DUuw4+%&MmApJ%sU-2TA;9Q%;;=iN{PpxJSV#t4_e&6z`5; zPryeD?yX`+MrZcgNo%YRZOF4*c?#|`lqR()VJ5b2L#zEW{q#J-;+p5`+AJfgt*p*c z=y^+iJ0*&u^abXic%H|~Qu=teXep~dN%4`1wImjyKG?tJV+0+sRF8(TcHE!V2)LU) zNK2{4>;7Kftu$N1RJP@XNbj=>aV=3@-FybJXVx@O6(c{@vwGURch!lf6^vp*NozVo zfMRCmooUi|V*cEHtaW`^@}HlUvw`&`x7B7YAYtu^5`VD<+7@l8Tzl-=g+ALcY8Oy? z6$N9_NOt|}<5&8r3kS2g{-S`VHboLl$6fxzDF2L({77D$1a9eIxIP)%FgLy^!l`4q zB;@Y;Ehs7Dg>>EZ)q4&Y9&HEEd?Irl5$!OqE~^4v%bm;c zFQ1~X?`}!rin%l$;;fhti1#YbYKhyiKb)G^Skr(0=KXeK>}N6mo#2uczbT8_r6lSk zUbSaJU)J&843;&ze~cS(R;8DoK{f$I?>A*4W`s}fypdSmghRW2nO z-Ug=2+*WR;CjiBjW=2*x)+bwgsNi?Jeg`>J6S9ZaW7Jun4{-T@$J z$hCD)6mdKER!F03x+^O1)i_sLYFIi~^A?%V!jI_>*2l=4dI~B+Z#eXb-p}e5ZBKM3 zG`dCHvSD?tx$~=?lh{#eB=W|~=%}?M=54c>S*X5J&W0FwwjUC$<0ZwjfwUER+E=u1 zxgE`6%J(?DKDbixxSTh*#gb>6GJdaW!Xj9E(E0=_4vYwTp8sRnw3#cMGU(=ZAUI;b z{84dM;$<|2cL?1d(=8I&ChmUTLtI9CA}sh)^^40#5w&>>D2SH}G+%Yl%ZY>hQI+tM zlbyB_SrT#auT!3mF%?`twkOYL){v6=5BsW4Cv$__7~-xOCr-<(x-I6en7v0kKVCSt z_V}$0$hvbKrfc)khC;xMJ!6a7HH~GP_;+onjO%nyz4uy(+zsoZBItB9Wh-StY2qbs z73%!%vD4u-TPWkn3anP92G`EfJMsV)-TliQOf9Q`*wR>+HkC)o8AaI9VB`5R877Uv z=ql9Ln*Wj9g3$$S*Q~$edUsx{I5@qoAr3p*(RJ)}m8!sr*;e&2@)Iimo!rwo4V^Lf zILYnSbFDbB6b>shx=;$SPsMEWXBSs+g~48=E|Z)p#+#~2n(N`w=*9iS-x2&R{ZL6P zgUYg#SxPU{;LzESN@siJqnZefQ&?KkVVQaGfrC5&U70ca%Blpsx&GMePYAC=8Jd;9 z`+>S1a6J|fVko|P{{!@W)?tVk<)6tDecb&fE&k}1LoXOhnl_@MqH==b>4hq+sQrdP z*#G&eWAuebSl^O_j+h4O?itLFnFAdiFsLQU%y>~#E!w}Ez?|I^B1hWm%y}J^f$4eH zpH{9&aRW^xL*|E7f8n|MAswX$p%rXSQ$VI^CP!uP_JQ0Y-@hCXpH01B1BHVC_;z6J z>aj8iz;7RhJd=KlwRpB>7DjNPg*p_Lmp?Zt0~qJv7X~2L0|D{-5fI5Uk_}M|KdbM~ z3qm^GeLw~_syCgqI!=9AHH7f+>9tSv&#^OKNmTd5%*`JhqtP>Uc&tn3x&VB-KeXnD zGJ9X-3KG8xfF2#E&3!b9+yY`ZoJPu41k{|)M95qesN|BTXxTXnMjOgqnubv2de z5dV$%7E&3vzo2g%&JZnZ`43#XEvUYpLsN5cwG3usR%lvfb~1TGuuT*B?FRp=)K{X9 zj?rW=BI%I{YENvxOXC{K_%6JF!P%Jo8o#uFhQZ+}v*=xo)SbY~#b7)V0B&9_+o&*_ z$PsC6ZJo-Oe!wAW-hd)rzCYwlmRP+0liYl87G{Q4A9V}uhtZ)V=a!gKnKl-qONQvS z1vP}n1@$!!jU8&Fo4dIG$Sz3VZG7Ozd;>wfXI~~*?b5UJ=)3vk^-jhJ2!}sH7TIWB zI9zJFJv*!N-o+w9d9nWW%(bB^05R+n(}{3RREK;fwcg{Z_eNHJ2Z{f`apa(EkCBW8 zk{KyQZ+z)ZeftXDvip26Q->S~<2nkB>?aLzIxm?TMhE^84-NM|hCUVetVMvKD_w^Y&L7@R6%Avw{*jBt$7Tq5exFY%d2$TXC|B`C=Y)2jeIZf5tYY_eEP3s%$ zh&O$U1M8rV%Kqo~+w`hIX-;~szBf7R+FUd>dk=b*Y9O1NBX!VRrG8&j@S5RqCjVif zaQh00CYK`ok1m-NS9zvRxibB03z&zFoPFvz%fw7=10uzg$Fa#gKW8V2bYn93ukjS+ zaPVBo1D0sd*q1LAokNK8Ergm@=A@#p0F3i-@+%&7eXIlF&nT<^pmboXDk-ajO_}XR zNnwbYH{{MzJ{~ue4}#hUWW0*le;gKYnzkZRh;U;Oz(}NsI!(~MM^)WiFid%?XY>116g2eZ9(%O z|2cJb$Xqkh3BvQ?X-SgVs1$?mG-tZW&BcLMRKOh?OV=I!$e-UZ6XV)H>ygkKBll{}! z$Z;Ss$v~v8h>oeF=LILw>0bK*bj}b zeB!P@mv^?$3!<4Pl}3DweIkCwvD!H(BoBW@SAM#rO-_5QOacOx|1vK9+Q)!eHX4dw zETKxk_@R)AE0w7>;Er__+hF>Ni?K+w!r9u=7N0?n)%bm_bCixs?8nGA+%J|Y;=I9M z4hk#f%mLC0M)EBjX?AwFES|T=qFeIdtAq-1umEdk@8?Ju0PL#wxP0!PRkqCBHjuuL z_-&p`Ls1*$koyC z_yEy-l&^&3r{?p;!uj$c#99MP1vPLmVub^}eSA1tYOo*wCGdAe&5ML#d7f@32iLsj zeOXA@3vYC)Xy{F+)}%O?AoRFIr+Eb}+#JAp#Kv}1PaokqIp-?!Qxt@g)IdZNgk?JR2x9Qe4> zJPoVF$F5}u$)>Ah;;c^bj}_STLq{-SgR6?ahy;fp7bd)D>5k9m+^+_P>12K4ukkZY zQ`ynJN8-DZYM1a$d^i|sR&lct=SK)ot8}NtA{q4)m~h#evBs`;b$LP z{Bd%fUKLnny4#m_rA0_b)DyyvC3Rr6zg@gI2;L4qW}l?qd6`Vk^VH%d9HY^eL8~4Z z(eKusr_ocW`*3+w>G(3Iw|ulvdk)?FVK6-rBlHFrY;MYHvVPt@DUnOn!R>`JzS691 zet1G+wEC^|N)u7@qr1Ve^~rIXDVy~=i{8-jg+MkZ z%$RSd>j)3tfTA)+NigQluB1qeF}Om#&2{8{#QZjB=hC&bUhq4H2x@9Re|LY?@GL>^ zpPWbEAEuoN1Zk49Rog1>bOVD!?5g?NT&P6|)t~x?x829Yb|AeSsGzT&?K^#~dTGd+ zm1YBP`L~nAWcaDR50P&Qn>nYneqY(xUq;M9I&I6+e%{o%zhC(IdD5-3c^jc1%F4Q+ zWb#i!lAgr)vMQNBO6Lx56DHl_(vZZCXQiRH6yfrUjH*1O__DB7Mgsq^yn06}Yk5&8 zdu!xwxlf;E@rBKDFq|>tN+*L4HC5A`0?5$auye2av!m4>NBvaxR>56$1MUW%)-Mg%`iJ{!* z7PI|L@}bU(h_K_XE=&fhJw8R@BBNYO|SmRz}QXI&lx&QS<7X_T%`W z#rrU%@THq?!pY9YM%N2*Gp+;k&H`XUnlrF+>~`UOsW$hp0K{yt{Ls35#m)X2a_CLo z_)YjnB3X{vCU#q^M`P|`HZyXZ?6B#*$H+7~+=rM}9%x_A)eXhis;j-zSgaF7qdpjG z8u^<~w$q=^;NK-Td`b2fgp5|h{(PgUS)PYflwTME0&>O(=;b`nZ!<{>=rkFMBZjBm z6lB4xcQRW}D9}Gcdcwf^S1mkOoHH0Da$!|KrljTC+y0PEQCWO;w~hL9d*fS2`TJJ; z7%Ep*Wf+k~AI9H95GK>XM^xGx(@y!?t9|Z+Taju}j!>)9HvBuG)jc@STHEi?29X)* zUMet#v?AHOZAIK$w9LkkqH>zDlchmIZbClvYS^=h+Q|j9u#E4X!iGC zn};e0K1g)R;1AnmF*VkP;EPbR9I?6iOdZC(*M*!Nrs!`$7SscTsMKoztcMJ1IzzV6 zB2^@@?I+UazzX91^XM^kPrEMc(PY9Cr4MRRrE#jeWJceoQi97QX z@>H=~Qw4;k&UpL_IvHa12Uul~$NcPEpYNp}t44amO)Sg%V`i>Tc1ZCE@aW0?ySBeg z>b`6F_GQh4lS9*dLy4x4Fd8Iu{M|k-7QUu5V|dy?+*ZOua67~p)8>OkS^UN)m=^R( zf7I3%Y>dkR9PN#(q;=Z{G(qY@voqH!_zSI26p0pB^Cn-hs484{t`dbAtj&vhvIh0L zoYI;jG*`{2{2ce)liqDfI6e5OoKJx*)b}evR6O?V>YW(IJ%R7q294Dx znu3Gh5((9Z;vT?j!N9GBidZ>3QHF(W?xxq8{rL9uzjq8Ks1KHH(h)+nz7qMeaJ$OgBcJn(9vGsy%mg z9|#Sol(Z(=ay@RE^tuAA%Y%L!Y%CjgH%9c)Jr-ko?9KfioF_~gB2o|?+{!IrN`1F* ziY^;j7lS1(J!C+soCS$2y7l`JmYL~VxR~czUA`|$HM=vbY2@-H|F$uI0+*~f* z6$F7#3wm?GBB*vAjT;ZX1D;NXyIVLU3&>B`N!~xm!j*_eqy|Yv?-9+MY+369&+R$! z17pxSD^Hp{jNjY2==h}QW@W$cWU__6x_(3F3sdU{vA?#&a+yr$;LZOBA18S@hGQ z*i=%o;UKKisJ!e78DG0bxM$dXT+X_wok)8ugY(a$oeUgph2GJ4v1i=riknWCOqaV3 zkF8VBk`!u;GIP!K(RYz=;sOfLl(+(Twa}fkn*G_{MtoKe_xHxjYQaL%{Q+pG}e-LeH@k~(<4UR7y*u_)h(qw z-VY&od9+`(_(7czO05v1s&vYd>jxJjk*5%-m#a{ubYHP5uzPp8yY({Sm4`+Zq*>=_ zfOuWofG^{y8KyD?$*zuN%I!`Tf+d;^@0QA$Qt~5@4-As6R_+_s9<9%wGHNbwRx!Vg z*H(?B;88MbC?24ApILg=(@52JtdQYiCO{Ea!AY~d&qB!Gb_Y#KL4L`cH$O4j4ZFnP zWEt|_+7^T2aRg{#EP9-MxmxU!N{Y+UAbrXsjtyn1K<#W&eN+?KHkDO&AKl+ZPMu%B1jzj4M*G z$0_CsH@sU6+)%~#mS>ghXFcNiA+ozJ0*B0woXq9zITh!v#4bdzM&awsIgO^#Vp{EjFMx_az8fBLN7LS5l6-th9dW69p< z(^JZi_;k}g+e9f;W!uuW2&U zGz}R)MTuQhSB1W{;8UHNo>zBznZZkJ;laBQx0TV_GSAO$$v$dZZtn@hez?1Hb#d`9 zhp`yFRzJZDdmll7Kle>C*2VoU6J_&;grI@6xOg(&Zs?~aBx<>~0^IXib$g@mQuBtC zTjs{HpUA(k?##OJ$3&z<^-rM>CW741I+>I<9&tXK0LwgcjVc(_5{j3)j9Krg@7&@F zS}a5usy*XhpN5?IejsG7AQ`2)aTXS{O)JO9tH@__iW-qSjCVD?*45qaOv09388qa> z*Za1z#Em{>6`i29@I-6wSL;^p98cy?>563au)F4%CVAzqRlAKUxRu^`J%wBCwEMK9 zzvP%FZVV=bWj$^zM7`Z48|G+)klSf~;+25goBkT>^1M~{RQgra~BI8x5#>7R*I~OJ0f4RP2?gs5+j}_tU+rCL`hssS2d)d^yUt| z=#>}NH=p(04y@K@H{C6!UzJRJ?XN(;Vv8v@JxJ*WLt5Q7x&;`s4IOZuPmi@y@$3Xwzr4HruKjp$;id zRNLGzm1mGY6VB5U`74-n>TNbfP_svBXQa!PXx7~5x;g~6XgPufa+J7aR@1?@+k2T! zilGe$Mw$Av!#Jg~=m1@<)H%jcMfGU6IG6|qr|YvLJ9*m-jj|;mag${?8PRdkz z{!~is9(mY!e-x>=U@^Tynt5h3IwXw6v^8bZ=}lI#5Qo^k05z_>k;++3EQx` zNYh2Dva})IqV=2Y)DIkg!c$tk3Xgey3X67?yMj%(dtqS9mamhZja8&A2p0bQOl?i_ zybLt;M_J&o8Tk3Q-6nfl#gV1JYKOzps~w1E}0=?^sV0`*;05jOVbTE{+MuGMT?a>?3US%O5@bl--4(E{f zAdr_tNJxkWUsjIUeOzv6=wK4U3?CbX$+AE!RNA6OblXiSOENpD$ zVei_s8HtpFC_)x1wfKukXfq>SDNv;3|0!uH{@1p#D6m{bvZguSNu z9scjtuQM+B+B+}uvjTwe_STOu^6wKsh`G+!*B|~pync=L{okWP>}U`G69(GX7N563 z1&P1y0EV>vp97E_{pVMJn-XX~Zm-}N{yjO3@XZIn1pw`T_bb8WZ+zSgCaZda=+j5A z;Saf@Pld$gE5W+EGt5S_j-UVOgd7Q=P9#cc)VcIcbU70A+{V*mB-X)l{?prb@7MWX zg=jfF)-AMy&MKhli!gs5I=d8j{h@ZNAmvVLU~GZz?;gB*c|J z(S?=?kf&D^`Yt+L6<-72^tHE_nGKD{V%BaR*Ydq(2nBs^2g;d(BNO&` zbdF=SMC zM=6VL1othC+YfeILmyP>;BW!-_vhDePc2sq5geaqy$@kk8zwvJ%J5f9Z%j|EE*Hgh z)+T3;S%>_V zE>KG*BN7foQFyeOs>7?0fJI&5!O>`5?o1X#3)PPt38&KjaskayW%ek$-jfpyhR&~( zCFvlj2@)e&v~rt>NmBG&&aTXkIiBEuzr3j6V~-1iqw?~!*lf{Fs;h)DlPf4-m;FNz zKHiwXPiI$(Gz^#)WpTb&BTkrP!WV?iR>pg9c*(1kB$4J+r$w+jv9#I_;}5I0zih~t zxFF0bsAZYvBjjZ_Ism1*=1^dbVvQ(5XI`MwKzyFUMuHcdi?gkpVQZU_h%%(p?s1*JotFx{jFg4P?VJg$e*TH-e~8SHmqNC%b^C$2 z4CxKpT*_E~=+?w#O6hD>8lpeLVxwL#h9!0%pYr?Z@LBB+@hjDnsgDt+Yn>mhTB3nK zkHPhIyv8?`NqRl5A+qJ^OT1-=`$J)oM>s!7Zdnr;dS3sOqr}1xpZBJg0qO<6$dT`; zI1;>>EIxiia2b7XH%5@!&=zK%feWOHS_Jy`(D6pH-HV zm%mfpEK1}s5D1e=i{-WmTbPoLK@kjQx$exR6G9?mPqp3NAl1}i9jc_!ya!__7T15) z{X*TRmwU=6HUAP35n)~;4g&EL{rq~Seb9kn0?dw&9Y-U9QRH4Q`9&Wrw1*rEhrJQ*@fT;&4R%-$>Y@DXbO^9Dw+oMR1J!Km(78E+`Re zWu3Y-G`l=M-vyrCieNvilLmq0;QwK>Ut!{nrt(Gc$$7cBN}zZ5&z@dwq{n}@#?Q6e zQg}4ahmt)xSKc%pU9@>l}4_l}FrPR8c0qLSx>(=@HK zR1fUrWGL+H__wyyG;CluBItnq%kjQ)O5GF&^I1Lyky(R4bW{%IpA_P^p?aaLwe#i z)8T7H9jnQ20vDs%od-&_t!!UGkVs?zcz#=@?Wje zb>1XNi%Y-1G3zc%20k=rF$lp-CtIW|&+tk&QDZsfs-n{XcviwXU;8!E$(zSz-~gD= z`?00>P)%h|!ta51Hk-Oj?>_>nMW^R~Hfm5Su;Uk!0rhtr2|C+0j4gp7284sL_izl; zu`6B~hg|;NJ^ynf{HsM9T!+v5M|WY5?jQs>5)9pD=N8T_R@j?1{e2R&))(~lquJO2 zPIU|&5H0{rl=i=i133KuP^S31KLQPEhKh>nhzA;~%Jb&`{xMMRW1?Z60#z47p0}iU zo`SyqW+NgXyTJZ4%TW3%Ft}n7^o6YbUumjz(F72=G4g@rA%ENdNzgf?r3@`TL;-#& zD9{=n_|_|;SAo_7Qx1B1dfH;Z&ct8X*(sRX%Lpu=N1L$5xM`g4iduu`$r%sa>o_vKS%%1gP+x5ltFmPeA4`@QyMvxKBCfuWnWZW z8z?smT7CO3-?>f(_c{5q9WIK(sCvBxK~IJL5gO%aZM!?XLtj5M_0Rm3yUqyy6$}d_ z8BA8yGoD*$IsH+@H?~DQc`T%)O0}MhGbE!EG4<>Pou!W;odfedwaQWIV!!=!ISh&_ z4$pp54L?Yn&>g9XoxcHnh51*YEbcYZn)9B`_QuvuMKQhF(Ib()=*#rD;X2sCrB6AU zy!1{1KZqeO#m{<>)vLBjuP>4-I0l#z(|=yu z-A%}UHt75v4>J^HUqWM0x>yTfXp|2$X)~vASS93B?xki zY4M=bGCNqEVtl^Z$0qY<5Koc~Ou3(R)C69PB3SPTBqIV=5VH8=(HeixPfbbL7efWb zNsd57JSAN2?@UwGq^8D3x6P8Mf@&%npH=MQnQCN$GRq*eoaZL>wfFN=*7y%}WqweA z5YYeA&(F=(r=?bHaae%lK@=Za`~^KDZVYYyBFS%O59eTD!C}$OL2M?%Wb9MjYs%Fz3$Ro37U zj<%P~DWAJWL!nk4ZbIvkH$gXi*ICm_MC=Z;t-k~}gkUYF8+5})4zUOv)#pYe#MAFK z?rZ%6z4-E(ghwmIRF{7)&i0>kmD(tmF7^WJ2?wB1TD8Q4Z$QwJt_%`5;O{;lOe`-T z@G5EONp`ng>ap@o#o{QP^Q|t-7)~06RPs_2;D_%LUGa0j{o@)V1e=X_r3Ef9>F~{_ z-7$#oFHB{1fcopfI1WL=Gh7WSIBvlkE170jaiEdxg82QRf+2z~Wi*5+;9VI2< z8w5ZnLJJWV%#I#<3SFI7R#JLyacTXOM(DK>-d`kqnR4p8iQdJsrSOh!iLGl~sI`peO0hP*A5xr%Z>;LE2-U;$ri zqL^<7T)?u2X0N9KjL+*UAmnMTb*chO!WEhi+(wAwbF;cQQulCqIWVuk(x1c;*&#q5*iN$#{DwK`rx zTK102!04dITwlRYvyOjt6~{o$*;ikmzPz9{1>Z5P?X3D@h8Rn3dMbK@kb z<*eK#$F97Fk;8jqhPuyuq%1Pu4(D9w%pi^oApQo^l0-|+E9bV+XH9QirP)fL@6$hD zT!3&2qRA_~!~8OD9j~o~-xl+FON7jg>iypO8LyEw;$K;B>^;BrDfBHj|aw~!V+F}qnMFJsKUmxAB z`y#bBacALzP&ALfeA*YC5GPG1=a;#aez82Gq&~c&{_hPY5gtc$b4gylUFI_SfsiP# z1cm-7rfSAnloog-Z;IFn>&YdBh#ytNr((Ghn7CYAW(Yrs(ci{v2$`3>dmJRmF=XxE z)0j)8R0ZRbbKkEd^ueJCV$f(pIq)uUG@7=5p;}*64>xjAsem!42R+d7lIXUIan;q> zpjzB&@;hmTv!;R)1O0=&asiUCF}b{Lgee{48f+&{T&Mi1Y8@qOXU@ctb!F}(mPkax z>3D>gap`@O2kHyS#X~59bIr3+gngKQ{J%|Qt+t8}P^NKlCnD~mE5xD`UUHFTQL+LB zISTKy`$&sWZ%LTdl@Y+AkJE*F21K&U-3tn;Kgs=3Y-KzQEF@w|M_*v&Xc2h2ZY0={PP zBo*lvVT^@}#@2en-j=Qh8J>b_+*{N2bCW&_7%a}fj5)`vPk82&GqXxrsXLw82a}av*SgD?TPs{i|a|a;n<|09VtHD zc6O7(Xw)nnDPZ$z)+>oY@IEyjD=03{jtGh^9JIACy^2|7Umh@oL*-jw^c$f>6(;I)=82q%HFt>Q&_CVl6fyjQd^Rc6&z05FElgixMg_>#&>7wa_RRp%=u1M z2N~kIL~2!;__X+5-94*QcS+Q|W=l_9ZmxNn1XH|xY+Hw!wS?G4iB8rmnitUXpvgPk zu&Pd(t`QE2Hc|CuY1i#Yzze}%8k~kx_?t8DoUL?_cX_q!UR@*fnLlA>_Bt z$qK%ySmTe!%Qh&Z%j6POHYRHSpV1K^FcCR}R&_shJ z0v4dz*?%|W^X`eqaU?(m_Q&H6IA=L$0}M=8|34dOjFR1z59G3Q4u@sYC8nGEvlD&t z%2*0j%wiYkh+3DpN@B~esCC>m1@4}aE4ye#z<^u9&KJ_)`OgpG6c zoY>=hPguZ&%a-Xyu-N#K8lx3)fzkEoKKgZ?RuMR5f%YNtBQs08NYY>k)c7P_=v+ZC znAGgFxt4tevt;4^?ak4K6^$;2x9wdg$$6F(vr)A*Ey3kOrE!WvYmDTgj92E)RY{n! zZ}pdg3JZZB#Ff_oe9*vKSF&|)dBkx_b(Q*iNzwYnUEjPid3%lC&~wJ|9R?9Ro0m}F z_2u}(Q6^pFGP~(xAfBzDi2WGdtLL&~mb{{<8)-r?k(62kWt@x0c`S3KAWU`QvfDcs z%`Zu$?=928hYdii?ra%n)}Iu1dLA0e!;<97-QT3~W+f-BTuLW1%9qF@TQ-PGT(=)A z%nJmVgl>lyZB)M<)?dRrC=|f1r`{pq@OhZ{_~A^r@@&!|C?CTsh_WXy8QKDBr!rK+9M#V^?Dn>o0ra$l)~4 zwc0InH~3#VXAX8ne-#DL!8=iaUo)!52(WJ(hH%iI+^k|VsY&$AEJvm}o8PVk3lbYM zn{4h>A`M~?^_+_pAgX(@gYA4NjM-hf7EmM2v;<-AW^N(6vnvd^bi9EH85^2>*)hO` zi@#ElYh@Y;Yh%}uNw}c7w_f7w5Kkc%j`h$xiM~@^!C!W$dKi6mpVvbfadQc#wflcSIRIbX}QGfFAvH+jMLq&o%z(fAH5|spnb3%9KBEvk{ z9A{~>cqGwRyRp$FX3U2>IdwePz;{))+?h-%w(8+6<*KUI5sSuvM!$saQmdF9nw8mv zL7#_LgM1~IpBq!L39n9k$l)^K;q54~)nt2g&?LeMHxO=onfVjdSjOMTeq;fOK;8|> z=6v%F_sMP*JW|{73P-#k#75h;=P7gU9JN5-^-4iN!y%owh%r(KsIUUK(g5Z4)R;`q#u_Z$;@cfs*! zLg|>h#X9fw6TeI%TDksr@4l)TJG>f#>FtU`Ze6i=LEG2#l0FS8<&j?uEATkDS=V^n z(=yXXQS4p0!TFDx9dPgau0u?>XL1^L_blHdV({Zw8dh})Z3455%24Vjupd%dQ+)VG z;G|$@1xuMy{E&+aFR3n4!Oo#_JyRSzFVBEETuRy;>K3~CO33|Y?*rOjBK()fvj^FWtD`!bJq4eh*El1->)uU2;jx69;Rj&5s{ zTiq#IcGc;FNvuD)x?c7tCNz!&(P^>F$=8NHe1}8ury-K+j379)Qt}AE!VFPUsEcr6 zT8dik1iYlMr*X%93^YZyvacSmGJAwk4cY(;V-#TE&`>!cRwJBPXk5TYOS(RAWZV3u zow%0R-6j$JT(VmAN=b{ zSjVl~ZxI{ml$*fT?3_unAmS(2_QsCM-oaV_&<1p_9xaDX^PV*BdPkZ~vFqE*3vRKP z`M!y5aP+J}`%$YC=A=OA?{FRx1MOt=xmt5Dk@+XaK$+wB;rsjT1;HG&WF-nq z9$?5wJQex&RVrs|Dk=&bcC$z# zX*8DJa%A?ULcvMH!CB*6}`MLa>$<_fH2Yf45$tK zs3#*k_U-widH&c?XxLckJr@7AZTN_EeGPqWFmbMfRWA+rHwxf)dnF{I< z^W_awNsYezfn{G*8*U|3X6t!En=UoE5SyY^eDGy!X?E<_^rB3%4egJKW9#y4H?1^X zBSaSlE7`4%5N+9F^CAXL(}o_JvVB%3hr>bi_&ZuRN7hz%%IDrkD@(A+09LLc218AG z5?EV3j7qt}s*Ke-W7I&syw)4sYv?!Qzx#1~>SiMmJ8JgI%K2-MZhB@aMV=CGO9m!v zwxhH(R7vS1qi6Zixme>yvzlj|!DgY5n`D-cj~l!@!(XdpD&STZoE?uJ7=D@~7DwMo z&nLf``PYK#lZ7klv)=cWiu0w;v9Gx`Qw3M*J`Ku~*ZaJQjpX>;X+qdnzgrqYU{Pxr zs(`+M&g7qEAgW8#&U}2gt9#h%?8vOR8!Bmn`8)sk9gq*!*hq3D^$0IMf2^6nJMzMw zAsz@?MA-g-|JYkP&oHTT6Bn!1RX*fgfwMT!A*Ta9k%rfwwp&Xt%Ha2Q&XHjK2&G?a zxdD$xioyq+uH=40*|Qd`G_s!0ji4gw;C+no^iVf}173Qfx*I4b6Q3LiU`j zWvLMXmSQwr#`7?`*!=4^n>4Ka6+Sb#O;gB_oZ{S4F!bv%8nOAj_wmY%J{LFMc@Jlb zpgM08n_tqA4uAW>l(c3wIiw!Qu?AR3DMV9}H9Z?J9)&xFd|$?J)e>nbDH#|TpmgW$ zvZ5HxEH+1#ESPNx6fb8g#T)RdAURo5a`KBikI>@%T_3zpC&*-SLOf zuRzaTZ(nx+o_mm}&#ScuJ$q|{ru5$NdxFE;SjTe|w=`o&bpr~j|2N%e=mU?5tF{c_?!lqbMMUkW^^R@$0PZb z6|hPDa~bsizqcM5QrQzgEUx`tUVlamSl*%C7PLWst?B#(r0sI<9|f*@+90EuY@UzP zFbM2~ZE12>gna3SBDU-|beX*F2OG>2lV+0QIZ-3&H@9&wGSBi9b4$?#m(O$e47-Ob z$gxT8#>4z5>Q9i0`dHuh+~hP3XNh`YI`|KsqrD{q!|eVV6v;Js)cF>TG0LWwO0&eK zqEZGM51wJ-%~fWjIT=wGZGEl1x!RiSddJ7ZrwywrgtXKy1ToR7H}GkgHTICDGWJRD zT%EXH0$m1s&2P3N5|Ww6SU%s@^F{mBtOea;Q~$ns2Qxejx>{fj-_ae~}RPzO*h+#PU6c=>@f)q=Qp6u@%_? zL$Q>oL@KnY^cM#!S(5V8C*>JXTtZBqiG@C?E@wsIGJ_fVLyOx1dVIQVnpf|O#EkMf zo6RBfF9<{h?@LYa$Pg9-?kS=CMX%0s_#tUAZV{h`N_3j$i7~0pndmROu=kY0Zg~nJIQd6TUGpf19?EQsp{wnEx|X zc5K#go58#dLzb-%;yAoX4|8=5xz?( z|Ew-%*al>*X_WPhfG^ow?_NAl-o8o029{{!&+?^gG13EXyMF{9+e=Ne_vKOvb{0bE z!y{um582+)0QW=+S$t`}bkm7;DrO8^L0s_0P;8F&Q&x7je0BE^KO3xKc@Qs$;l1svs!VvXN!VVT@uU{1Z4lEfTdl? zP&0R%k$a>fHTk24uo;oq$RLmj-f{$kN-$K>cPyjjf+a8bOlgGI-N9OqILX234)V65 z==ZWRWv9{Vaw|0Ug5Lg~C6UBmoC|E{9y)fFEkpPYl&3yMhtGwVC?{-?fX8-=Y%|8& z2z~9n7M^)?fkEUy(%kes#*ha0JAINafqqBc!O;9JGj*Xy<3gMiKA!Y`?oqLOk&d5* zhcO6%OYd5rT^!996g7zOY^v~jnd&QC>$S4nuRXqr<-2^(mQ!NslJ^PuXsz%@&pb~{ ztu}k`HiU!rXe(wY3Jt#AtXCR0UiyvEindlW-+*&Rw9!QaJ9}wFoml~zD=%rb4<)+; z@)7p;3c<>r(@*JqP7l(V^=i>^y2B_EMf1rp%a$w18q^$nyuUHcrmw%-Ek+5GM{i2Cfg=)5b+MX>d|NT_JC&+! z6)pTT+um@`dsH}1G#JkUnHIOk13+ID2@&X%)$&UG5*DviDbBm9*4Qkk?e0!wN0vw>Kh{@jy(#dN8;hMPIoa7gmO4B| zU3Ncl${W`3b%vGPEK80WgGZ667Z1XPOV^m}^4nmZ_UVc!#FX2HJmk;Qu@>ohAfJ65Q%~q;*!J1z?DGrx|}qq9f7~#g^{w0{@o&*?=RocZBQK z%(zGpwG*pbhuii@#vxEmXBHx;BQrJZTwIyZ>?A;4tP1p^Ihsm|!WD-JVTZ-fF>< zNtxw3xwFFlO&N#(A5N-V=yp#Dq-+8CVq$VO$FHO`ls25BanmI5a@arJC5$Z6{@ zzOs_-KBf2(R|k_J<+`VF)6Ix2e_QbF(VVNA)g*nyoBZ(}=9m2UqsKFakN<rDtr_9$8AB1-QXmLA`rfC zY{B@YfbxqYkL}A@eccqTI>a?c677SZ@7m+R=hPh*|L90UyoyIH?$N>un7+sNvmJ+BkYJJB_^E?!C^@>0236sCdN(5K}@{y7~H+ zo!MyU?^;NzIc`MPN>iHU8m}<-SzjYcw=z2&T=}a?#J_LaEoK5TmmOx_(R&wnJeLW( zf%#~NDz9(Vz~q*yQ+8)pgO${aEbH^_!q3e=;i(ckL3Sz#gZbIS<_aW~j>s0^=|O^- z`MtJJ5Fal;tyAkQ)*SU2Srnymaywz9^xLNJ<;39EeO_DNmyux1dpQ>kGc_}Ie>&TG z75!6mX^N<+?y$|2wauSf1Uc%AeJQs$;HJ&}0H#_BwzPhdom5ls_YA`P{J99(cD+*)kPG5q8X-vd!l{Ln!5`uoUX8mGG`6K(YFm zjqjtQSv~Z6Wnq8J2a{94d6MiacI>ADr`lum8C1f?F_EMwB74Zy!!UODTc|X*ir-;L z>K$k6+>o__o>KQx?;R&GXnf2eK*-UIAc@FRpj7O!;K{Z|tc85cBWY3_7+y}G2sx6N!>eKX58)P$utwut3K3{Pd zF0faMlJ-G*zhrtEKHe+GNIB2kdJYWI>8=8cH_9i^HcT5X5)i#pEYRed^Nw(DD%MPg@346b4xV$ThLs8zo)WP*gVASsfhul0aY(@nDTi5NgTPhkeeGCX@<&> zbWNB0dG1R)+z9YD{oMGNXw9smpDN=g*88^oNn^cF?N`De?*ODzHajI+S7>Caqn3u> zv?Lwx z&fR2dE-sN<)0y6L(9m~edgO+4!RZzeUp*e09MQwZYO`#{(X~P@b4&85Tc#ND%-z*I zMgL21vGQkJf9s_=?Wj_h;KHtEw|g3eHX}6x67Hb&0*#xSIb1&UFf273&I}F`M59Y$ z(coC+f(%JWx5P1>o_7&nSfGgs0Z0U0tiL*^j*$q#C#!ALgj~>%{=l6Er4sPiQ4UK` zG~3@W%IV4aUQL`tD5(_1Mrd}QCDt#>$jhO+flW~~TU-OyotsovkUq(Bht+}^qfG_- zxh$cTYm4P^4bn%Gy=AyMi!NNs*|epJMwD#)B85Cj~iq0sRwz?SI4wK@uv-+^Q*ox)(#6HMQr;_b)rSbfMDp1R2+bbZG*z-`_lc zD%vf5sO8m~p)w^YwmSFpD?cSNZv|7Xr6i<6cqE$EX=rG3YZFVw5wCD5atOA>7wf)= zE1W1)Ww1R6EAas&2IZ)oDCg%nnITn+v(bn3>rY$p>5a`ic-?YzMh!)_Z*Pwc0*%^O_i^#p9sebr3k1DbmS9JVxGByUzL9R{dWF2)kd(;ZI z34JZeezjMoQ}A~EV9x(aNUzHTPf{d7B!puywsuy&NYsQ!FSqZ9@kUA0`$itAYsnuKq%<%h> z?as}NVji!)Tm-{PUgi|%_9$^3fX<=i-l__v;Nn?oaWyqR%!o98g3cTh$Q14yY<<*KCFpN+ zXv<@UJ5L|ur2cv;r>ssxwQ0dzO7B5}tGs>-vwoniRB`-$4Nn;6#W}dU+9%6rj3cXd z{Ifxa5)-UD-QBd8gW!@wR+LP>WmhSbr%F0E-PzX)N^!k1Gszch<8S_E4a_OOeodvr z_KZyNbPGl<$;|xVK~M87kWK`d+~8~J@4RAd9j4)I-Nj~(srMMsataUcqo%ec=o4cR_BTU;P zYj5*g7rD>iM(n2XR&eD+CH;zAlk~-nO{-8UF0gnE&7Zwa@H>O z8AB<2=bl>pi!YAC$0mdiB!lBk#x}AXx>>Lv6ay=WG^f15<`mfP@lC3Dp~r!*Efe0R zHhkw}VPGgCwt^;0@E4`FyiJy$)cQQ0D2jGwWp!&fP(2@&JDgHuxl=%}b!5?vPu()8 zP-|(P#W;#(BG%S_yIx{uRKf@1$UFwA!7tcL*{^AbrKO`e%dO3`J19HqDvpCB7cH!2 z<*zrF&xvWt*NgM&I+ZMzDVXAfP|i`ZvGrR|48K=rR26+)2Ns!?lIj%wZjO7J(V4+T zF++KSK!i+0c8g2|3wT02ZPe&_nW0i!`BwZ?z6otNL&RolvmHm!@xHO7+H@(MG6^HO z&loYfj<)=JC^FBoUQ@`@={_rEQ&5G;=k|@J(7CE?AG`lGQ zR3RsaE$Q*2kH{zg^{!F2%k$E_;BE7K8^@dib)?b~K6L~q!pWvb0>qFlwihHH+s8$u zl!uyMp_ysg(rVm={T#m=VL~8+DhnZ5+n2_yr#_g1r%9Lh9LHS^ZE8!j-!|E$i4jko zaZ8R?=dagJ!S;d|GCf$pRNJ^Xp)-wCrsnvYPCQ7DLXMIwZ14X?Z`{)u>opT>t5?`-U>l zE*n>Dx!vpw25iDsN3y?5P%PGLW+^}kT!8}Om}+ViZ~f69_!_+tK2q-Mi%=W#E~)Gl zXw`<_`6ya0WqF;o8WI_6t4^_6Zf?v7AF9fz_H6s=)%Vo*wIsHeAp$>97 z3iIs?3+mMVn1Tgv7yGGaR|z6jaz&;@ymhu!9%t2ljG56TOO~Z%3-@O-+(Hb=` zvR|pQ<$n%NtF(52CH|SRjE-x_(Kl)>XgX-LQ0A`O+kdufTzRYLot6fy*S*#wnj*!< z9D4Kor|{R6hTbxrq3`vPg$wY#U(>=;$ZH9_JoNW1ZbwqwXmMY0?irVv6^5B^y7kBl zMPSb8BwfS;Rp61If_{Uubr7(%Aaa?0-WrHG^>dSfRc=6E;oAjQNI8WM=fxgQJbZLn zz;h>mPo2|Ze29IYu>jH=U19ntaCyYa6jX}#>^fmSvk6xi7ZB(_7k+_-qjfOTdvrAG zd2rNz)bkwY4W6(t%NI;stQ>tah>6KE2F=q-x_}>}x{nwS-yv5+pemDZWgrt{ziMtr z%bf{c3KGM?+)GZpOjb{IiHK!Nz?{YFJf7Ya*(5MCCrdcK`&Oo+YXYep=c=_tE)cMd&#J)gmRL_qU3OC# z1Iw1}cQ12kyRQC>nXUGhJJf9GvyTh*9hM!&KeyW$OtdGb*Y)+*A#`xtX32jOo~s2Ra1_7f1959y&m$0lD+H z=Y*8)NJv!q*`hyKs{Rut<34=~c)I>Jn&;2I{o6p2M@DY{kF9h}OyJbN?F;$){|pQJ zA@~IeP(J@{zCVyqy+EA2Y{85$ihq03mxPe~CMaIpB()wm#2?fVtyc=XdezOU=Najn zs#$9@9{9fBx(?800__ou+Hn!(m`L{xo-oAOQuOxVs z+;Xw&D9!V6qNW@?*`lN4@r2f$TVUSmD<@-oTl4MUm zMN=?Q(WtXtq-h{xx}KOUp_4S5u@<$MEww=J+~GhW%NmwdDu3+-CuiO)o+d4ouMTU2 zN$77e+wAUo@h5v1iWGpg^Y;F&jP-YJoXo0Uh~)HV%9r9eO#W{{q5RIBCd%itrOQ2HZ@1>CBN63+c=2s(eq5@RG;MUyS zMh3y;9xO&c1e(3R>G0Ff4_j`;ZohXJUl1Si*xCiz^FclN#0dn6$?cNC8D*|7|msTMdMYiOewdQ`@`Qb; zOmeh&vU169=XuOPlR>n3wTumNIALRwJTYKi_<^Dpj{d)J$1mf+kI-hK4J+#&>3AO4 z^T_3iPU?GDI?@Qzv6G!*I9*PRJf;k7KG#$MyM{Tk5MbR5nt8i&#vJP`f7*yGbn#a( zOnh*T(U$ztD^m(@ju}S1_UW5gS(&Sw?;IVeRC$q%{O{{a|CUdO&oR*Wl1Al;oV!=m zi?EG}i3w?GY02!=b^XeTw?BihkMlO_EiXbDi3IoMh__9Pg>56R^YX5AaUFnK(~XK| z(Lf34dVbIQ{M=kog*KiwQ6;7G@EN7`&tZc%T(AB+YvV=L8o6Hv*IDSRFqz#Pyi;51 zUo{OXvs!33(&o(uq6vT|;)8_x=$ILF2@+elVt}#!+nHXx#wY%7>wnKm{KqLMPoDhi zVZbi^pPHTjFD|$m=lCRD{R4KOGM$k|ZSk8YKLi4D&47%CxF~~-n7}Xma0e{R#dl+4 zR#c@K6{L1Dzy}C{)Jjt;sj4!;H2?mGRv^fQh6+3I`5wWS3?P$tg!n+?U*q+|8aOpt zJZGYk=?Rh~5jv1W7>P#h@4nS$J){>t&&Yrs19<;SlN(JS?JDWIHrsE^rZizgn;{Sq zc;&jzBUc~d)an^A4;bZJ>$gkbcAhi_U@|KTg1aPL8b!tb_TFf`(s1TD z9R*Vmxq-iqlG=kZ+nT;%of{wjhOjXq{y09e7Oe2hUP>K5CN%Arv!o`)rg_F{cO6Fa zY0nf4(ya&}4xcA&wwVa-)J`%{yBCB9w|#=c6rzgY45Rn{PtIL;e#ACl)a|w|x0MUB zQIZjBfDF}-M-DY>EEgMA;$4!UX}xSdHU;4`a3>NkzK{s7-*ySse6uC?;q7Q6!0oOw z|6ztbMFNDSuCQ*gytxyNg6qysZ+fmF-J)7A2@9Q|*9#!K@P`2oC@N63#V@$6!v0Pw z_>pAUyzJb>t+LArjx;LjkIzjX^S!9DPOUbzpfX2+3QGi-7I7RdsLW;-{>G%Zb8s6a zH?Ix#eHV_|QS@!HB3Qbp_N7Xs#i+j`ODPyUoRF^w*zWzQXsWk3wP+RBOvw6iULVz?bAxtX>44}v?16fB;Yvo@HA_a(se_qWkiE(> z7q!cWp~qPN#c8F{SuxfuitZVlOF6SlVPuh06^DyAv7xbjLsOKv!+9bSTgcddh zWZuopX;9}bXV`bGh_9i3Gn&Sl+<5)m;z?I#_6}=ik4%$?%94<9pvREzUwd7>(!L%| zcNV0=!y_lBt62!$^*ois$~GyME5*>ZQ~?V0X};LxCTa|hFF|4C+Y%cZ+3GwrNM zE7f;oaYHIMw>Tvhyx)Eu#^Lq!Qrr0Hk!4K5>ch&ECjRzF(WKkutjZ^{6%p2#UJf2P zg7|n3YXttrIb~MMF&s&YUv&NG1(mux`(gXq9iezi(5@8xQIpycjDhnX@}2GC8?dOz z7Kob>@2G_ui=L}7zli%p=|r2NFUnci@1Y+E3t6hgN9#r6k1fp195wlq*^|4;k4a9+ zoXjRKeEN9BeWx{R#u6d)?>kFrvH#9$qP>vY<&SKL#b^s?27iSQYeCL5T(^sCJ>-xX zZ%cILPrH8Tm{an0kqVr=UpC(6%VL8u_kL%Ey@D?@m6O&P7C*pcl4L;-3|RR|3s-4; z%Q-fnQ z=0(LeI#lRzxbor^DOKQ2)vX@hTZnf3LM&z^iKEk<^cCzUOR+A3baETrJUziFJwob2 zpj!Vfr~SEVw$ZrzT=Y1)rqlLSvngw_=x#z0^ZEz#?U5`%HmM}hZsfDVSHH7avO}im zEILyfWrAjvFVes0uW?*zwP-WPOX01`VLs;3@qJd4LJ&C)YrEp;qJWn?(4m`Jk1I;b z#lv%Fgbibvnw0{PD2wPpx7OmgJG$jwv=*WMo%76HxDb8PDU|clc5xN5Aw!n~q^xE& z-%l6iX)sf`WI0iRK*^_{`}{s$ZZZ};ekv1ct-Sl$Ms%(Q#2dEk7569bf~a0Leyk<- zrOGZf!BX|eMR&tZs{*-K!SCSFH(LZhkT-hxX<W`)3fu{o*4%&ASwv2VD2ZlXT&gZNOV(RHeoNKvBO+qUbZ;DhaF|RFC zt2hdw6m>>uHC3H_Geap&()IR6AG50ceVNE=T#QxQxC65=n3%q!u;X~bC5MV#+)VaO zuA7E|930m0XG%oWK*Fm+UV{x(`p|j{`rKlunTB;nm1OI?!s)_gea{PlC?Pj8_35v{ z6h3_vVId@2qvg@L8_`=8$Bzy3&V#iNxqQIWbZ}8!u}AG?IUZ$DUsbT}C^Rggf3tpn z$*9e$mD%9t4&o;t;w^KlWs4=zGWZSgf^NoWF)f0RVu&)Mi-`}M2ag3tZA=S0ngJvH zm3w2YB|Sn_^=ZGwu~_jTJ3SS4{JC=v;6 z0n61=GkebDYADyl*v#YrzU zXSE(ya?+*g`XUqO9%-uUoIG4)?iHb{lO!(vW^y2tEp_*R#isLEpj#K_L}TM$OXws} zFpvwM-I^VQBv?hATN?Y;7twDMQ$33#)wC{1ygl1&J2$pghO*6?E#T9YoEwMUh~={5 zVn#wt`ZcW)^KNQ$7RI8j&T>WDq3)G(5#yrUG0%N4HMGf(bsln2x~W#6pDeW8XI1W0 zwdg(yEc1Ju8Y7_RiS%A={WvnEcysJOzqZ}$fS4uc$J7%rZ$8f31v-Uki(9jl>HOJS z9NNK9n70m-BeD6MqEw{5STXy%RVL(|l%TRc03$(@!-B9sywA7a{x=FB|5ocC`aI7| z=RGxjJumflojC9g2o&CW5@%;$VbL2#vRHS_>0Xo88eEi;k$h}i2v6mzNQ(04EKGbM z8?be1M$N?j{@VVfT%D-5`3o{?pVBe2fuUu_LvUf>ec6(c7V6Q8yqvi6{`R3it1R?r4KL1qYWi6s{O6Jl7x8o#4D~u3Yk`ckG*Ig}gt~sxtq4%!8 zB%@K6rPa+qu*}Uz13 zMp{x8y|y`zpSkMw`NJQ|35YsxPVX`~s{S&c`_KZ9I%0FmxYa0+ykv;u+MTfopVPHs z?U?OM$2@R*Tx0y}PwR~;5z(3wx*}k5APD1x%`Ps;2dWlsZkqNB`*=iRWr*b2cIue# z1zo?J)?_cBK=&XT9a!{dAEsPC`Jon3AUxTek@F`m2AEGp!gUcfH`~ zie}z_-g;lYDl$$(8lElsveh|_L8(Z&EUbd&*gG=BK~zR2hT5mkBmCw(9?`3Yx=2-YFlI4yovQkR z^;3yOdM_m~i{fUAm@dhA8Y8--VMw zGc;T40@hDWI|jZ3kx<00JC(hN9sSoD6FxQxeju&#%d)%QyA9HM$*qrl+ZFzksNlv> zVuAM3Z`Rk936=(y6ysb&=e2M5?U)U4G$O49%grZk2XW9zm$+(1fr57sp^vCer5B9P z&Rw2L(#6>M$lI+nkr{Re_Dj1?rM``_^9yJ=_&zLI98GB{`9~%fWxm?rkzyOW8C`F{ z;UO*f^a>HA$XRFvjz0JDwu-vFx!_+X;c0ADIT~<&sX|{AAS7fs@5i0h%QyMJYjqjN zr@3qhy()^d|E-$+nli%X>66D%oh&rq5vF$maA<%)vp-!ojhj$!K33hxme63afb^BXS>+PvmIAXTF%(<>Di4AxuJOH z6QvS<`x(pSW>*_Tj!+s?(LU%&8%*@CT=>JvLr3Ro7O>jTkvQ3PaV^BZ(?zV=hu2FT{3e zu$wadLD3K3F;KgEi+UJ?`CN~7S$9J02QjV(o9?hmO3H-R4bNM^(P0lxD^X*bHBGXb z9;*laa8S%~Db$wh7?VX$PL7hCI+aRTb?7Q=hGjyH)f@yy`7>1Ul#G7z*n<|TNauhqtrh8Yq0TPh$-Lh2SX;bP?tBAn`|pGZiMC#CMN;`_m1P&YWSRRvc<52MS=ag{(3jQx;`L( z>X*V-?6VVnW&Bez*W2iqo0oqq`a_9f^aSpO@-!dTcXyRfdogU=KCvUzvU^VK~(o^g|g$+mRp`++%wVvZt;c-uCEF)7Kx7qM8f|};|J-VfN7L! zsVF~36GZM-8)vDgel+PmLHA=P$0Kg!akWgH*)fiZ-G~cQyZib^TJ#?F2IgcbzCn|r zpQSL_m^hGTE^Wr1Qf-jD7#1TG{!!BTopGoeQ`%Aig-k&6R<#KIte?|W*sgtsXx`qS z=)z_bIkTS5ex3PT&&dbbJlQ*!;M@3f7r&O5%i2xGOCNBa$siWczzk1O7`?3*ie(s` z>LOC)OlL@HAuc=C;bV=pCE#%D_#;=9;o_ajQ16h|)=bH=9H}|5m6BVT)Q<4Wnnh_n zj_jXj%p-cLr8}l69`sAy6)0(#9b8V9xpwOYj=iocTY8evQmMOh`nXGY$(Zw8B=@w} zO!)^!>eacPlp-o0d=+y}M@f2}!m)8D&CvomjN_6wJs_^}$-=R6RiUi8C7n|!|K@ts z%JNc^hZL*SX$|Zm721Zcx9pg1Dip^}U+Hv^Eby&PoqDWv#(k=A4P6tL{8uB1_Ar6< z+bOx(!sF(*j+A7F%gvOr8P4YTU}L$8!6s;QqhpljVl!zE00m`ke`OCqq9rombswPs`Bnt%6Vm{VtEZU@-0- zV{&`W8HGM%T_7tyGdTJ2SK-zCGR^VNKV2{HZm;Pb-)@_xYkU;|QYs}hQFbLaO6cT~ zTRiN2JA*1@7|eSOTMbHmlugrgF?O^}l+HI(uF`)*-z?L+bff=;crHXq$cy- z80A5w7Ip_x>@!Lqpy22)EbjN$3NkXGnVycW&AGYTWF>iBw#0Gg*65rV8l(*Zi3tfY zj{QhTUMv;r+7ySt2$Ih5dWwBZnq;fUl!7z*o#e39w>H?Kp4cD*{o}Gq0^S>^q} z#Z~;XN#^#n^Y!lFO2+grNF&4T#JyYF%gqsl03S@}Yez2c_?zq}nmPoDo*@Lu5J*Fp z{yb(1`NYe^l?oWH{x6xPJoO@sW)!N$=?gU2H^ly&)Og9KOIy59V`B^ZaASd@up3cB`G2Da#3wi zKl7f-VBWeeu*(vEAcQEay!xF7S(AUISxs<;P$*chl2+ zT@uYYh%E`9M`uoY^jRXx{~Hm_v7z2vtv~X>*Oj?XLDM(n<6;y*FeFg=h-V%hCh``_ z?bL#KE=j8{uB$eW*K^tTAC9;?T^)|*Dx}g_9!W98Z_iL=@AnT1CA=H!tS3uhs|i}x zo_%zdI+fY8aZU`xTeTGiR=fES@amqQe2V`{fKwe`{ar z=}n4HsN3nP<9ZyIAL4)?h=VrEqcAd3p&{4a&vHcQ^pauFuXXh|FU_<;mx#=KYz)FR z4qqOjN4EVUBUYmNa=Es<3&9z2H#ooCJcfBxIf*yDjq`-r0O_XPBV0}RRM(^z8q~6AEX9@D_u4w1LIaxOu@}afJjq!}Gpco#rB-@d5 zO*+4Y2RC9KUw|(-94@r}BrWrq)g$EAk_<#OXRo&Uma9@N@SLh~Ws^*as_2W{UyrIt z_usE`+_wKotF@yR*a58UD;J=!sGtfSZ2WMOeQMjU9aAPS-dOvLdT#qZEi_za6}63D zuwd4fL~rLx6~4c%qfym>rVP-#SAKhtG@gu-oB&LNmBsF}WH(`ADfU*8yrJ@RWN1$m z7*)gvbHf;H2^o5Wtk3f# z3)K55j!-C7CC-%GZr$tkTJ7@j0)kFdav%ApR7QAsqzVe)B`lEnHv{Q(5fP)VU+bI5UN>N~wC?;p(Y3NvMeM zeqHG!lV_Q6McfiXz$lQ8gM`nb(E=^T+IN8eW$QpGZ+7Aj%#JedZwSBSZf(^erK_Zf zX|2$qGw{5;3c`u;J~!c&GB{zsf9iB!au?4Gi!>jLK?nlcox9!<*q-C5AcJHIPB{u(HuO zycvmjE;lj85G!ctIS+hQhi;|(Yq**HD~_>j@tLNgMNZ<&u)cJa(02}LpXO^~HIHOX zlzuTCFFIW=501W-%$0d5owRS>AJ>-S<5FcZmp#Uxr`m|z{NE7ioEIsJ)q7JLqu4(i z3p^hCH?%&G_N^=oi7Kd}6f2aBaTS)g5IDM=4hcHV2FbcgM_*+ezAEJpm&{|ACUPX^ zuPqDZPy4=1S3TkmqBtDBVI&F{a84sHoNHhH1Rq>It4kfiUHUnF-pm6#EO;DlFlY5Z zd}vZU9t7TSc@$t8F_0L9`2Bj=hzlR>h{I-o0FnB$0$Z?Pkw-I!&lR%+Q?OER*(17y zc;gVEAc`z;)kc;+J@qF!?(EV?JEoOBOt;njn?cM@5|^uR6`+~Qxznz<{^ZjaX8|} ztpp?iOtLzfmo+3{A|9_+&AU10Rl(&Tv|mkb`rGajWLl)NvHfz`N%9!T)OEg(dkrQe zi;Wx-X$1Ehe-3l;a{coZqY+kU7zY}$A~U*mfLqq>Bmkf+-bwfB1JhubPJ^APWKB9$ zi)n{m`I44(p^stZm7A=)p~7sKhp|iHMe^7@#fq4xpqfJhd+vS|6=IRM?%=btj(kbK zy_@O{eni5oo4?}`r~CPx=!PH1%*a)R?&X@t2bLl;UFZ84|G^*^*NYPmqqtT^G=S;m zsP-$LpZm+@@Hy!uUf*{7EGm$>t1TP&s6Eg*Y+u`2!ZAOL*w4w5TKt}z*mQ7aqB>%i z0nZ<1)7lF?>V1XoW%OvDdB@A1||eEvL4hQ zIE*|y`k2R+TbaWkw&{eeai6870I9mvJL9XF+tTj2iz9Pb7+vu?9jJH_nt53a!s($w*hroK26%MVyc2?UG`Q)Ng}_qt288wF z8-XfMujI+XD1xPM>vu5A3`&N%MAz*r3x;ZV$0aG4!P+UJIPPw{^n!)BaxbJeCJN?C zzoOo}+7%c3rhskKwLaQk>!FTo!gb~+P-CF@Q0Hr;>Ip$R2IM`FlM|w&DwABQhw^WL z>%AJF=1ABes!MHpocg1(u|W87d4b31dMHX?5p8i>l{Va?11Q3^|A5nZ^f>nsICO~BhYTzVwSw+u_>tRBL(b|Mgih{CV`3#fglszQ3x@whq5A zZNM{pwfJi_KjpQ&-UdqcA>Es-jq*jm;d7i6W%7R4aIs%+-EmzNr{|bvx}olLuCdbh zuy=((C7!J<#I?T|>)ahN#&|ZB#89(}BUs6Q2NY`R+=1sgXt*lBvl#XyXr++>%bY1D z4`AzXo8+Rf*Gy$&t)qqou=4YrH9IUaf1r-S5&LA>T^2_-!Eqg0gj*448|d!gy<~wJ zc!9=VCA8i)7|2IDEP3WD!!P#*NGclk+daZtaiVUYr+waIpX6~0a%jIH-k8=~QceBF zkCQ`%!Y7{Re)3TDI3@2{3N^Ye~gj5dHQuPByk+C zsf_n?BYqmDhUQe1;UGa6T3UrktcEs4k(SgWtf_mFZGtM$_I2fEN1T0jM_I{XUW(7m z)a+wuFlYbahsGPO#9m52@ilk2ia#@b&DDc`dPEIuMw$QF=!3aE{inaGtGaXI z$i+AsSa7y`VE`GFhWg{J$-aq*;yJkRk%r4BLc$#R@Av^iY%*3{kWm@qEz{x6Ti1e6 zN&Kw5fy3Ejcv>%07SbfGRbh7CXgiFOGSlDH^I?qVcHzv;#n{raurl>09g(L&PnUO^ zIXt1t_D5(7J(C-r%dN7HiEcN+O8!gJuMb(3Dt+dJ zGqdtKqpI%Xq!^u2e5}XvM~g$nMoDzGlWnbTo_I+dQECr(*BHh*7u6D99rp~wOsb2n z@7h*l{_m7t` z@~NL$mbgJGvBp~a6EWK{nVWx}y)ZV+?W7B*MD=h#@B@}LMYIJkC(A50>*?sTmBFMW zT1@2n=noYP^eh?eA1$BAwdYV%N#(3RKdlQNy*DpeR70~}@#3NjrkwD}6AQmFueMq_ zA)p>O8#0fldhzOGxz9-m*pl4?Qo;>Cu$R;n>f7f#Ch#bd7hM;8e*w<+UQNqg8Pzzuj$3Jh|oV9qbjeN3ESLYeZeL zSb+G4)%7gHiFJxhl$W^z;}Xv9Ph!YrM+_8kA}iY%nG8OzO$at0w_&jf=bL>`Czqd< zzq{1Z#wU13$64W6YLYEzB+U0l^1AVX%yJLIR-){P-!-jAQIZD_t^!c^Lgj~0s~S zLameJF`i-#%Cw~`bJApI@v6con?qtlOXO?Fh%`wmRX|Xz*-L7e%uEK|rZEhABbNOsayeUAW$x{yKfFtq8 z65<7E?@aMY3HqLfLv`*8=S-hG{h9imv;Gvi2_Mrw4Oy*fJFv6UaR5JoxkF;BWwy++ zwd8~FN33bND#54hPy1Wi7I5L(Qj>i1Z|~C$ocDy9zD{dA7yu{ zf;?NiIZR7=}L(10l(0s}`KRPN?irY`Fcva@}3g=E|^$7jcU__cYsV-7>;Y zlRU49_3gT}rjAa*_idH`Nz9{-xF3l}a&C%ATcu3aj<90SMw*q?)R+~ca>lhbZ=+I# zV`Cy^l{um%E%NV=-vh)VJZK>SZ@xF2YZl;$OC*|)|9SY!Q9koJ69CG60W$29 zJ3f!A%(CnXAmg+#whZUr{!fqwFzy-s6)7-RnYd93{|DC1uF?aMq-aqE!Mv*E)L70ONTu^bsUa8)b&k zrB|QcxM!62y}|(qvQRf{DE)^Wj`cF^8~Mv;NYOamuiJkOK#5#m1FJ}AF(%_79Ji;K0JyxG{~C~Am8iRDlJ>OQA|)k@=P% zsHS^$(i|(wPVRGpM)X*}q;ZSrg06k%pwdbO z(D?`!%p6r6`0Pw;)g84VqoZS>p}SRQC;Ud1(Hfp}8NXWtr8N;iWKGTG(`|r4P4^hG zN1TwwZZ~GV7uTtJZ0WK4aa(d2VX#lGqcUx3PTL&#Cl`leRy;7glY_nUR|L@9Iu@L* z=2Yt?yM@-Ga9#eZ2Kx&yrWiYF+c)o+o6ijllbFfceM9411?J~8fl{<|(>esC^;@t< z*3}1i3)>=4`!>b);AF0mZkS-H9=27pAcEty4ZLW$Uw%H(m29mn0XV-ME(l4jKsBb> z11p;9*tV+2aRY+kXgXK?_`30G;&s@7kGy70F`CelC@+WQ6htEb@Yjo$P%}YfERNlS z=xU2E=L@0QT)j2w>_sh1BF-wW7?Kpm?HzVv8A;wsWmzPSVnqNSwu8Qs4+cJx? z45MVZg=A7cM02sSE@narR|^Deo0@$~zi|8Tk1a&(f@0l{Bi*FDYcjU&V0 z!>+!@+zm-@DwQ~p3?J0IT&ShYwTZ>_a9N9I1vmsGtFn%CJ(4wAAR~^Ib91n!D}n3e zZsXQAJCMA5i63p6&HUb5RJ{+ISc2ywf|`-Bk$-3wuXYRFyn1WJPEqxhQV)6WhYDM& z6n-wu$I~w^H47NAzQrPKf(pqb@g{7cZc36_CfD9hh4OkvgO#361T zVs)+@8ZXP$)@Ki+xF7wj{D0}sovnKv${shEN^XuZqe0;wd3vzUp! z3~OTR)}ZG|lX3*42}n_p7L;BB(hne@bb?f+BOs9)s)8IqKm;j5r~=ZY1|(78k zYa%uD&_gKU4&eQF?tSie*E2t|r>vQknZ0MtUhjI>JcTgBINf5!j8YZ)UD(cK3v+RH z9>5KdN^h~uNBkn(GRZfo@At_570df7k{HiF0rvDX}AnMd8l<9RrE#tFOcfxC@v&H z1a<=_tpc&7Ccmit*SoKcwc3*)SF!^YKLtfz0I#rhbg|-M{O+`z~0(SH^#J zeX|NxU;e$6#_5GCedgq_aXPQ+i}pua@;9wy55)oMSs?{~sx6~LJebi`- zXfqdq`;v2=I&(2YVNK3&Uq;I!6?mE=nD9!dH$V9J0$)uLn#aRJ3Dz5*`~!RDuCC7C zVx)`cW+P8g1lKKD&o33lb50_Np>)}J<(}lwXh9vc!!o67Ml1qha&ZM&x29=h!Yx~z z(I9Et)=5lo`z}xDJc-02f@j?a9k6lD-fYG4Q@)rTs!zD#R5@wih4c@2M~>UMhaIn= zW^5s-2`qXf*n<{JY#ZKpWOze#9vFm2!}l<|pMyPO-NuRAr-2f$+bpIa7{qS~F;Opu zjH~z+x{XK%ibtrUBWI<9YHgl&DWGsOFU7{AHvt$t?1cJ|^{c9r?A!WtIM6DH~nz#Nz z@39O;Pc`-g2b#JIfRR;2uBQI2prJ&uPoEUYnbp$myl@kp%dXz8Gh-9z!H(Q@S~{&G zlOr;hwV44UmS%E8xq2IzOff*z{(fEe1f}@VQ&ymUT$o|bbw1SG$_A)Q;zFj`x;OJ% zZmWwxls2U7yJlFM+6&#s-<0noAiajxkD`Suf%{ChVe?kuNOvonIkQI5@9`)1n2Tg# zM={sJbd|?>?g3MgcZp`UBq%1bfr<^lp_&FLP_UBd_HS|kI`8F@3_BGlWbusMd162s z4kgQc)J%?xJRe_vS?MXpS_t^{L54(}I4PUce7LM2C!;1bu?2)%DK3aJQDmbNVq!4TzarqB z(7*tb#LnuzZ2D`!PYb`?rv}o8(!Beeg_bYN0B1tCJ+lFwlEvd49;hDvZdZu+{lIf?=7?{c^Ub@PsS*p*`RupPRYx^u`1SAGC*F zMb8Ob^AFk+zr(S0u@YixZ}YZ2OlB7=e2T{agmmQ_2l+xjxM%`0Fq{@|Cp*Q!PNkPZrNC_2dHggcQINsW>97b}db`u;xi(4=jl7!v{ zgD(R_)KgZQWijws?KdGoI+UmKs!=pVAJoFCWt)|{~g@>`t|&a%Lyle!foN(VLHp{R2wtJMC@ZU6&Kvm8V+f?+g^C*3c1_77*8I1fr-V z3ox~MO(?kKv0L5K!2&_6juga;3YbO}5aCi(sM?(2xZTFD*bu0URE=|W9W<~ke9SFY zVe@oQM(Bk$Jz*zEth&r9T8jg*?g~)yZIf_ams5|sI(n27lgz3+>tHf!_%#lX;Lf?x z9Re2zy<)}sGDHjIP~;>Yl=xbUvX_O*MH`|6)YWM7S~e*!FJ0Oe_2onq6Rr{zxo z3Dwm8)3(4>N;t{S6AQhhdBA^Q^lG)o+v0nXIRtw@!BH85Bo-DX<%et5Nc-6MTO*(C zT9ljiK0{>GR#P{H2$%el73vVbJpD7Ed$!9h{l2S-B^|uU4nLjL;hRu@^@pO8#)_FF z|AWNr$8$x3CTbTEbBk$Z6>j>ZYw%*TFJX?#Sr3ufC+I9AGcmZbyaoQuU!v09hDZk* zy`KM?@5enkF7sK9`6qr27)?$^@Wu_sus;{TQHVLPB1}|=orO{BvM_5<)2%D$%ID-9 z+>Gce(#yk$dY{aRPqn3l{_SSjCCsi9!m{4)sRu*I_CkkEnwCh?z#Ws|ITBxz5O_ig z$m$DelLn^%%ulIr_;0?0u(wnOJPdSqN2VlBkMQfdu(RC~Q17TxcUH)0i8-88g3J4? zkA}AIQB>UlYBw=nhzA`M<-=!xH7%`~_PLsEs5|_=*wh%duv@t?FV7_2&SXN&XmZcs zPV)(#m*AWdx1~7y=m#_iz3eJE?PfvCj@9;>9b8k7XBg|q7-?IfO2g_eNRkrlV8gd&eHQ<$n)Ze{XtB=@ui)f>!}ZMRnO6I+rw3e~7;L zhE`2@y3|8SXkYwFO)c@w{zz7-R#`(9^uov0w%!bG0s4fU9sOy54ilC|9t#(G*uqAn zVSF3SagmT*mc{jL|gZn5!tr5A3eEL1@(=(Xwa z+v7sO6>+cvUf+P+WfR0uR`S07SI45lpym9&Q`7LFs44hRXRVyH7{v?MxY&;bmIbp* zOiR-V9jf(BDCqgFIYC0+WTtbCMV%9LXKOalQtd`#IJz;CuZnAPlp-7_4$kqxkE2m0JK~WHL!s=87$S(H%z{iw>&B59Qix8@w+RHkKGciT_rW zoH?Umrc>mQFl=F_kAa1(5BA7|k;b$00rzd^6n$_~*+nX(5qdp8$?pMw7X;Q5VV?W@ zErAPA!q*rc+FC(?D&*j6EP2%7GEUv9d0rij0yp`Xyz7(o^>?>=yFlYHxw{2f%Zuj! zjrD6OHz!Z~I9gS7+yQmU)*dzBDq1&N9Ipu%C#ZyQI+gxFfZvTvbNa?(op#^oA21aR zd4KsxOhL0es{+F1j)MV1)1QZTZYS0+f2VM})Yr(G8tkXNve4Hj0si*zsq1ftsIyd- zJk1HyptZ&#FxBP|;iX+~fMM`m9g`#&n%_fJ#ie2jyVJfpQ4p}X0CKj}ResMy%VrF@ z{oqwZr{Qx0Pr1#pLbJo}M(*<88guwJV@X%+j=8MdgxLB3$4$bCT-4YOR%adD0nSB9 zq|d#VY>s?c^080}Bz7IL=TIebQ|R3y$I<{l(NE{xVuD?AqtowuY3?Ykp5M0*bWm}8HD`D{ z(`tnRYru~o0Ny3KtvCj5F8^3}2Nc0zKM$jOV*QpSg&7ZqdXHqp?+t!GY_t>ct*&5X^yJ>n6ufnBevSUoh}xfgiz3P{+J4P=9pb_O~J zDO}{DEX2^r2K;vQ+W!8wtE1wW+(Xlh(FVncv0{PzNIe;D`dz{J%rl&eBUqmMa6gWg zIpgYnEU@+{w3^OV4oLY&89x340R4t#UOzpilj;KSx%R&`HvaU>PfYvq59BJKg-lyR LU;U%1?eqTv;)IkR literal 67049 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 Date: Thu, 27 Jun 2024 07:23:09 +0100 Subject: [PATCH 016/430] doc: fix README format --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1c85faa613..897281f845 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ a closer look, then clone and configure the project as explained in the next sec You can download the ISO from the [openSUSE Build Service](https://download.opensuse.org/repositories/systemsmanagement:/Agama:/Devel/images/iso/). -> [!NOTE] Make sure to download the correct ISO file according to your system architecture (eg. you -> would need to choose a file including `x86_64` if you use an Intel or AMD 64-bit processor). +> [!NOTE] +> Make sure to download the correct ISO file according to your system architecture (eg. you would +> need to choose a file including `x86_64` if you use an Intel or AMD 64-bit processor). ## Remote access @@ -72,10 +73,11 @@ For the case you do not know the address, or just for convenience, the Live ISO mDNS (sometimes called Avahi, Zeroconf, Bonjour) for hostname resolution. Therefore, connecting to `https://agama.local` should do the trick. -> [!WARNING] Do not use the `.local` hostnames in untrusted networks (like public WiFi networks, -> shared networks), it is a security risk. An attacker can easily send malicious responses for the -> `.local` hostname resolutions and point you to a wrong Agama instance which could for example -> steal your root password! +> [!WARNING] +> Do not use the `.local` hostnames in untrusted networks (like public WiFi networks, shared +> networks), it is a security risk. An attacker can easily send malicious responses for the `.local` +> hostname resolutions and point you to a wrong Agama instance which could for example steal your +> root password! If you have troubles or you want to know more about this feature, check our [Avahi/mDNS](./doc/avahi.md) documentation. From afdebef7e76da1ec541aa23c58defc0ede042a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 28 May 2024 11:41:03 +0100 Subject: [PATCH 017/430] refactor(rust): replace rpassword with inquire --- rust/Cargo.lock | 111 +++++++++++++++++++++++++++++------- rust/agama-cli/Cargo.toml | 3 +- rust/agama-cli/src/auth.rs | 25 +++++++- rust/agama-cli/src/error.rs | 4 +- 4 files changed, 116 insertions(+), 27 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e16907f6d1..2d9923e5a5 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -28,11 +28,12 @@ dependencies = [ "console", "curl", "fs_extra", + "home", "indicatif", + "inquire", "log", "nix 0.27.1", "reqwest 0.11.27", - "rpassword", "serde", "serde_json", "serde_yaml", @@ -945,6 +946,31 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -1073,6 +1099,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.10.0" @@ -1355,6 +1387,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1788,6 +1829,22 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.5.0", + "crossterm", + "dyn-clone", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "instant" version = "0.1.12" @@ -2081,6 +2138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -2103,6 +2161,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nix" version = "0.26.4" @@ -2895,27 +2962,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "rpassword" -version = "7.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" -dependencies = [ - "libc", - "rtoolbox", - "windows-sys 0.48.0", -] - -[[package]] -name = "rtoolbox" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "rust-ini" version = "0.19.0" @@ -3193,6 +3239,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index 71d3629795..f891d1f6cc 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -25,8 +25,9 @@ zbus = { version = "3", default-features = false, features = ["tokio"] } tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } async-trait = "0.1.77" reqwest = { version = "0.11", features = ["json"] } -rpassword = "7.3.1" url = "2.5.0" +home = "0.5.9" +inquire = { version = "0.7.5", default-features = false, features = ["crossterm", "one-liners"] } [[bin]] name = "agama" diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 7ce7d35f13..a0d5f8d3b4 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -2,6 +2,7 @@ use agama_lib::auth::AuthToken; use clap::Subcommand; use crate::error::CliError; +use inquire::{validator::Validation, Password}; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use std::io::{self, IsTerminal}; @@ -37,15 +38,35 @@ pub async fn run(subcommand: AuthCommands) -> anyhow::Result<()> { fn read_password() -> Result { let stdin = io::stdin(); let password = if stdin.is_terminal() { - rpassword::prompt_password("Please, introduce the root password: ")? + ask_password()? } else { let mut buffer = String::new(); - stdin.read_line(&mut buffer)?; + stdin + .read_line(&mut buffer) + .map_err(|_| CliError::MissingPassword)?; buffer }; Ok(password) } +/// Asks interactively for the password. +fn ask_password() -> Result { + let validator = |input: &str| { + if input.is_empty() { + Ok(Validation::Invalid("The password cannot be blank.".into())) + } else { + Ok(Validation::Valid) + } + }; + + Password::new("Please, introduce the root password:") + .with_validator(validator) + .without_confirmation() + .with_help_message("Press to exit.") + .prompt() + .map_err(|_| CliError::MissingPassword) +} + /// Necessary http request header for authenticate fn authenticate_headers() -> HeaderMap { let mut headers = HeaderMap::new(); diff --git a/rust/agama-cli/src/error.rs b/rust/agama-cli/src/error.rs index 2aa5985981..228692513f 100644 --- a/rust/agama-cli/src/error.rs +++ b/rust/agama-cli/src/error.rs @@ -6,6 +6,6 @@ pub enum CliError { ValidationError, #[error("Could not start the installation")] InstallationError, - #[error("Could not read the password: {0}")] - MissingPassword(#[from] std::io::Error), + #[error("No password was provided")] + MissingPassword, } From 8593a435929df7cc36303791cdb2720c88136724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 28 May 2024 12:36:37 +0100 Subject: [PATCH 018/430] chore(rust): update the changes file --- rust/package/agama.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index fed08d18ff..83ea4bc679 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jun 27 07:02:29 UTC 2024 - Imobach Gonzalez Sosa + +- Improve the prompt to introduce the password in the "auth login" + command (gh#openSUSE/agama#1271). + ------------------------------------------------------------------- Wed Jun 26 12:56:31 UTC 2024 - Knut Anderssen From 5e83aaecfc2524e968f93826b691535ef0b5cede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 27 Jun 2024 09:35:12 +0100 Subject: [PATCH 019/430] fix(storage): avoid error in storage actions (hot-fix) --- service/lib/agama/storage/manager.rb | 21 ++++++++++++++++++++- service/package/rubygem-agama-yast.changes | 6 ++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index a880adf3cc..a91f4c141a 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -171,7 +171,26 @@ def actions probed = Y2Storage::StorageManager.instance.probed staging = Y2Storage::StorageManager.instance.staging - ActionsGenerator.new(probed, staging).generate + # FIXME: This is a hot-fix to avoid segmentation fault in the actions, see + # https://github.com/openSUSE/agama/issues/1396. + # + # Source of the problem: + # * An actiongraph is generated from the target devicegraph. + # * The list of compound actions is recovered from the actiongraph. + # * No refrence to the actiongraph is kept, so the object is a candidate to be cleaned by + # the ruby GC. + # * Accessing to the generated actions raises a segmentation fault if the actiongraph was + # cleaned. + # + # There was a previous attempt of fixing the issue by keeping a reference to the + # actiongraph in the ActionsGenerator object. But that solution is not enough because + # the ActionGenerator object is also cleaned up. + # + # As a hot-fix, the generator is kept in an instance variable to avoid the GC to kill it. + # A better solution is needed, for example, by avoiding to store an instance of a compound + # action in the Action object. + @generator = ActionsGenerator.new(probed, staging) + @generator.generate end # Changes the service's locale diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 8d358c5ca1..c267b7ce6b 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jun 27 08:36:13 UTC 2024 - José Iván López González + +- Avoid error in storage actions (hot-fix) + (gh#openSUSE/agama#1400). + ------------------------------------------------------------------- Wed Jun 26 13:54:28 UTC 2024 - José Iván López González From e81e7437258ae26b5309f0910d0632927a9e3b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 10:28:30 +0100 Subject: [PATCH 020/430] Apply suggestions from code review Co-authored-by: Martin Vidner --- rust/agama-cli/src/auth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index a0d5f8d3b4..317b9dca48 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -49,7 +49,7 @@ fn read_password() -> Result { Ok(password) } -/// Asks interactively for the password. +/// Asks interactively for the password. (For authentication, not for changing it) fn ask_password() -> Result { let validator = |input: &str| { if input.is_empty() { @@ -59,7 +59,7 @@ fn ask_password() -> Result { } }; - Password::new("Please, introduce the root password:") + Password::new("Please enter the root password:") .with_validator(validator) .without_confirmation() .with_help_message("Press to exit.") From c20364f578612067724b246e238931acce62888d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 12:50:03 +0100 Subject: [PATCH 021/430] fix(cli): allow empty passwords in "auth login" --- rust/agama-cli/src/auth.rs | 15 +++------------ rust/agama-cli/src/error.rs | 7 +++++-- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 317b9dca48..435b2924b5 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -2,7 +2,7 @@ use agama_lib::auth::AuthToken; use clap::Subcommand; use crate::error::CliError; -use inquire::{validator::Validation, Password}; +use inquire::Password; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use std::io::{self, IsTerminal}; @@ -43,7 +43,7 @@ fn read_password() -> Result { let mut buffer = String::new(); stdin .read_line(&mut buffer) - .map_err(|_| CliError::MissingPassword)?; + .map_err(CliError::StdinPassword)?; buffer }; Ok(password) @@ -51,20 +51,11 @@ fn read_password() -> Result { /// Asks interactively for the password. (For authentication, not for changing it) fn ask_password() -> Result { - let validator = |input: &str| { - if input.is_empty() { - Ok(Validation::Invalid("The password cannot be blank.".into())) - } else { - Ok(Validation::Valid) - } - }; - Password::new("Please enter the root password:") - .with_validator(validator) .without_confirmation() .with_help_message("Press to exit.") .prompt() - .map_err(|_| CliError::MissingPassword) + .map_err(CliError::InteractivePassword) } /// Necessary http request header for authenticate diff --git a/rust/agama-cli/src/error.rs b/rust/agama-cli/src/error.rs index 228692513f..5ae196612e 100644 --- a/rust/agama-cli/src/error.rs +++ b/rust/agama-cli/src/error.rs @@ -1,3 +1,4 @@ +use inquire::InquireError; use thiserror::Error; #[derive(Error, Debug)] @@ -6,6 +7,8 @@ pub enum CliError { ValidationError, #[error("Could not start the installation")] InstallationError, - #[error("No password was provided")] - MissingPassword, + #[error("Could not read the password")] + InteractivePassword(#[source] InquireError), + #[error("Could not read the password from the standard input")] + StdinPassword(#[source] std::io::Error), } From 6d58288bbb9f3512525646b846647de32626580c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 12:50:36 +0100 Subject: [PATCH 022/430] refactor(cli): rename CliError variants --- rust/agama-cli/src/error.rs | 4 ++-- rust/agama-cli/src/main.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/agama-cli/src/error.rs b/rust/agama-cli/src/error.rs index 5ae196612e..0fe383b741 100644 --- a/rust/agama-cli/src/error.rs +++ b/rust/agama-cli/src/error.rs @@ -4,9 +4,9 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum CliError { #[error("Cannot perform the installation as the settings are not valid")] - ValidationError, + Validation, #[error("Could not start the installation")] - InstallationError, + Installation, #[error("Could not read the password")] InteractivePassword(#[source] InquireError), #[error("Could not read the password from the standard input")] diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index e3bde3f913..1c0642eddc 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -63,7 +63,7 @@ async fn install(manager: &ManagerClient<'_>, max_attempts: u8) -> anyhow::Resul manager.wait().await?; if !manager.can_install().await? { - return Err(CliError::ValidationError)?; + return Err(CliError::Validation)?; } let progress = tokio::spawn(async { show_progress().await }); @@ -81,7 +81,7 @@ async fn install(manager: &ManagerClient<'_>, max_attempts: u8) -> anyhow::Resul } if attempts == max_attempts { eprintln!("Giving up."); - return Err(CliError::InstallationError)?; + return Err(CliError::Installation)?; } attempts += 1; sleep(Duration::from_secs(1)); From ddfedf47cfb7f0ece824566a68ff09988c11c8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:20:49 +0100 Subject: [PATCH 023/430] fix(web): do not lost Storage title in overview (#1402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The Overview section is missing the `Storage` title under certain circumstances, see https://github.com/openSUSE/agama/issues/1401 ## Solution Properly wrap the returned text within the internal `Content` component at overview/StorageSection. --------- Co-authored-by: Imobach González Sosa --- web/package/agama-web-ui.changes | 6 ++++++ web/src/components/overview/StorageSection.jsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 3e681de84b..9fd854841b 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jun 27 12:05:19 UTC 2024 - David Diaz + +- Do not lose the storage title in the overview + (gh#openSUSE/agama#1402). + ------------------------------------------------------------------- Wed Jun 26 16:46:58 UTC 2024 - David Diaz diff --git a/web/src/components/overview/StorageSection.jsx b/web/src/components/overview/StorageSection.jsx index cf45b749c3..b26dc82ee9 100644 --- a/web/src/components/overview/StorageSection.jsx +++ b/web/src/components/overview/StorageSection.jsx @@ -155,7 +155,7 @@ export default function StorageSection() { const pvDevices = result.settings.targetPVDevices; if (pvDevices.length > 1) { - return {msgLvmMultipleDisks(result.settings.spacePolicy)}; + return {msgLvmMultipleDisks(result.settings.spacePolicy)}; } else { const [msg1, msg2] = msgLvmSingleDisk(result.settings.spacePolicy).split("%s"); From 677c4f6c97ac52bbd1d68e0e17d46e7f1840b5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 13:22:49 +0100 Subject: [PATCH 024/430] fix(cli): remove useless help text --- rust/agama-cli/src/auth.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/agama-cli/src/auth.rs b/rust/agama-cli/src/auth.rs index 435b2924b5..a64a754055 100644 --- a/rust/agama-cli/src/auth.rs +++ b/rust/agama-cli/src/auth.rs @@ -53,7 +53,6 @@ fn read_password() -> Result { fn ask_password() -> Result { Password::new("Please enter the root password:") .without_confirmation() - .with_help_message("Press to exit.") .prompt() .map_err(CliError::InteractivePassword) } From c608ed2fc5942e7d2fe5d03abae3e062be18476d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 13:24:13 +0100 Subject: [PATCH 025/430] fix(cli): drop unused dependency --- rust/Cargo.lock | 1 - rust/agama-cli/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2d9923e5a5..ae78586b97 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -28,7 +28,6 @@ dependencies = [ "console", "curl", "fs_extra", - "home", "indicatif", "inquire", "log", diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index f891d1f6cc..5896b79901 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -26,7 +26,6 @@ tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } async-trait = "0.1.77" reqwest = { version = "0.11", features = ["json"] } url = "2.5.0" -home = "0.5.9" inquire = { version = "0.7.5", default-features = false, features = ["crossterm", "one-liners"] } [[bin]] From 9c3f65e2f986a1be0abbc9bb502de8234d666037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 07:12:05 +0100 Subject: [PATCH 026/430] chore: bump version --- live/src/agama-live.kiwi | 2 +- service/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/live/src/agama-live.kiwi b/live/src/agama-live.kiwi index 841330f111..f426efb05b 100644 --- a/live/src/agama-live.kiwi +++ b/live/src/agama-live.kiwi @@ -13,7 +13,7 @@ - 8.0.0 + 9.0.0 zypper en_US us diff --git a/service/Gemfile.lock b/service/Gemfile.lock index e0bfcd9a64..eacdcece4e 100755 --- a/service/Gemfile.lock +++ b/service/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - agama-yast (8) + agama-yast (9) cfa (~> 1.0.2) cfa_grub2 (~> 2.0.0) cheetah (~> 1.0.0) From 049a900a8f46b616a19ab3bdfcd9e8fc867ca738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 07:12:40 +0100 Subject: [PATCH 027/430] chore: update changes files --- live/src/agama-live.changes | 5 +++++ rust/package/agama.changes | 5 +++++ service/package/rubygem-agama-yast.changes | 5 +++++ web/package/agama-web-ui.changes | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/live/src/agama-live.changes b/live/src/agama-live.changes index 43571da31b..caed6ac0f1 100644 --- a/live/src/agama-live.changes +++ b/live/src/agama-live.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Jun 27 13:24:19 UTC 2024 - Imobach Gonzalez Sosa + +- Version 9 + ------------------------------------------------------------------- Fri Jun 14 10:36:52 UTC 2024 - Ladislav Slezák diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 83ea4bc679..472902e50f 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Jun 27 13:22:51 UTC 2024 - Imobach Gonzalez Sosa + +- Version 9 + ------------------------------------------------------------------- Thu Jun 27 07:02:29 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index c267b7ce6b..83ea92b628 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Jun 27 13:22:06 UTC 2024 - Imobach Gonzalez Sosa + +- Version 9 + ------------------------------------------------------------------- Thu Jun 27 08:36:13 UTC 2024 - José Iván López González diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 9fd854841b..ca839203ab 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Jun 27 13:23:08 UTC 2024 - Imobach Gonzalez Sosa + +- Version 9 + ------------------------------------------------------------------- Thu Jun 27 12:05:19 UTC 2024 - David Diaz From 7d5c42301d01c3778530a0f4119eac3f073c4055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 14:59:43 +0100 Subject: [PATCH 028/430] doc(auto): update agama-auto changelog --- autoinstallation/package/agama-auto.changes | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/autoinstallation/package/agama-auto.changes b/autoinstallation/package/agama-auto.changes index e58869128e..269d223d66 100644 --- a/autoinstallation/package/agama-auto.changes +++ b/autoinstallation/package/agama-auto.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Jun 27 13:58:17 UTC 2024 - Imobach Gonzalez Sosa + +- Version 9 + ------------------------------------------------------------------- Fri May 24 08:15:21 UTC 2024 - Imobach Gonzalez Sosa From bbd6236a4f27087ccce80df0389255a915bbf0d1 Mon Sep 17 00:00:00 2001 From: Steffen Winterfeldt Date: Thu, 27 Jun 2024 16:19:32 +0200 Subject: [PATCH 029/430] enable checksum generation for s390x agama live images For some resaon this has been enabled for all architectures but s390x. It should be enabled for all. --- live/src/agama-live.kiwi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/live/src/agama-live.kiwi b/live/src/agama-live.kiwi index f426efb05b..1198bdeb07 100644 --- a/live/src/agama-live.kiwi +++ b/live/src/agama-live.kiwi @@ -34,7 +34,7 @@ - + From a203ce39fc6603f591fa8a4ec4939154967c5f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 27 Jun 2024 15:37:42 +0100 Subject: [PATCH 030/430] doc(live): update live changes file --- live/src/agama-live.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/live/src/agama-live.changes b/live/src/agama-live.changes index caed6ac0f1..155178eca6 100644 --- a/live/src/agama-live.changes +++ b/live/src/agama-live.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jun 27 14:33:24 UTC 2024 -Steffen Winterfeldt + +- Enable checksum generation for s390x agama live images + (gh#openSUSE/agama#1406). + ------------------------------------------------------------------- Thu Jun 27 13:24:19 UTC 2024 - Imobach Gonzalez Sosa From ad6106da8d744ef04f0848537cb8daccc16b9b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 28 Jun 2024 06:52:55 +0100 Subject: [PATCH 031/430] storage: rename JSON conversions --- service/lib/agama/dbus/storage/manager.rb | 40 +++++------ .../lib/agama/storage/proposal_settings.rb | 18 +++++ .../storage/proposal_settings_conversion.rb | 20 ------ .../storage/proposal_settings_conversions.rb | 31 ++++++++ .../from_json.rb} | 64 ++++++++--------- .../to_json.rb} | 25 ++++--- service/lib/agama/storage/volume.rb | 23 +++++- .../lib/agama/storage/volume_conversion.rb | 20 ------ .../lib/agama/storage/volume_conversions.rb | 31 ++++++++ .../from_json.rb} | 71 ++++++++++--------- .../to_json.rb} | 14 ++-- .../test/agama/dbus/storage/manager_test.rb | 7 +- .../proposal_settings_conversion_test.rb | 27 ------- .../from_json_test.rb} | 42 +++++------ .../from_y2storage_test.rb | 0 .../to_json_test.rb} | 8 +-- .../to_y2storage_test.rb | 0 .../agama/storage/proposal_settings_test.rb | 27 +++++++ .../agama/storage/volume_conversion_test.rb | 28 -------- .../from_json_test.rb} | 44 ++++++------ .../from_y2storage_test.rb | 0 .../to_json_test.rb} | 6 +- .../to_y2storage_test.rb | 0 service/test/agama/storage/volume_test.rb | 53 ++++++++++++++ 24 files changed, 339 insertions(+), 260 deletions(-) create mode 100644 service/lib/agama/storage/proposal_settings_conversions.rb rename service/lib/agama/storage/{proposal_settings_conversion/from_schema.rb => proposal_settings_conversions/from_json.rb} (71%) rename service/lib/agama/storage/{proposal_settings_conversion/to_schema.rb => proposal_settings_conversions/to_json.rb} (80%) create mode 100644 service/lib/agama/storage/volume_conversions.rb rename service/lib/agama/storage/{volume_conversion/from_schema.rb => volume_conversions/from_json.rb} (65%) rename service/lib/agama/storage/{volume_conversion/to_schema.rb => volume_conversions/to_json.rb} (88%) rename service/test/agama/storage/{proposal_settings_conversion/from_schema_test.rb => proposal_settings_conversions/from_json_test.rb} (86%) rename service/test/agama/storage/{proposal_settings_conversion => proposal_settings_conversions}/from_y2storage_test.rb (100%) rename service/test/agama/storage/{proposal_settings_conversion/to_schema_test.rb => proposal_settings_conversions/to_json_test.rb} (91%) rename service/test/agama/storage/{proposal_settings_conversion => proposal_settings_conversions}/to_y2storage_test.rb (100%) rename service/test/agama/storage/{volume_conversion/from_schema_test.rb => volume_conversions/from_json_test.rb} (83%) rename service/test/agama/storage/{volume_conversion => volume_conversions}/from_y2storage_test.rb (100%) rename service/test/agama/storage/{volume_conversion/to_schema_test.rb => volume_conversions/to_json_test.rb} (93%) rename service/test/agama/storage/{volume_conversion => volume_conversions}/to_y2storage_test.rb (100%) create mode 100644 service/test/agama/storage/volume_test.rb diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 6019320ef5..70fd4cf970 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -30,13 +30,13 @@ require "agama/dbus/interfaces/service_status" require "agama/dbus/storage/devices_tree" require "agama/dbus/storage/iscsi_nodes_tree" -require "agama/dbus/storage/proposal_settings_conversion" require "agama/dbus/storage/proposal" +require "agama/dbus/storage/proposal_settings_conversion" require "agama/dbus/storage/volume_conversion" require "agama/dbus/storage/with_iscsi_auth" require "agama/dbus/with_service_status" require "agama/storage/encryption_settings" -require "agama/storage/proposal_settings_conversion" +require "agama/storage/proposal_settings" require "agama/storage/volume_templates_builder" Yast.import "Arch" @@ -99,12 +99,12 @@ def probe # AutoYaST settings: { "storage": ... } vs { "legacyAutoyastStorage": ... }. def apply_storage_config(serialized_config) @serialized_storage_config = serialized_config - storage_config = JSON.parse(serialized_config, symbolize_names: true) + config_json = JSON.parse(serialized_config, symbolize_names: true) - if (guided_settings = storage_config.dig(:storage, :guided)) - calculate_guided_proposal(guided_settings) - elsif (autoyast_settings = storage_config[:legacyAutoyastStorage]) - calculate_autoyast_proposal(autoyast_settings) + if (settings_json = config_json.dig(:storage, :guided)) + calculate_guided_proposal(settings_json) + elsif (settings_json = config_json[:legacyAutoyastStorage]) + calculate_autoyast_proposal(settings_json) else raise "Invalid config: #{serialized_config}" end @@ -259,11 +259,10 @@ def proposal_result return {} unless proposal.calculated? if proposal.strategy?(ProposalStrategy::GUIDED) - settings = Agama::Storage::ProposalSettingsConversion.to_schema(proposal.settings) { "success" => proposal.success?, "strategy" => ProposalStrategy::GUIDED, - "settings" => settings.to_json + "settings" => proposal.settings.to_json_settings.to_json } else { @@ -400,16 +399,15 @@ def proposal # Calculates a guided proposal. # - # @param settings [Hash] Settings according to the JSON schema. + # @param settings_json [Hash] JSON settings according to schema. # @return [Integer] 0 success; 1 error - def calculate_guided_proposal(settings) - proposal_settings = Agama::Storage::ProposalSettingsConversion.from_schema( - settings, config: config - ) + def calculate_guided_proposal(settings_json) + proposal_settings = Agama::Storage::ProposalSettings + .new_from_json(settings_json, config: config) logger.info( "Calculating guided storage proposal from D-Bus.\n" \ - "Input settings: #{settings}\n" \ + "Input settings: #{settings_json}\n" \ "Agama settings: #{proposal_settings.inspect}" ) @@ -419,15 +417,15 @@ def calculate_guided_proposal(settings) # Calculates an AutoYaST proposal. # - # @param settings [Hash] AutoYaST settings. + # @param settings_json [Hash] AutoYaST settings. # @return [Integer] 0 success; 1 error - def calculate_autoyast_proposal(settings) + def calculate_autoyast_proposal(settings_json) # Ensures keys are strings. - autoyast_settings = JSON.parse(settings.to_json) + autoyast_settings = JSON.parse(settings_json.to_json) logger.info( "Calculating AutoYaST storage proposal from D-Bus.\n" \ - "Input settings: #{settings}\n" \ + "Input settings: #{settings_json}\n" \ "AutoYaST settings: #{autoyast_settings}" ) @@ -437,12 +435,12 @@ def calculate_autoyast_proposal(settings) # Generates the storage config from the current proposal, if any. # - # @return [Hash] Storage config according to the JSON schema. + # @return [Hash] Storage config according to JSON schema. def generate_storage_config if proposal.strategy?(ProposalStrategy::GUIDED) { storage: { - guided: Agama::Storage::ProposalSettingsConversion.to_schema(proposal.settings) + guided: proposal.settings.to_json_settings } } elsif proposal.strategy?(ProposalStrategy::AUTOYAST) diff --git a/service/lib/agama/storage/proposal_settings.rb b/service/lib/agama/storage/proposal_settings.rb index 77e142dda7..cbf3d297a8 100644 --- a/service/lib/agama/storage/proposal_settings.rb +++ b/service/lib/agama/storage/proposal_settings.rb @@ -22,6 +22,7 @@ require "agama/storage/boot_settings" require "agama/storage/device_settings" require "agama/storage/encryption_settings" +require "agama/storage/proposal_settings_conversions" require "agama/storage/space_settings" module Agama @@ -85,6 +86,23 @@ def default_boot_device end end + # Creates a new proposal settings object from JSON hash according to schema. + # + # @param settings_json [Hash] + # @param config [Config] + # + # @return [ProposalSettings] + def self.new_from_json(settings_json, config:) + Storage::ProposalSettingsConversions::FromJSON.new(settings_json, config: config).convert + end + + # Generates a JSON hash according to schema. + # + # @return [Hash] + def to_json_settings + Storage::ProposalSettingsConversions::ToJSON.new(self).convert + end + private # Device used for booting. diff --git a/service/lib/agama/storage/proposal_settings_conversion.rb b/service/lib/agama/storage/proposal_settings_conversion.rb index ea0f28f161..f8439c7531 100644 --- a/service/lib/agama/storage/proposal_settings_conversion.rb +++ b/service/lib/agama/storage/proposal_settings_conversion.rb @@ -19,9 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/proposal_settings_conversion/from_schema" require "agama/storage/proposal_settings_conversion/from_y2storage" -require "agama/storage/proposal_settings_conversion/to_schema" require "agama/storage/proposal_settings_conversion/to_y2storage" module Agama @@ -47,24 +45,6 @@ def self.from_y2storage(y2storage_settings, settings) def self.to_y2storage(settings, config:) ToY2Storage.new(settings, config: config).convert end - - # Performs conversion from Hash according to the JSON schema. - # - # @param schema_settings [Hash] - # @param config [Agama::Config] - # - # @return [Agama::Storage::ProposalSettings] - def self.from_schema(schema_settings, config:) - FromSchema.new(schema_settings, config: config).convert - end - - # Performs conversion according to the JSON schema. - # - # @param settings [Agama::Storage::ProposalSettings] - # @return [Hash] - def self.to_schema(settings) - ToSchema.new(settings).convert - end end end end diff --git a/service/lib/agama/storage/proposal_settings_conversions.rb b/service/lib/agama/storage/proposal_settings_conversions.rb new file mode 100644 index 0000000000..6ad03db45f --- /dev/null +++ b/service/lib/agama/storage/proposal_settings_conversions.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/proposal_settings_conversions/from_json" +require "agama/storage/proposal_settings_conversions/to_json" + +module Agama + module Storage + # Conversions for the proposal settings. + module ProposalSettingsConversions + end + end +end diff --git a/service/lib/agama/storage/proposal_settings_conversion/from_schema.rb b/service/lib/agama/storage/proposal_settings_conversions/from_json.rb similarity index 71% rename from service/lib/agama/storage/proposal_settings_conversion/from_schema.rb rename to service/lib/agama/storage/proposal_settings_conversions/from_json.rb index 6db366f4d7..6cd1a0b872 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/from_schema.rb +++ b/service/lib/agama/storage/proposal_settings_conversions/from_json.rb @@ -24,20 +24,19 @@ require "agama/storage/encryption_settings" require "agama/storage/proposal_settings_reader" require "agama/storage/space_settings" -require "agama/storage/volume_conversion" +require "agama/storage/volume" require "y2storage/encryption_method" require "y2storage/pbkd_function" module Agama module Storage - module ProposalSettingsConversion - # Proposal settings conversion from Hash according to the JSON schema. - class FromSchema - # @param schema_settings [Hash] + module ProposalSettingsConversions + # Proposal settings conversion from JSON hash according to schema. + class FromJSON + # @param settings_json [Hash] # @param config [Config] - def initialize(schema_settings, config:) - # @todo Raise error if schema_settings does not match the JSON schema. - @schema_settings = schema_settings + def initialize(settings_json, config:) + @settings_json = settings_json @config = config end @@ -45,6 +44,7 @@ def initialize(schema_settings, config:) # # @return [ProposalSettings] def convert + # @todo Raise error if settings_json does not match the JSON schema. device_settings = target_conversion boot_settings = boot_conversion encryption_settings = encryption_conversion @@ -63,49 +63,49 @@ def convert private # @return [Hash] - attr_reader :schema_settings + attr_reader :settings_json # @return [Config] attr_reader :config def target_conversion - target_schema = schema_settings[:target] - return unless target_schema + target_json = settings_json[:target] + return unless target_json - if target_schema == "disk" + if target_json == "disk" Agama::Storage::DeviceSettings::Disk.new - elsif target_schema == "newLvmVg" + elsif target_json == "newLvmVg" Agama::Storage::DeviceSettings::NewLvmVg.new - elsif (device = target_schema[:disk]) + elsif (device = target_json[:disk]) Agama::Storage::DeviceSettings::Disk.new(device) - elsif (devices = target_schema[:newLvmVg]) + elsif (devices = target_json[:newLvmVg]) Agama::Storage::DeviceSettings::NewLvmVg.new(devices) end end def boot_conversion - boot_schema = schema_settings[:boot] - return unless boot_schema + boot_json = settings_json[:boot] + return unless boot_json Agama::Storage::BootSettings.new.tap do |boot_settings| - boot_settings.configure = boot_schema[:configure] - boot_settings.device = boot_schema[:device] + boot_settings.configure = boot_json[:configure] + boot_settings.device = boot_json[:device] end end def encryption_conversion - encryption_schema = schema_settings[:encryption] - return unless encryption_schema + encryption_json = settings_json[:encryption] + return unless encryption_json Agama::Storage::EncryptionSettings.new.tap do |encryption_settings| - encryption_settings.password = encryption_schema[:password] + encryption_settings.password = encryption_json[:password] - if (method_value = encryption_schema[:method]) + if (method_value = encryption_json[:method]) method = Y2Storage::EncryptionMethod.find(method_value.to_sym) encryption_settings.method = method end - if (function_value = encryption_schema[:pbkdFunction]) + if (function_value = encryption_json[:pbkdFunction]) function = Y2Storage::PbkdFunction.find(function_value) encryption_settings.pbkd_function = function end @@ -113,13 +113,13 @@ def encryption_conversion end def space_conversion - space_schema = schema_settings[:space] - return unless space_schema + space_json = settings_json[:space] + return unless space_json Agama::Storage::SpaceSettings.new.tap do |space_settings| - space_settings.policy = space_schema[:policy].to_sym + space_settings.policy = space_json[:policy].to_sym - actions_value = space_schema[:actions] || [] + actions_value = space_json[:actions] || [] space_settings.actions = actions_value.map { |a| action_conversion(a) }.inject(:merge) end end @@ -132,11 +132,11 @@ def action_conversion(action) end def volumes_conversion - volumes_schema = schema_settings[:volumes] - return [] unless volumes_schema + volumes_json = settings_json[:volumes] + return [] unless volumes_json - volumes_schema.map do |volume_schema| - VolumeConversion.from_schema(volume_schema, config: config) + volumes_json.map do |volume_json| + Volume.new_from_json(volume_json, config: config) end end diff --git a/service/lib/agama/storage/proposal_settings_conversion/to_schema.rb b/service/lib/agama/storage/proposal_settings_conversions/to_json.rb similarity index 80% rename from service/lib/agama/storage/proposal_settings_conversion/to_schema.rb rename to service/lib/agama/storage/proposal_settings_conversions/to_json.rb index 071b1e424a..9eee973d39 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/to_schema.rb +++ b/service/lib/agama/storage/proposal_settings_conversions/to_json.rb @@ -20,19 +20,18 @@ # find current contact information at www.suse.com. require "agama/storage/device_settings" -require "agama/storage/volume_conversion" module Agama module Storage - module ProposalSettingsConversion - # Proposal settings conversion according to the JSON schema. - class ToSchema + module ProposalSettingsConversions + # Proposal settings conversion to JSON hash according to schema. + class ToJSON # @param settings [ProposalSettings] def initialize(settings) @settings = settings end - # Performs the conversion according to the JSON schema. + # Performs the conversion to JSON. # # @return [Hash] def convert @@ -41,9 +40,9 @@ def convert boot: boot_conversion, space: space_conversion, volumes: volumes_conversion - }.tap do |schema| - encryption_schema = encryption_conversion - schema[:encryption] = encryption_schema if encryption_schema + }.tap do |settings_json| + encryption_json = encryption_conversion + settings_json[:encryption] = encryption_json if encryption_json end end @@ -68,9 +67,9 @@ def target_conversion def boot_conversion { configure: settings.boot.configure? - }.tap do |schema| + }.tap do |boot_json| device = settings.boot.device - schema[:device] = device if device + boot_json[:device] = device if device end end @@ -80,9 +79,9 @@ def encryption_conversion { password: settings.encryption.password, method: settings.encryption.method.id.to_s - }.tap do |schema| + }.tap do |encryption_json| function = settings.encryption.pbkd_function - schema[:pbkdFunction] = function.value if function + encryption_json[:pbkdFunction] = function.value if function end end @@ -100,7 +99,7 @@ def action_key(action) end def volumes_conversion - settings.volumes.map { |v| VolumeConversion.to_schema(v) } + settings.volumes.map(&:to_json_settings) end end end diff --git a/service/lib/agama/storage/volume.rb b/service/lib/agama/storage/volume.rb index 2ff73300d7..76b4a39417 100644 --- a/service/lib/agama/storage/volume.rb +++ b/service/lib/agama/storage/volume.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -20,10 +20,12 @@ # find current contact information at www.suse.com. require "forwardable" +require "json" require "y2storage/disk_size" require "agama/storage/btrfs_settings" -require "agama/storage/volume_outline" +require "agama/storage/volume_conversions" require "agama/storage/volume_location" +require "agama/storage/volume_outline" module Agama module Storage @@ -111,6 +113,23 @@ def auto_size_supported? # for the snapshots but does not allow to enable them). outline.adaptive_sizes? end + + # Creates a new volume object from a JSON hash according to schema. + # + # @param volume_json [Hash] + # @param config [Config] + # + # @return [Volume] + def self.new_from_json(volume_json, config:) + Storage::VolumeConversions::FromJSON.new(volume_json, config: config).convert + end + + # Generates a JSON hash according to schema. + # + # @return [Hash] + def to_json_settings + Storage::VolumeConversions::ToJSON.new(self).convert + end end end end diff --git a/service/lib/agama/storage/volume_conversion.rb b/service/lib/agama/storage/volume_conversion.rb index 9b393fa4e8..432f39034f 100644 --- a/service/lib/agama/storage/volume_conversion.rb +++ b/service/lib/agama/storage/volume_conversion.rb @@ -19,9 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/volume_conversion/from_schema" require "agama/storage/volume_conversion/from_y2storage" -require "agama/storage/volume_conversion/to_schema" require "agama/storage/volume_conversion/to_y2storage" module Agama @@ -43,24 +41,6 @@ def self.from_y2storage(volume) def self.to_y2storage(volume) ToY2Storage.new(volume).convert end - - # Performs conversion from Hash according to the JSON schema. - # - # @param volume_schema [Hash] - # @param config [Agama::Config] - # - # @return [Agama::Storage::Volume] - def self.from_schema(volume_schema, config:) - FromSchema.new(volume_schema, config: config).convert - end - - # Performs conversion according to the JSON schema. - # - # @param volume [Agama::Storage::Volume] - # @return [Hash] - def self.to_schema(volume) - ToSchema.new(volume).convert - end end end end diff --git a/service/lib/agama/storage/volume_conversions.rb b/service/lib/agama/storage/volume_conversions.rb new file mode 100644 index 0000000000..3c37c4d329 --- /dev/null +++ b/service/lib/agama/storage/volume_conversions.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/volume_conversions/from_json" +require "agama/storage/volume_conversions/to_json" + +module Agama + module Storage + # Conversions for a volume + module VolumeConversions + end + end +end diff --git a/service/lib/agama/storage/volume_conversion/from_schema.rb b/service/lib/agama/storage/volume_conversions/from_json.rb similarity index 65% rename from service/lib/agama/storage/volume_conversion/from_schema.rb rename to service/lib/agama/storage/volume_conversions/from_json.rb index f792314576..a400d250a2 100644 --- a/service/lib/agama/storage/volume_conversion/from_schema.rb +++ b/service/lib/agama/storage/volume_conversions/from_json.rb @@ -26,21 +26,22 @@ module Agama module Storage - module VolumeConversion - # Volume conversion from Hash according to the JSON schema. - class FromSchema - # @param volume_schema [Hash] + module VolumeConversions + # Volume conversion from JSON hash according schema. + class FromJSON + # @param volume_json [Hash] # @param config [Config] - def initialize(volume_schema, config:) - # @todo Raise error if volume_schema does not match the JSON schema. - @volume_schema = volume_schema + def initialize(volume_json, config:) + @volume_json = volume_json @config = config end - # Performs the conversion from Hash according to the JSON schema. + # Performs the conversion from JSON Hash according to schema. # # @return [Volume] def convert + # @todo Raise error if volume_json does not match the JSON schema. + default_volume.tap do |volume| mount_conversion(volume) filesystem_conversion(volume) @@ -52,15 +53,15 @@ def convert private # @return [Hash] - attr_reader :volume_schema + attr_reader :volume_json # @return [Agama::Config] attr_reader :config # @param volume [Volume] def mount_conversion(volume) - path_value = volume_schema.dig(:mount, :path) - options_value = volume_schema.dig(:mount, :options) + path_value = volume_json.dig(:mount, :path) + options_value = volume_json.dig(:mount, :options) volume.mount_path = path_value volume.mount_options = options_value if options_value @@ -68,31 +69,31 @@ def mount_conversion(volume) # @param volume [Volume] def filesystem_conversion(volume) - filesystem_schema = volume_schema[:filesystem] - return unless filesystem_schema + filesystem_json = volume_json[:filesystem] + return unless filesystem_json - if filesystem_schema.is_a?(String) - filesystem_string_conversion(volume, filesystem_schema) + if filesystem_json.is_a?(String) + filesystem_string_conversion(volume, filesystem_json) else - filesystem_hash_conversion(volume, filesystem_schema) + filesystem_hash_conversion(volume, filesystem_json) end end # @param volume [Volume] - # @param filesystem [String] - def filesystem_string_conversion(volume, filesystem) + # @param filesystem_json [String] + def filesystem_string_conversion(volume, filesystem_json) filesystems = volume.outline.filesystems - fs_type = filesystems.find { |t| t.to_s == filesystem } + fs_type = filesystems.find { |t| t.to_s == filesystem_json } volume.fs_type = fs_type if fs_type end # @param volume [Volume] - # @param filesystem [Hash] - def filesystem_hash_conversion(volume, filesystem) + # @param filesystem_json [Hash] + def filesystem_hash_conversion(volume, filesystem_json) filesystem_string_conversion(volume, "btrfs") - snapshots_value = filesystem.dig(:btrfs, :snapshots) + snapshots_value = filesystem_json.dig(:btrfs, :snapshots) return if !volume.outline.snapshots_configurable? || snapshots_value.nil? volume.btrfs.snapshots = snapshots_value @@ -101,16 +102,16 @@ def filesystem_hash_conversion(volume, filesystem) # @todo Support array format ([min, max]) and string format ("2 GiB") # @param volume [Volume] def size_conversion(volume) - size_schema = volume_schema[:size] - return unless size_schema + size_json = volume_json[:size] + return unless size_json - if size_schema == "auto" + if size_json == "auto" volume.auto_size = true if volume.auto_size_supported? else volume.auto_size = false - min_value = size_schema[:min] - max_value = size_schema[:max] + min_value = size_json[:min] + max_value = size_json[:max] volume.min_size = Y2Storage::DiskSize.new(min_value) volume.max_size = if max_value @@ -122,22 +123,22 @@ def size_conversion(volume) end def target_conversion(volume) - target_schema = volume_schema[:target] - return unless target_schema + target_json = volume_json[:target] + return unless target_json - if target_schema == "default" + if target_json == "default" volume.location.target = :default volume.location.device = nil - elsif (device = target_schema[:newPartition]) + elsif (device = target_json[:newPartition]) volume.location.target = :new_partition volume.location.device = device - elsif (device = target_schema[:newVg]) + elsif (device = target_json[:newVg]) volume.location.target = :new_vg volume.location.device = device - elsif (device = target_schema[:device]) + elsif (device = target_json[:device]) volume.location.target = :device volume.location.device = device - elsif (device = target_schema[:filesystem]) + elsif (device = target_json[:filesystem]) volume.location.target = :filesystem volume.location.device = device end @@ -146,7 +147,7 @@ def target_conversion(volume) def default_volume Agama::Storage::VolumeTemplatesBuilder .new_from_config(config) - .for(volume_schema.dig(:mount, :path)) + .for(volume_json.dig(:mount, :path)) end end end diff --git a/service/lib/agama/storage/volume_conversion/to_schema.rb b/service/lib/agama/storage/volume_conversions/to_json.rb similarity index 88% rename from service/lib/agama/storage/volume_conversion/to_schema.rb rename to service/lib/agama/storage/volume_conversions/to_json.rb index 0ba7e8b1ff..4915ae68c0 100644 --- a/service/lib/agama/storage/volume_conversion/to_schema.rb +++ b/service/lib/agama/storage/volume_conversions/to_json.rb @@ -25,15 +25,15 @@ module Agama module Storage - module VolumeConversion - # Volume conversion according to the JSON schema. - class ToSchema + module VolumeConversions + # Volume conversion to JSON hash according to schema. + class ToJSON # @param volume [Volume] def initialize(volume) @volume = volume end - # Performs the conversion according to the JSON schema. + # Performs the conversion to JSON. # # @return [Hash] def convert @@ -41,9 +41,9 @@ def convert mount: mount_conversion, size: size_conversion, target: target_conversion - }.tap do |schema| - filesystem_schema = filesystem_conversion - schema[:filesystem] = filesystem_schema if filesystem_schema + }.tap do |volume_json| + filesystem_json = filesystem_conversion + volume_json[:filesystem] = filesystem_json if filesystem_json end end diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index 16a8896b03..cc81b29000 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -27,7 +27,6 @@ require "agama/storage/manager" require "agama/storage/proposal" require "agama/storage/proposal_settings" -require "agama/storage/proposal_settings_conversion" require "agama/storage/volume" require "agama/storage/iscsi/manager" require "agama/storage/dasd/manager" @@ -547,7 +546,7 @@ def pretty_json(value) it "returns serialized storage config including guided proposal settings" do expected_config = { storage: { - guided: Agama::Storage::ProposalSettingsConversion.to_schema(settings) + guided: settings.to_json_settings } } @@ -626,9 +625,7 @@ def pretty_json(value) it "returns a Hash with success, strategy and settings" do result = subject.proposal_result - serialized_settings = Agama::Storage::ProposalSettingsConversion - .to_schema(proposal.settings) - .to_json + serialized_settings = proposal.settings.to_json_settings.to_json expect(result.keys).to contain_exactly("success", "strategy", "settings") expect(result["success"]).to eq(true) diff --git a/service/test/agama/storage/proposal_settings_conversion_test.rb b/service/test/agama/storage/proposal_settings_conversion_test.rb index e57a03e257..dfb1db457a 100644 --- a/service/test/agama/storage/proposal_settings_conversion_test.rb +++ b/service/test/agama/storage/proposal_settings_conversion_test.rb @@ -20,7 +20,6 @@ # find current contact information at www.suse.com. require_relative "../../test_helper" -require "agama/config" require "agama/storage/proposal_settings" require "agama/storage/proposal_settings_conversion" require "y2storage" @@ -47,30 +46,4 @@ expect(result).to be_a(Y2Storage::ProposalSettings) end end - - describe "#from_schema" do - let(:config) { Agama::Config.new } - - let(:schema_settings) do - { - target: { - disk: "/dev/vda" - } - } - end - - it "generates proposal settings from settings according to the JSON schema" do - result = described_class.from_schema(schema_settings, config: config) - expect(result).to be_a(Agama::Storage::ProposalSettings) - end - end - - describe "#to_schema" do - let(:proposal_settings) { Agama::Storage::ProposalSettings.new } - - it "generates settings according to the JSON schema from the proposal settings" do - result = described_class.to_schema(proposal_settings) - expect(result).to be_a(Hash) - end - end end diff --git a/service/test/agama/storage/proposal_settings_conversion/from_schema_test.rb b/service/test/agama/storage/proposal_settings_conversions/from_json_test.rb similarity index 86% rename from service/test/agama/storage/proposal_settings_conversion/from_schema_test.rb rename to service/test/agama/storage/proposal_settings_conversions/from_json_test.rb index 14d7a89d91..3426264077 100644 --- a/service/test/agama/storage/proposal_settings_conversion/from_schema_test.rb +++ b/service/test/agama/storage/proposal_settings_conversions/from_json_test.rb @@ -20,15 +20,15 @@ # find current contact information at www.suse.com. require_relative "../../../test_helper" -require "agama/storage/proposal_settings_conversion/from_schema" +require "agama/storage/proposal_settings_conversions/from_json" require "agama/config" require "agama/storage/device_settings" require "agama/storage/proposal_settings" require "y2storage/encryption_method" require "y2storage/pbkd_function" -describe Agama::Storage::ProposalSettingsConversion::FromSchema do - subject { described_class.new(settings_schema, config: config) } +describe Agama::Storage::ProposalSettingsConversions::FromJSON do + subject { described_class.new(settings_json, config: config) } let(:config) { Agama::Config.new(config_data) } @@ -71,7 +71,7 @@ end describe "#convert" do - let(:settings_schema) do + let(:settings_json) do { target: { disk: "/dev/sda" @@ -107,7 +107,7 @@ } end - it "generates settings with the values provided from hash according to the JSON schema" do + it "generates settings with the values provided from JSON" do settings = subject.convert expect(settings).to be_a(Agama::Storage::ProposalSettings) @@ -127,8 +127,8 @@ ) end - context "when the hash settings is missing some values" do - let(:settings_schema) { {} } + context "when the JSON is missing some values" do + let(:settings_json) { {} } it "completes the missing values with default values from the config" do settings = subject.convert @@ -148,8 +148,8 @@ end end - context "when the hash settings does not indicate the target" do - let(:settings_schema) { {} } + context "when the JSON does not indicate the target" do + let(:settings_json) { {} } it "generates settings with disk target and without specific device" do settings = subject.convert @@ -159,8 +159,8 @@ end end - context "when the hash settings indicates disk target without device" do - let(:settings_schema) do + context "when the JSON indicates disk target without device" do + let(:settings_json) do { target: "disk" } @@ -174,8 +174,8 @@ end end - context "when the hash settings indicates disk target with a device" do - let(:settings_schema) do + context "when the JSON indicates disk target with a device" do + let(:settings_json) do { target: { disk: "/dev/vda" @@ -191,8 +191,8 @@ end end - context "when the hash settings indicates newLvmVg target without devices" do - let(:settings_schema) do + context "when the JSON indicates newLvmVg target without devices" do + let(:settings_json) do { target: "newLvmVg" } @@ -206,8 +206,8 @@ end end - context "when the hash settings indicates newLvmVg target with devices" do - let(:settings_schema) do + context "when the JSON indicates newLvmVg target with devices" do + let(:settings_json) do { target: { newLvmVg: ["/dev/vda", "/dev/vdb"] @@ -223,8 +223,8 @@ end end - context "when the hash settings does not indicate volumes" do - let(:settings_schema) { { volumes: [] } } + context "when the JSON does not indicate volumes" do + let(:settings_json) { { volumes: [] } } it "generates settings with the default volumes from config" do settings = subject.convert @@ -244,8 +244,8 @@ end end - context "when the hash settings does not contain a required volume" do - let(:settings_schema) do + context "when the JSON does not contain a required volume" do + let(:settings_json) do { volumes: [ { diff --git a/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversions/from_y2storage_test.rb similarity index 100% rename from service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb rename to service/test/agama/storage/proposal_settings_conversions/from_y2storage_test.rb diff --git a/service/test/agama/storage/proposal_settings_conversion/to_schema_test.rb b/service/test/agama/storage/proposal_settings_conversions/to_json_test.rb similarity index 91% rename from service/test/agama/storage/proposal_settings_conversion/to_schema_test.rb rename to service/test/agama/storage/proposal_settings_conversions/to_json_test.rb index 8eba7d5a45..fad93ce2d5 100644 --- a/service/test/agama/storage/proposal_settings_conversion/to_schema_test.rb +++ b/service/test/agama/storage/proposal_settings_conversions/to_json_test.rb @@ -20,14 +20,14 @@ # find current contact information at www.suse.com. require_relative "../../../test_helper" -require "agama/storage/proposal_settings_conversion/to_schema" +require "agama/storage/proposal_settings_conversions/to_json" require "agama/storage/device_settings" require "agama/storage/proposal_settings" require "agama/storage/volume" require "y2storage/encryption_method" require "y2storage/pbkd_function" -describe Agama::Storage::ProposalSettingsConversion::ToSchema do +describe Agama::Storage::ProposalSettingsConversions::ToJSON do let(:default_settings) { Agama::Storage::ProposalSettings.new } let(:custom_settings) do @@ -44,7 +44,7 @@ end describe "#convert" do - it "converts the settings to the proper hash according to the JSON schema" do + it "converts the settings to a JSON hash according to schema" do # @todo Check whether the result matches the JSON schema. expect(described_class.new(default_settings).convert).to eq( @@ -101,7 +101,7 @@ end end - it "converts the settings to the proper hash according to the JSON schema" do + it "converts the settings to a JSON hash according to schema" do expect(described_class.new(settings).convert).to eq( target: { newLvmVg: ["/dev/vda"] diff --git a/service/test/agama/storage/proposal_settings_conversion/to_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb similarity index 100% rename from service/test/agama/storage/proposal_settings_conversion/to_y2storage_test.rb rename to service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb diff --git a/service/test/agama/storage/proposal_settings_test.rb b/service/test/agama/storage/proposal_settings_test.rb index 82fcc58d8b..0e2aafcdad 100644 --- a/service/test/agama/storage/proposal_settings_test.rb +++ b/service/test/agama/storage/proposal_settings_test.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require_relative "../../test_helper" +require "agama/config" require "agama/storage/device_settings" require "agama/storage/proposal_settings" require "agama/storage/volume" @@ -170,4 +171,30 @@ include_examples "volume devices" end end + + describe ".new_from_json" do + let(:config) { Agama::Config.new } + + let(:settings_json) do + { + target: { + disk: "/dev/vda" + } + } + end + + it "generates a proposal settings from JSON according to schema" do + result = described_class.new_from_json(settings_json, config: config) + expect(result).to be_a(Agama::Storage::ProposalSettings) + end + end + + describe "#to_json_settings" do + let(:proposal_settings) { Agama::Storage::ProposalSettings.new } + + it "generates a JSON hash according to schema" do + result = proposal_settings.to_json_settings + expect(result).to be_a(Hash) + end + end end diff --git a/service/test/agama/storage/volume_conversion_test.rb b/service/test/agama/storage/volume_conversion_test.rb index af2d9175bf..b63866edf5 100644 --- a/service/test/agama/storage/volume_conversion_test.rb +++ b/service/test/agama/storage/volume_conversion_test.rb @@ -20,8 +20,6 @@ # find current contact information at www.suse.com. require_relative "../../test_helper" -require "agama/config" -require "agama/storage/proposal_settings" require "agama/storage/volume" require "agama/storage/volume_conversion" require "y2storage" @@ -44,30 +42,4 @@ expect(result).to be_a(Y2Storage::VolumeSpecification) end end - - describe "#from_schema" do - let(:config) { Agama::Config.new } - - let(:volume_schema) do - { - mount: { - path: "/test" - } - } - end - - it "generates a volume from settings according to the JSON schema" do - result = described_class.from_schema(volume_schema, config: config) - expect(result).to be_a(Agama::Storage::Volume) - end - end - - describe "#to_schema" do - let(:volume) { Agama::Storage::Volume.new("/test") } - - it "generates volume settings according to the JSON schema from a volume" do - result = described_class.to_schema(volume) - expect(result).to be_a(Hash) - end - end end diff --git a/service/test/agama/storage/volume_conversion/from_schema_test.rb b/service/test/agama/storage/volume_conversions/from_json_test.rb similarity index 83% rename from service/test/agama/storage/volume_conversion/from_schema_test.rb rename to service/test/agama/storage/volume_conversions/from_json_test.rb index 0a009e1257..579da8505b 100644 --- a/service/test/agama/storage/volume_conversion/from_schema_test.rb +++ b/service/test/agama/storage/volume_conversions/from_json_test.rb @@ -24,15 +24,15 @@ require "agama/config" require "agama/storage/volume" require "agama/storage/volume_templates_builder" -require "agama/storage/volume_conversion/from_schema" +require "agama/storage/volume_conversions/from_json" require "y2storage/disk_size" def default_volume(mount_path) Agama::Storage::VolumeTemplatesBuilder.new_from_config(config).for(mount_path) end -describe Agama::Storage::VolumeConversion::FromSchema do - subject { described_class.new(volume_schema, config: config) } +describe Agama::Storage::VolumeConversions::FromJSON do + subject { described_class.new(volume_json, config: config) } let(:config) { Agama::Config.new(config_data) } @@ -67,7 +67,7 @@ def default_volume(mount_path) end describe "#convert" do - let(:volume_schema) do + let(:volume_json) do { mount: { path: "/test", @@ -84,13 +84,13 @@ def default_volume(mount_path) } end - it "generates a volume with the expected outline from the config" do + it "generates a volume with the expected outline from JSON" do volume = subject.convert expect(volume.outline).to eq_outline(default_volume("/test").outline) end - it "generates a volume with the values provided from hash according to the JSON schema" do + it "generates a volume with the values provided from JSON" do volume = subject.convert expect(volume).to be_a(Agama::Storage::Volume) @@ -105,8 +105,8 @@ def default_volume(mount_path) expect(volume.btrfs.snapshots).to eq(false) end - context "when the hash settings is missing some values" do - let(:volume_schema) do + context "when the JSON is missing some values" do + let(:volume_json) do { mount: { path: "/test" @@ -129,8 +129,8 @@ def default_volume(mount_path) end end - context "when the hash settings does not indicate max size" do - let(:volume_schema) do + context "when the JSON does not indicate max size" do + let(:volume_json) do { mount: { path: "/test" @@ -148,7 +148,7 @@ def default_volume(mount_path) end end - context "when the hash settings indicates auto size for a supported volume" do + context "when the JSON indicates auto size for a supported volume" do let(:outline) do { "auto_size" => { @@ -157,7 +157,7 @@ def default_volume(mount_path) } end - let(:volume_schema) do + let(:volume_json) do { mount: { path: "/test" @@ -173,10 +173,10 @@ def default_volume(mount_path) end end - context "when the hash settings indicates auto size for an unsupported volume" do + context "when the JSON indicates auto size for an unsupported volume" do let(:outline) { {} } - let(:volume_schema) do + let(:volume_json) do { mount: { path: "/test" @@ -192,10 +192,10 @@ def default_volume(mount_path) end end - context "when the hash settings indicates a filesystem included in the outline" do + context "when the JSON indicates a filesystem included in the outline" do let(:outline) { { "filesystems" => ["btrfs", "ext4"] } } - let(:volume_schema) do + let(:volume_json) do { mount: { path: "/test" @@ -211,10 +211,10 @@ def default_volume(mount_path) end end - context "when the hash settings indicates a filesystem not included in the outline" do + context "when the JSON indicates a filesystem not included in the outline" do let(:outline) { { "filesystems" => ["btrfs"] } } - let(:volume_schema) do + let(:volume_json) do { mount: { path: "/test" @@ -230,10 +230,10 @@ def default_volume(mount_path) end end - context "when the hash settings indicates snapshots for a supported volume" do + context "when the JSON indicates snapshots for a supported volume" do let(:outline) { { "snapshots_configurable" => true } } - let(:volume_schema) do + let(:volume_json) do { mount: { path: "/test" @@ -253,10 +253,10 @@ def default_volume(mount_path) end end - context "when the D-Bus settings provide Snapshots for an unsupported volume" do + context "when the JSON indicates snapshots for an unsupported volume" do let(:outline) { { "snapshots_configurable" => false } } - let(:volume_schema) do + let(:volume_json) do { mount: { path: "/test" diff --git a/service/test/agama/storage/volume_conversion/from_y2storage_test.rb b/service/test/agama/storage/volume_conversions/from_y2storage_test.rb similarity index 100% rename from service/test/agama/storage/volume_conversion/from_y2storage_test.rb rename to service/test/agama/storage/volume_conversions/from_y2storage_test.rb diff --git a/service/test/agama/storage/volume_conversion/to_schema_test.rb b/service/test/agama/storage/volume_conversions/to_json_test.rb similarity index 93% rename from service/test/agama/storage/volume_conversion/to_schema_test.rb rename to service/test/agama/storage/volume_conversions/to_json_test.rb index a3e757678e..11cb600328 100644 --- a/service/test/agama/storage/volume_conversion/to_schema_test.rb +++ b/service/test/agama/storage/volume_conversions/to_json_test.rb @@ -20,12 +20,12 @@ # find current contact information at www.suse.com. require_relative "../../../test_helper" -require "agama/storage/volume_conversion/to_schema" +require "agama/storage/volume_conversions/to_json" require "agama/storage/volume" require "y2storage/filesystems/type" require "y2storage/disk_size" -describe Agama::Storage::VolumeConversion::ToSchema do +describe Agama::Storage::VolumeConversions::ToJSON do let(:default_volume) { Agama::Storage::Volume.new("/test") } let(:custom_volume1) do @@ -48,7 +48,7 @@ end describe "#convert" do - it "converts the volume to the proper hash according to the JSON schema" do + it "converts the volume to a JSON hash according to schema" do # @todo Check whether the result matches the JSON schema. expect(described_class.new(default_volume).convert).to eq( diff --git a/service/test/agama/storage/volume_conversion/to_y2storage_test.rb b/service/test/agama/storage/volume_conversions/to_y2storage_test.rb similarity index 100% rename from service/test/agama/storage/volume_conversion/to_y2storage_test.rb rename to service/test/agama/storage/volume_conversions/to_y2storage_test.rb diff --git a/service/test/agama/storage/volume_test.rb b/service/test/agama/storage/volume_test.rb new file mode 100644 index 0000000000..f0e4ae8a7d --- /dev/null +++ b/service/test/agama/storage/volume_test.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "agama/config" +require "agama/storage/volume" + +describe Agama::Storage::Volume do + describe ".new_from_json" do + let(:config) { Agama::Config.new } + + let(:volume_json) do + { + mount: { + path: "/test" + } + } + end + + it "generates a volume from JSON according to schema" do + result = described_class.new_from_json(volume_json, config: config) + expect(result).to be_a(Agama::Storage::Volume) + expect(result.mount_path).to eq("/test") + end + end + + describe "#to_json_settngs" do + let(:volume) { Agama::Storage::Volume.new("/test") } + + it "generates a JSON hash according to schema" do + result = volume.to_json_settings + expect(result).to be_a(Hash) + end + end +end From 9f24154e190b0abf407eb6224c7dc900a13ae9bd Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 28 Jun 2024 08:06:57 +0200 Subject: [PATCH 032/430] Use gzip (.gz) instead of bzip2 (.bz2) to compress logs (gh#openSUSE/agama#1378) so that they can be attached to GitHub issues. Note that bzip2 is still used for source tarballs and PO tarballs at *development* time, but AFAIK no longer used at *run* time, so the dependency is kept in .github/workflows but not in .spec --- rust/agama-cli/src/logs.rs | 2 +- rust/agama-server/src/manager/web.rs | 2 +- rust/package/agama.spec | 2 +- setup-services.sh | 2 +- web/src/components/core/LogsButton.jsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/agama-cli/src/logs.rs b/rust/agama-cli/src/logs.rs index 8642d1ae63..786ed144d5 100644 --- a/rust/agama-cli/src/logs.rs +++ b/rust/agama-cli/src/logs.rs @@ -115,7 +115,7 @@ const DEFAULT_PATHS: [&str; 14] = [ const DEFAULT_RESULT: &str = "/tmp/agama-logs"; // what compression is used by default: // (, ) -const DEFAULT_COMPRESSION: (&str, &str) = ("bzip2", "tar.bz2"); +const DEFAULT_COMPRESSION: (&str, &str) = ("gzip", "tar.gz"); const TMP_DIR_PREFIX: &str = "agama-logs."; /// A wrapper around println which shows (or not) the text depending on the boolean variable diff --git a/rust/agama-server/src/manager/web.rs b/rust/agama-server/src/manager/web.rs index bdfac34c0d..4dc7e352e8 100644 --- a/rust/agama-server/src/manager/web.rs +++ b/rust/agama-server/src/manager/web.rs @@ -218,6 +218,6 @@ async fn generate_logs() -> Result { .status() .map_err(|e| ServiceError::CannotGenerateLogs(e.to_string()))?; - let full_path = format!("{path}.tar.bz2"); + let full_path = format!("{path}.tar.gz"); Ok(full_path) } diff --git a/rust/package/agama.spec b/rust/package/agama.spec index 5c0ad022a2..3d7d5292b5 100644 --- a/rust/package/agama.spec +++ b/rust/package/agama.spec @@ -40,7 +40,7 @@ BuildRequires: pkgconfig(pam) Requires: jsonnet Requires: lshw # required by "agama logs store" -Requires: bzip2 +Requires: gzip Requires: tar # required for translating the keyboards descriptions BuildRequires: xkeyboard-config-lang diff --git a/setup-services.sh b/setup-services.sh index 4c6e291238..5a3127bf3c 100755 --- a/setup-services.sh +++ b/setup-services.sh @@ -124,8 +124,8 @@ which cargo || $SUDO zypper --non-interactive install cargo # Packages required by Rust code (see ./rust/package/agama.spec) $SUDO zypper --non-interactive install \ - bzip2 \ clang-devel \ + gzip \ jsonnet \ lshw \ pam-devel \ diff --git a/web/src/components/core/LogsButton.jsx b/web/src/components/core/LogsButton.jsx index 5034198a55..a6bfb8f16b 100644 --- a/web/src/components/core/LogsButton.jsx +++ b/web/src/components/core/LogsButton.jsx @@ -27,7 +27,7 @@ import { Alert, Button } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { _ } from "~/i18n"; -const FILENAME = "agama-installation-logs.tar.bzip2"; +const FILENAME = "agama-installation-logs.tar.gz"; /** * Button for collecting and downloading YaST logs From 87636badb505598209e66d3090d8e9ef34db6fd8 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 28 Jun 2024 08:20:24 +0200 Subject: [PATCH 033/430] chore: help ripgrep (rg) search also in .github/workflows --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c52190d24d..ea097847f6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ /*.pot *.mo *.bz2 +# Do NOT ignore .github: for git this is a no-op +# but it helps ripgrep (rg) which would otherwise ignore dotfiles and dotdirs +!/.github/ From 0b7ccea65c457f0010036d10149db7dc0684528e Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 28 Jun 2024 08:58:24 +0200 Subject: [PATCH 034/430] chore: update .changes --- rust/package/agama.changes | 7 +++++++ web/package/agama-web-ui.changes | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 472902e50f..1af0a29218 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Fri Jun 28 06:56:02 UTC 2024 - Martin Vidner + +- Use gzip (.gz) instead of bzip2 (.bz2) to compress logs + so that they can be attached to GitHub issues + (gh#openSUSE/agama#1378) + ------------------------------------------------------------------- Thu Jun 27 13:22:51 UTC 2024 - Imobach Gonzalez Sosa diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index ca839203ab..feae42319d 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Fri Jun 28 06:56:02 UTC 2024 - Martin Vidner + +- Use gzip (.gz) instead of bzip2 (.bz2) to compress logs + so that they can be attached to GitHub issues + (gh#openSUSE/agama#1378) + ------------------------------------------------------------------- Thu Jun 27 13:23:08 UTC 2024 - Imobach Gonzalez Sosa From a3bc41ec2380883b2fef2f0d88661ab1bcc0d2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 28 Jun 2024 07:28:33 +0100 Subject: [PATCH 035/430] storage: move Y2Storage conversions --- .../lib/agama/storage/proposal_settings.rb | 8 +++ .../storage/proposal_settings_conversion.rb | 50 ------------------- .../storage/proposal_settings_conversions.rb | 2 + .../from_y2storage.rb | 8 +-- .../to_y2storage.rb | 7 ++- .../storage/proposal_strategies/autoyast.rb | 3 +- .../storage/proposal_strategies/guided.rb | 10 ++-- service/lib/agama/storage/volume.rb | 10 ++-- .../lib/agama/storage/volume_conversion.rb | 46 ----------------- .../lib/agama/storage/volume_conversions.rb | 2 + .../from_y2storage.rb | 2 +- .../to_y2storage.rb | 2 +- .../proposal_settings_conversion_test.rb | 49 ------------------ .../from_y2storage_test.rb | 4 +- .../to_y2storage_test.rb | 4 +- .../agama/storage/proposal_settings_test.rb | 12 +++++ .../agama/storage/volume_conversion_test.rb | 45 ----------------- .../volume_conversions/from_y2storage_test.rb | 4 +- .../volume_conversions/to_y2storage_test.rb | 4 +- service/test/agama/storage/volume_test.rb | 10 ++++ 20 files changed, 65 insertions(+), 217 deletions(-) delete mode 100644 service/lib/agama/storage/proposal_settings_conversion.rb rename service/lib/agama/storage/{proposal_settings_conversion => proposal_settings_conversions}/from_y2storage.rb (93%) rename service/lib/agama/storage/{proposal_settings_conversion => proposal_settings_conversions}/to_y2storage.rb (96%) delete mode 100644 service/lib/agama/storage/volume_conversion.rb rename service/lib/agama/storage/{volume_conversion => volume_conversions}/from_y2storage.rb (99%) rename service/lib/agama/storage/{volume_conversion => volume_conversions}/to_y2storage.rb (99%) delete mode 100644 service/test/agama/storage/proposal_settings_conversion_test.rb delete mode 100644 service/test/agama/storage/volume_conversion_test.rb diff --git a/service/lib/agama/storage/proposal_settings.rb b/service/lib/agama/storage/proposal_settings.rb index cbf3d297a8..8ec12201ae 100644 --- a/service/lib/agama/storage/proposal_settings.rb +++ b/service/lib/agama/storage/proposal_settings.rb @@ -103,6 +103,14 @@ def to_json_settings Storage::ProposalSettingsConversions::ToJSON.new(self).convert end + # Generates Y2Storage proposal settings. + # + # @param config [Config] + # @return [Y2Storage::ProposalSettings] + def to_y2storage(config:) + Storage::ProposalSettingsConversions::ToY2Storage.new(self, config: config).convert + end + private # Device used for booting. diff --git a/service/lib/agama/storage/proposal_settings_conversion.rb b/service/lib/agama/storage/proposal_settings_conversion.rb deleted file mode 100644 index f8439c7531..0000000000 --- a/service/lib/agama/storage/proposal_settings_conversion.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -# 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 version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/proposal_settings_conversion/from_y2storage" -require "agama/storage/proposal_settings_conversion/to_y2storage" - -module Agama - module Storage - # Conversions for the proposal settings. - module ProposalSettingsConversion - # Performs conversion from Y2Storage. - # - # @param y2storage_settings [Y2Storage::ProposalSettings] - # @param settings [Agama::Storage::ProposalSettings] - # - # @return [Agama::Storage::ProposalSettings] - def self.from_y2storage(y2storage_settings, settings) - FromY2Storage.new(y2storage_settings, settings).convert - end - - # Performs conversion to Y2Storage. - # - # @param settings [Agama::Storage::ProposalSettings] - # @param config [Agama::Config] - # - # @return [Y2Storage::ProposalSettings] - def self.to_y2storage(settings, config:) - ToY2Storage.new(settings, config: config).convert - end - end - end -end diff --git a/service/lib/agama/storage/proposal_settings_conversions.rb b/service/lib/agama/storage/proposal_settings_conversions.rb index 6ad03db45f..11517c4b58 100644 --- a/service/lib/agama/storage/proposal_settings_conversions.rb +++ b/service/lib/agama/storage/proposal_settings_conversions.rb @@ -20,7 +20,9 @@ # find current contact information at www.suse.com. require "agama/storage/proposal_settings_conversions/from_json" +require "agama/storage/proposal_settings_conversions/from_y2storage" require "agama/storage/proposal_settings_conversions/to_json" +require "agama/storage/proposal_settings_conversions/to_y2storage" module Agama module Storage diff --git a/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb b/service/lib/agama/storage/proposal_settings_conversions/from_y2storage.rb similarity index 93% rename from service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb rename to service/lib/agama/storage/proposal_settings_conversions/from_y2storage.rb index 475e53e25c..4a5f26983f 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb +++ b/service/lib/agama/storage/proposal_settings_conversions/from_y2storage.rb @@ -19,11 +19,11 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "agama/storage/volume_conversion" +require "agama/storage/volume_conversions/from_y2storage" module Agama module Storage - module ProposalSettingsConversion + module ProposalSettingsConversions # Proposal settings conversion from Y2Storage. # # @note This class does not perform a real conversion from Y2Storage settings. Instead of @@ -60,7 +60,7 @@ def convert # Recovers space actions. # # @note Space actions are generated in the conversion of the settings to Y2Storage format, - # see {ProposalSettingsConversion::ToY2Storage}. + # see {ProposalSettingsConversions::ToY2Storage}. # # @param target [Agama::Storage::ProposalSettings] def space_actions_conversion(target) @@ -77,7 +77,7 @@ def volumes_conversion(target) # @param volume [Agama::Storage::Volume] # @return [Agama::Storage::Volume] def volume_conversion(volume) - VolumeConversion.from_y2storage(volume) + VolumeConversions::FromY2Storage.new(volume).convert end end end diff --git a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb b/service/lib/agama/storage/proposal_settings_conversions/to_y2storage.rb similarity index 96% rename from service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb rename to service/lib/agama/storage/proposal_settings_conversions/to_y2storage.rb index eacee97742..72572e491a 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb +++ b/service/lib/agama/storage/proposal_settings_conversions/to_y2storage.rb @@ -21,12 +21,11 @@ require "y2storage" require "agama/storage/device_settings" -require "agama/storage/volume_conversion" require "agama/storage/volume_templates_builder" module Agama module Storage - module ProposalSettingsConversion + module ProposalSettingsConversions # Proposal settings conversion to Y2Storage. class ToY2Storage # @param settings [Agama::Storage::ProposalSettings] @@ -142,10 +141,10 @@ def space_policy_conversion(target) def volumes_conversion(target) target.swap_reuse = :none - volumes = settings.volumes.map { |v| VolumeConversion.to_y2storage(v) } + volumes = settings.volumes.map(&:to_y2storage) disabled_volumes = missing_volumes.map do |volume| - VolumeConversion.to_y2storage(volume).tap { |v| v.proposed = false } + volume.to_y2storage.tap { |v| v.proposed = false } end target.volumes = volumes + disabled_volumes diff --git a/service/lib/agama/storage/proposal_strategies/autoyast.rb b/service/lib/agama/storage/proposal_strategies/autoyast.rb index 832e1b1eb2..e076672095 100644 --- a/service/lib/agama/storage/proposal_strategies/autoyast.rb +++ b/service/lib/agama/storage/proposal_strategies/autoyast.rb @@ -22,7 +22,6 @@ require "agama/storage/proposal_strategies/base" require "agama/storage/proposal_settings" require "agama/storage/proposal_settings_reader" -require "agama/storage/proposal_settings_conversion" module Agama module Storage @@ -78,7 +77,7 @@ def issues # @return [Y2Storage::ProposalSettings] def proposal_settings agama_default = ProposalSettingsReader.new(config).read - ProposalSettingsConversion.to_y2storage(agama_default, config: config) + agama_default.to_y2storage(config: config) end # Agama issue equivalent to the given AutoYaST issue diff --git a/service/lib/agama/storage/proposal_strategies/guided.rb b/service/lib/agama/storage/proposal_strategies/guided.rb index 105b86d46d..2137b033ea 100644 --- a/service/lib/agama/storage/proposal_strategies/guided.rb +++ b/service/lib/agama/storage/proposal_strategies/guided.rb @@ -21,7 +21,7 @@ require "agama/storage/proposal_strategies/base" require "agama/storage/device_settings" -require "agama/storage/proposal_settings_conversion" +require "agama/storage/proposal_settings_conversions/from_y2storage" module Agama module Storage @@ -43,7 +43,7 @@ def initialize(config, logger, input_settings) # Settings used for calculating the proposal. # # @note Some values are recoverd from Y2Storage, see - # {ProposalSettingsConversion::FromY2Storage} + # {ProposalSettingsConversions::FromY2Storage} # # @return [ProposalSettings] attr_reader :settings @@ -55,7 +55,9 @@ def calculate proposal.propose ensure storage_manager.proposal = proposal - @settings = ProposalSettingsConversion.from_y2storage(proposal.settings, input_settings) + @settings = ProposalSettingsConversions::FromY2Storage + .new(proposal.settings, input_settings) + .convert end # @see Base#issues @@ -107,7 +109,7 @@ def missing_target_device?(settings) # @return [Y2Storage::GuidedProposal] def guided_proposal(settings) Y2Storage::MinGuidedProposal.new( - settings: ProposalSettingsConversion.to_y2storage(settings, config: config), + settings: settings.to_y2storage(config: config), devicegraph: probed_devicegraph, disk_analyzer: disk_analyzer ) diff --git a/service/lib/agama/storage/volume.rb b/service/lib/agama/storage/volume.rb index 76b4a39417..023c4df78a 100644 --- a/service/lib/agama/storage/volume.rb +++ b/service/lib/agama/storage/volume.rb @@ -30,9 +30,6 @@ module Agama module Storage # A volume represents the characteristics of a file system to create in the system. - # - # A volume is converted to D-Bus and to Y2Storage formats in order to provide the volume - # information with the expected representation, see {VolumeConversion}. class Volume extend Forwardable @@ -130,6 +127,13 @@ def self.new_from_json(volume_json, config:) def to_json_settings Storage::VolumeConversions::ToJSON.new(self).convert end + + # Generates a Y2Storage volume. + # + # @return [Y2Storage::VolumeSpecification] + def to_y2storage + Storage::VolumeConversions::ToY2Storage.new(self).convert + end end end end diff --git a/service/lib/agama/storage/volume_conversion.rb b/service/lib/agama/storage/volume_conversion.rb deleted file mode 100644 index 432f39034f..0000000000 --- a/service/lib/agama/storage/volume_conversion.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -# 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 version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/volume_conversion/from_y2storage" -require "agama/storage/volume_conversion/to_y2storage" - -module Agama - module Storage - # Conversions for a volume - module VolumeConversion - # Performs conversion from Y2Storage. - # - # @param volume [Agama::Storage::Volume] - # @return [Agama::Storage::Volume] - def self.from_y2storage(volume) - FromY2Storage.new(volume).convert - end - - # Performs conversion to Y2Storage. - # - # @param volume [Agama::Storage::Volume] - # @return [Y2Storage::VolumeSpecification] - def self.to_y2storage(volume) - ToY2Storage.new(volume).convert - end - end - end -end diff --git a/service/lib/agama/storage/volume_conversions.rb b/service/lib/agama/storage/volume_conversions.rb index 3c37c4d329..e8a2518a78 100644 --- a/service/lib/agama/storage/volume_conversions.rb +++ b/service/lib/agama/storage/volume_conversions.rb @@ -20,7 +20,9 @@ # find current contact information at www.suse.com. require "agama/storage/volume_conversions/from_json" +require "agama/storage/volume_conversions/from_y2storage" require "agama/storage/volume_conversions/to_json" +require "agama/storage/volume_conversions/to_y2storage" module Agama module Storage diff --git a/service/lib/agama/storage/volume_conversion/from_y2storage.rb b/service/lib/agama/storage/volume_conversions/from_y2storage.rb similarity index 99% rename from service/lib/agama/storage/volume_conversion/from_y2storage.rb rename to service/lib/agama/storage/volume_conversions/from_y2storage.rb index 0c493fe707..0ad025eb1b 100644 --- a/service/lib/agama/storage/volume_conversion/from_y2storage.rb +++ b/service/lib/agama/storage/volume_conversions/from_y2storage.rb @@ -23,7 +23,7 @@ module Agama module Storage - module VolumeConversion + module VolumeConversions # Volume conversion from Y2Storage. # # @note This class does not perform a real conversion from Y2Storage. Instead of that, it diff --git a/service/lib/agama/storage/volume_conversion/to_y2storage.rb b/service/lib/agama/storage/volume_conversions/to_y2storage.rb similarity index 99% rename from service/lib/agama/storage/volume_conversion/to_y2storage.rb rename to service/lib/agama/storage/volume_conversions/to_y2storage.rb index dd258c3bce..be7a7d43d5 100644 --- a/service/lib/agama/storage/volume_conversion/to_y2storage.rb +++ b/service/lib/agama/storage/volume_conversions/to_y2storage.rb @@ -24,7 +24,7 @@ module Agama module Storage - module VolumeConversion + module VolumeConversions # Volume conversion to Y2Storage. class ToY2Storage # @param volume [Agama::Storage::Volume] diff --git a/service/test/agama/storage/proposal_settings_conversion_test.rb b/service/test/agama/storage/proposal_settings_conversion_test.rb deleted file mode 100644 index dfb1db457a..0000000000 --- a/service/test/agama/storage/proposal_settings_conversion_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -# 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 version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require_relative "../../test_helper" -require "agama/storage/proposal_settings" -require "agama/storage/proposal_settings_conversion" -require "y2storage" - -describe Agama::Storage::ProposalSettingsConversion do - describe "#from_y2storage" do - let(:y2storage_settings) { Y2Storage::ProposalSettings.new } - - let(:settings) { Agama::Storage::ProposalSettings.new } - - it "generates proposal settings from Y2Storage settings" do - result = described_class.from_y2storage(y2storage_settings, settings) - expect(result).to be_a(Agama::Storage::ProposalSettings) - end - end - - describe "#to_y2storage" do - let(:config) { Agama::Config.new } - - let(:settings) { Agama::Storage::ProposalSettings.new } - - it "generates Y2Storage settings from proposal settings" do - result = described_class.to_y2storage(settings, config: config) - expect(result).to be_a(Y2Storage::ProposalSettings) - end - end -end diff --git a/service/test/agama/storage/proposal_settings_conversions/from_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversions/from_y2storage_test.rb index 93f0f9e519..cb4b4b4dd0 100644 --- a/service/test/agama/storage/proposal_settings_conversions/from_y2storage_test.rb +++ b/service/test/agama/storage/proposal_settings_conversions/from_y2storage_test.rb @@ -22,10 +22,10 @@ require_relative "../../../test_helper" require "agama/config" require "agama/storage/proposal_settings" -require "agama/storage/proposal_settings_conversion/from_y2storage" +require "agama/storage/proposal_settings_conversions/from_y2storage" require "y2storage" -describe Agama::Storage::ProposalSettingsConversion::FromY2Storage do +describe Agama::Storage::ProposalSettingsConversions::FromY2Storage do subject { described_class.new(y2storage_settings, original_settings) } let(:y2storage_settings) do diff --git a/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb index 48918aa4bb..82b5d6a110 100644 --- a/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb +++ b/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb @@ -24,10 +24,10 @@ require "agama/config" require "agama/storage/device_settings" require "agama/storage/proposal_settings" -require "agama/storage/proposal_settings_conversion/to_y2storage" +require "agama/storage/proposal_settings_conversions/to_y2storage" require "y2storage" -describe Agama::Storage::ProposalSettingsConversion::ToY2Storage do +describe Agama::Storage::ProposalSettingsConversions::ToY2Storage do include Agama::RSpec::StorageHelpers subject { described_class.new(settings, config: config) } diff --git a/service/test/agama/storage/proposal_settings_test.rb b/service/test/agama/storage/proposal_settings_test.rb index 0e2aafcdad..2d509f4fd4 100644 --- a/service/test/agama/storage/proposal_settings_test.rb +++ b/service/test/agama/storage/proposal_settings_test.rb @@ -24,6 +24,7 @@ require "agama/storage/device_settings" require "agama/storage/proposal_settings" require "agama/storage/volume" +require "y2storage/proposal_settings" describe Agama::Storage::ProposalSettings do describe "#default_boot_device" do @@ -197,4 +198,15 @@ expect(result).to be_a(Hash) end end + + describe "#to_y2storage" do + let(:config) { Agama::Config.new } + + let(:settings) { Agama::Storage::ProposalSettings.new } + + it "generates Y2Storage settings from proposal settings" do + result = subject.to_y2storage(config: config) + expect(result).to be_a(Y2Storage::ProposalSettings) + end + end end diff --git a/service/test/agama/storage/volume_conversion_test.rb b/service/test/agama/storage/volume_conversion_test.rb deleted file mode 100644 index b63866edf5..0000000000 --- a/service/test/agama/storage/volume_conversion_test.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -# 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 version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require_relative "../../test_helper" -require "agama/storage/volume" -require "agama/storage/volume_conversion" -require "y2storage" - -describe Agama::Storage::VolumeConversion do - describe "#from_y2storage" do - let(:volume) { Agama::Storage::Volume.new("/test") } - - it "generates a volume" do - result = described_class.from_y2storage(volume) - expect(result).to be_a(Agama::Storage::Volume) - end - end - - describe "#to_y2storage" do - let(:volume) { Agama::Storage::Volume.new("/test") } - - it "generates a Y2Storage volume spec" do - result = described_class.to_y2storage(volume) - expect(result).to be_a(Y2Storage::VolumeSpecification) - end - end -end diff --git a/service/test/agama/storage/volume_conversions/from_y2storage_test.rb b/service/test/agama/storage/volume_conversions/from_y2storage_test.rb index ceecba866b..7fa66d558b 100644 --- a/service/test/agama/storage/volume_conversions/from_y2storage_test.rb +++ b/service/test/agama/storage/volume_conversions/from_y2storage_test.rb @@ -23,10 +23,10 @@ require_relative "../storage_helpers" require_relative "../../rspec/matchers/storage" require "agama/storage/volume" -require "agama/storage/volume_conversion/from_y2storage" +require "agama/storage/volume_conversions/from_y2storage" require "y2storage" -describe Agama::Storage::VolumeConversion::FromY2Storage do +describe Agama::Storage::VolumeConversions::FromY2Storage do include Agama::RSpec::StorageHelpers before { mock_storage } diff --git a/service/test/agama/storage/volume_conversions/to_y2storage_test.rb b/service/test/agama/storage/volume_conversions/to_y2storage_test.rb index 8b52a1372f..5c7ae44b53 100644 --- a/service/test/agama/storage/volume_conversions/to_y2storage_test.rb +++ b/service/test/agama/storage/volume_conversions/to_y2storage_test.rb @@ -20,10 +20,10 @@ # find current contact information at www.suse.com. require_relative "../../../test_helper" -require "agama/storage/volume_conversion/to_y2storage" +require "agama/storage/volume_conversions/to_y2storage" require "y2storage" -describe Agama::Storage::VolumeConversion::ToY2Storage do +describe Agama::Storage::VolumeConversions::ToY2Storage do subject { described_class.new(volume) } describe "#convert" do diff --git a/service/test/agama/storage/volume_test.rb b/service/test/agama/storage/volume_test.rb index f0e4ae8a7d..2c46c68acc 100644 --- a/service/test/agama/storage/volume_test.rb +++ b/service/test/agama/storage/volume_test.rb @@ -22,6 +22,7 @@ require_relative "../../test_helper" require "agama/config" require "agama/storage/volume" +require "y2storage/volume_specification" describe Agama::Storage::Volume do describe ".new_from_json" do @@ -50,4 +51,13 @@ expect(result).to be_a(Hash) end end + + describe "#to_y2storage" do + let(:volume) { Agama::Storage::Volume.new("/test") } + + it "generates a Y2Storage volume spec" do + result = volume.to_y2storage + expect(result).to be_a(Y2Storage::VolumeSpecification) + end + end end From 0638ef981b377ba3449291a7c6d3b8d9394668ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 28 Jun 2024 12:44:37 +0100 Subject: [PATCH 036/430] storage: avoid reference to compound action --- service/lib/agama/dbus/storage/manager.rb | 2 +- service/lib/agama/dbus/storage/proposal.rb | 2 +- service/lib/agama/storage/action.rb | 53 ++++++++++++------- .../lib/agama/storage/actions_generator.rb | 11 ++-- service/lib/agama/storage/manager.rb | 23 +------- service/lib/agama/storage/proposal.rb | 2 +- .../test/agama/dbus/storage/manager_test.rb | 13 ++--- .../test/agama/dbus/storage/proposal_test.rb | 13 ++--- service/test/agama/storage/action_test.rb | 8 +-- 9 files changed, 54 insertions(+), 73 deletions(-) diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 70fd4cf970..79aea7d2de 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -160,7 +160,7 @@ def deprecated_system def read_actions backend.actions.map do |action| { - "Device" => action.device.sid, + "Device" => action.device_sid, "Text" => action.text, "Subvol" => action.on_btrfs_subvolume?, "Delete" => action.delete?, diff --git a/service/lib/agama/dbus/storage/proposal.rb b/service/lib/agama/dbus/storage/proposal.rb index bac490e8af..fd9c45428f 100644 --- a/service/lib/agama/dbus/storage/proposal.rb +++ b/service/lib/agama/dbus/storage/proposal.rb @@ -85,7 +85,7 @@ def actions # * "Resize" [Boolean] def to_dbus_action(action) { - "Device" => action.device.sid, + "Device" => action.device_sid, "Text" => action.text, "Subvol" => action.on_btrfs_subvolume?, "Delete" => action.delete?, diff --git a/service/lib/agama/storage/action.rb b/service/lib/agama/storage/action.rb index 19100f35a2..8f34ec31ca 100644 --- a/service/lib/agama/storage/action.rb +++ b/service/lib/agama/storage/action.rb @@ -23,49 +23,51 @@ module Agama module Storage # Represents an action to perform in the storage devices. class Action - # @param action [Y2Storage::CompoundAction] - # @param system_graph [Y2Storage::Devicegraph] - def initialize(action, system_graph) - @action = action - @system_graph = system_graph - end - - # Affected device + # SID of the affected device # - # @return [Y2Storage::Device] - def device - action.target_device - end + # @return [Integer] + attr_reader :device_sid # Text describing the action. # # @return [String] - def text - action.sentence + attr_reader :text + + # @note Do not keep a reference to the compound action. Accessing to the compound action could + # raise a segmentation fault if the source actiongraph object was killed by the ruby GC. + # + # See https://github.com/openSUSE/agama/issues/1396. + # + # @param action [Y2Storage::CompoundAction] + # @param system_graph [Y2Storage::Devicegraph] + def initialize(action, system_graph) + @system_graph = system_graph + @device_sid = action.target_device.sid + @text = action.sentence + @delete = action.delete? + @resize = resize_action?(action.target_device, system_graph) + @on_btrfs_subvolume = action.device_is?(:btrfs_subvolume) end # Whether the action affects to a Btrfs subvolume. # # @return [Boolean] def on_btrfs_subvolume? - action.device_is?(:btrfs_subvolume) + @on_btrfs_subvolume end # Whether the action deletes the device. # # @return [Boolean] def delete? - action.delete? + @delete end # Whether the action resizes the device. # # @return [Boolean] def resize? - return false unless device.exists_in_devicegraph?(system_graph) - return false unless device.respond_to?(:size) - - system_graph.find_device(device.sid).size != device.size + @resize end private @@ -75,6 +77,17 @@ def resize? # @return [Y2Storage::Devicegraph] attr_reader :system_graph + + # @param device [Y2Storage::Device] + # @param system_graph [Y2Storage::Devicegraph] + # + # @return [Boolean] + def resize_action?(device, system_graph) + return false unless device.exists_in_devicegraph?(system_graph) + return false unless device.respond_to?(:size) + + system_graph.find_device(device.sid).size != device.size + end end end end diff --git a/service/lib/agama/storage/actions_generator.rb b/service/lib/agama/storage/actions_generator.rb index 082a61ff96..059b4e7281 100644 --- a/service/lib/agama/storage/actions_generator.rb +++ b/service/lib/agama/storage/actions_generator.rb @@ -29,10 +29,7 @@ class ActionsGenerator # param target_graph [Y2Storage::Devicegraph] def initialize(system_graph, target_graph) @system_graph = system_graph - # It is important to keep a reference to the actiongraph. Otherwise, the gargabe collector - # could kill the actiongraph, leaving the compound actions orphan. - # See https://github.com/openSUSE/agama/issues/1377. - @actiongraph = target_graph.actiongraph + @target_graph = target_graph end # All actions properly sorted. @@ -47,8 +44,8 @@ def generate # @return [Y2Storage::Devicegraph] attr_reader :system_graph - # @return [Y2Storage::Actiongraph] - attr_reader :actiongraph + # @return [Y2Storage::Devicegraph] + attr_reader :target_graph # Sorted main actions (everything except subvolume actions). # @@ -70,7 +67,7 @@ def subvolume_actions # # @return [Array] def actions - @actions ||= actiongraph.compound_actions.map do |action| + @actions ||= target_graph.actiongraph.compound_actions.map do |action| Action.new(action, system_graph) end end diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index a91f4c141a..1d4faf9f9c 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -165,32 +165,13 @@ def software # Storage actions. # - # @return [Array] + # @return [Array] def actions return [] unless Y2Storage::StorageManager.instance.probed? probed = Y2Storage::StorageManager.instance.probed staging = Y2Storage::StorageManager.instance.staging - # FIXME: This is a hot-fix to avoid segmentation fault in the actions, see - # https://github.com/openSUSE/agama/issues/1396. - # - # Source of the problem: - # * An actiongraph is generated from the target devicegraph. - # * The list of compound actions is recovered from the actiongraph. - # * No refrence to the actiongraph is kept, so the object is a candidate to be cleaned by - # the ruby GC. - # * Accessing to the generated actions raises a segmentation fault if the actiongraph was - # cleaned. - # - # There was a previous attempt of fixing the issue by keeping a reference to the - # actiongraph in the ActionsGenerator object. But that solution is not enough because - # the ActionGenerator object is also cleaned up. - # - # As a hot-fix, the generator is kept in an instance variable to avoid the GC to kill it. - # A better solution is needed, for example, by avoiding to store an instance of a compound - # action in the Action object. - @generator = ActionsGenerator.new(probed, staging) - @generator.generate + ActionsGenerator.new(probed, staging).generate end # Changes the service's locale diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb index 010666545e..eb826128b4 100644 --- a/service/lib/agama/storage/proposal.rb +++ b/service/lib/agama/storage/proposal.rb @@ -105,7 +105,7 @@ def calculate_autoyast(partitioning) # Storage actions. # - # @return [Array] + # @return [Array] def actions return [] unless proposal&.devices diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index cc81b29000..6bd71ca962 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -119,7 +119,7 @@ let(:action1) do instance_double(Agama::Storage::Action, text: "test1", - device: device1, + device_sid: 1, on_btrfs_subvolume?: false, delete?: false, resize?: false) @@ -128,7 +128,7 @@ let(:action2) do instance_double(Agama::Storage::Action, text: "test2", - device: device2, + device_sid: 2, on_btrfs_subvolume?: false, delete?: true, resize?: false) @@ -137,7 +137,7 @@ let(:action3) do instance_double(Agama::Storage::Action, text: "test3", - device: device3, + device_sid: 3, on_btrfs_subvolume?: false, delete?: false, resize?: true) @@ -146,17 +146,12 @@ let(:action4) do instance_double(Agama::Storage::Action, text: "test4", - device: device4, + device_sid: 4, on_btrfs_subvolume?: true, delete?: false, resize?: false) end - let(:device1) { instance_double(Y2Storage::Device, sid: 1) } - let(:device2) { instance_double(Y2Storage::Device, sid: 2) } - let(:device3) { instance_double(Y2Storage::Device, sid: 3) } - let(:device4) { instance_double(Y2Storage::Device, sid: 4) } - it "returns a list with a hash for each action" do expect(subject.actions.size).to eq(4) expect(subject.actions).to all(be_a(Hash)) diff --git a/service/test/agama/dbus/storage/proposal_test.rb b/service/test/agama/dbus/storage/proposal_test.rb index f2270b064d..59b5768c41 100644 --- a/service/test/agama/dbus/storage/proposal_test.rb +++ b/service/test/agama/dbus/storage/proposal_test.rb @@ -114,7 +114,7 @@ let(:action1) do instance_double(Agama::Storage::Action, text: "test1", - device: device1, + device_sid: 1, on_btrfs_subvolume?: false, delete?: false, resize?: false) @@ -123,7 +123,7 @@ let(:action2) do instance_double(Agama::Storage::Action, text: "test2", - device: device2, + device_sid: 2, on_btrfs_subvolume?: false, delete?: true, resize?: false) @@ -132,7 +132,7 @@ let(:action3) do instance_double(Agama::Storage::Action, text: "test3", - device: device3, + device_sid: 3, on_btrfs_subvolume?: false, delete?: false, resize?: true) @@ -141,17 +141,12 @@ let(:action4) do instance_double(Agama::Storage::Action, text: "test4", - device: device4, + device_sid: 4, on_btrfs_subvolume?: true, delete?: false, resize?: false) end - let(:device1) { instance_double(Y2Storage::Device, sid: 1) } - let(:device2) { instance_double(Y2Storage::Device, sid: 2) } - let(:device3) { instance_double(Y2Storage::Device, sid: 3) } - let(:device4) { instance_double(Y2Storage::Device, sid: 4) } - it "returns a list with a hash for each action" do expect(subject.actions.size).to eq(4) expect(subject.actions).to all(be_a(Hash)) diff --git a/service/test/agama/storage/action_test.rb b/service/test/agama/storage/action_test.rb index a6b26f93a4..7109e0d1b9 100644 --- a/service/test/agama/storage/action_test.rb +++ b/service/test/agama/storage/action_test.rb @@ -59,17 +59,17 @@ described_class.new(action, system_graph) end - describe "#device" do - it "returns the affected device" do + describe "#device_sid" do + it "returns the SID of the affected device" do sda2 = system_graph.find_by_name("/dev/sda2") - expect(sda2_action.device.sid).to eq(sda2.sid) + expect(sda2_action.device_sid).to eq(sda2.sid) home_subvol = sda2 .filesystem .btrfs_subvolumes .find { |s| s.path == "home" } - expect(subvol_action.device.sid).to eq(home_subvol.sid) + expect(subvol_action.device_sid).to eq(home_subvol.sid) end end From 591c536c9e17f43e2349154a189aa653d8ee2814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 28 Jun 2024 12:59:19 +0100 Subject: [PATCH 037/430] service: changelog --- service/package/rubygem-agama-yast.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 83ea92b628..e7237a446e 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Jun 28 11:57:39 UTC 2024 - José Iván López González + +- Proper solution to avoid error in storage actions + (gh#openSUSE/agama#1410). + ------------------------------------------------------------------- Thu Jun 27 13:22:06 UTC 2024 - Imobach Gonzalez Sosa From 08ddf923c237acb2c87ebd84753a4713d31d9553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 28 Jun 2024 15:30:04 +0200 Subject: [PATCH 038/430] Syntax highlighting for "agama config edit" --- live/src/agama-live.kiwi | 1 + live/src/config.sh | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/live/src/agama-live.kiwi b/live/src/agama-live.kiwi index 1198bdeb07..867fb160c8 100644 --- a/live/src/agama-live.kiwi +++ b/live/src/agama-live.kiwi @@ -54,6 +54,7 @@ + diff --git a/live/src/config.sh b/live/src/config.sh index 4286912cf7..695c8813d7 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -82,6 +82,11 @@ sed -i -e "s/@@LIVE_MEDIUM_LABEL@@/$label/g" /usr/bin/live-password # Clean-up logs rm /var/log/zypper.log /var/log/zypp/history +# reduce the "vim-data" content, this package is huge (37MB unpacked!), keep only +# support for JSON (for "agama config edit") and Ruby (fixing/debugging the Ruby +# service) +rpm -ql vim-data | grep -v -e '/ruby.vim$' -e '/json.vim$' -e colors | xargs rm 2> /dev/null || true + du -h -s /usr/{share,lib}/locale/ # Agama expects that the same locales available in the installation system can From bba7e18f63f26166c361ef35ac71a9f900ebbc16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 28 Jun 2024 15:41:22 +0200 Subject: [PATCH 039/430] Changes --- live/src/agama-live.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/live/src/agama-live.changes b/live/src/agama-live.changes index 155178eca6..4d6f26a98f 100644 --- a/live/src/agama-live.changes +++ b/live/src/agama-live.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Jun 28 13:40:35 UTC 2024 - Ladislav Slezák + +- Syntax highlighting for "agama config edit" + (gh#openSUSE/agama#1411) + ------------------------------------------------------------------- Thu Jun 27 14:33:24 UTC 2024 -Steffen Winterfeldt From 83c2e786f17b8f988bb3ab346be2a28600849350 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 28 Jun 2024 15:39:47 +0200 Subject: [PATCH 040/430] fix(web): VolumeDialog: share numeric value between Fixed and Range In Add/Edit file system dialog (VolumeDialog), keep the numeric value when switching between Fixed and Range sizing (gh#openSUSE/agama#1277) Do it by removing size and sizeUnit from formData and using minSize, minSizeUnit for both roles. --- web/src/components/storage/VolumeDialog.jsx | 7 +------ web/src/components/storage/VolumeFields.jsx | 10 +++++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/web/src/components/storage/VolumeDialog.jsx b/web/src/components/storage/VolumeDialog.jsx index fc5d64f092..47b411f121 100644 --- a/web/src/components/storage/VolumeDialog.jsx +++ b/web/src/components/storage/VolumeDialog.jsx @@ -43,8 +43,6 @@ import { * @property {VolumeFormErrors} errors * * @typedef {object} VolumeFormData - * @property {number|string} [size] - * @property {string} [sizeUnit] * @property {number|string} [minSize] * @property {string} [minSizeUnit] * @property {number|string} [maxSize] @@ -490,7 +488,6 @@ const sanitizeMountPath = (mountPath) => { */ const createUpdatedVolume = (volume, formData) => { let sizeAttrs = {}; - const size = parseToBytes(`${formData.size} ${formData.sizeUnit}`); const minSize = parseToBytes(`${formData.minSize} ${formData.minSizeUnit}`); const maxSize = parseToBytes(`${formData.maxSize} ${formData.maxSizeUnit}`); @@ -499,7 +496,7 @@ const createUpdatedVolume = (volume, formData) => { sizeAttrs = { minSize: undefined, maxSize: undefined, autoSize: true }; break; case SIZE_METHODS.MANUAL: - sizeAttrs = { minSize: size, maxSize: size, autoSize: false }; + sizeAttrs = { minSize, maxSize: minSize, autoSize: false }; break; case SIZE_METHODS.RANGE: sizeAttrs = { minSize, maxSize: formData.maxSize ? maxSize : undefined, autoSize: false }; @@ -543,8 +540,6 @@ const prepareFormData = (volume) => { const { size: maxSize = "", unit: maxSizeUnit = minSizeUnit || DEFAULT_SIZE_UNIT } = splitSize(volume.maxSize); return { - size: minSize, - sizeUnit: minSizeUnit, minSize, minSizeUnit, maxSize, diff --git a/web/src/components/storage/VolumeFields.jsx b/web/src/components/storage/VolumeFields.jsx index a74b52a7ae..dddaeca68e 100644 --- a/web/src/components/storage/VolumeFields.jsx +++ b/web/src/components/storage/VolumeFields.jsx @@ -321,9 +321,9 @@ const SizeManual = ({ errors, formData, isDisabled, onChange }) => { // (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString) // or use the "globalize" JS library which can also parse the localized string back // (https://github.com/globalizejs/globalize#number-module) - value={formData.size} - onChange={(size) => onChange({ size })} - validated={errors.size && 'error'} + value={formData.minSize} + onChange={(minSize) => onChange({ minSize })} + validated={errors.minSize && 'error'} isDisabled={isDisabled} /> @@ -334,8 +334,8 @@ const SizeManual = ({ errors, formData, isDisabled, onChange }) => { // TRANSLATORS: units selector (like KiB, MiB, GiB...) aria-label={_("Size unit")} units={Object.values(SIZE_UNITS)} - value={formData.sizeUnit} - onChange={(_, sizeUnit) => onChange({ sizeUnit })} + value={formData.minSizeUnit} + onChange={(_, minSizeUnit) => onChange({ minSizeUnit })} isDisabled={isDisabled} /> From ca1d1eea65ebfb86e607f7c947794134f5ac8d33 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 28 Jun 2024 15:50:47 +0200 Subject: [PATCH 041/430] chore: update .changes --- web/package/agama-web-ui.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index feae42319d..630e1ce64d 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Jun 28 13:49:54 UTC 2024 - Martin Vidner + +- In Add/Edit file system dialog (VolumeDialog), keep the numeric value + when switching between Fixed and Range sizing (gh#openSUSE/agama#1277) + ------------------------------------------------------------------- Fri Jun 28 06:56:02 UTC 2024 - Martin Vidner From 9987351c1621527897ec5f90749ea6e94e883818 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 30 Jun 2024 02:51:30 +0000 Subject: [PATCH 042/430] Update web PO files Agama-weblate commit: cbc16878be96c84d92ad6383b45382141c118d7d --- web/po/ca.po | 521 +++++++++++++++++------------- web/po/cs.po | 409 +++++++++++++----------- web/po/de.po | 523 +++++++++++++++++------------- web/po/es.po | 539 +++++++++++++++++-------------- web/po/fr.po | 545 ++++++++++++++++++-------------- web/po/id.po | 522 +++++++++++++++++------------- web/po/ja.po | 522 +++++++++++++++++------------- web/po/ka.po | 698 ++++++++++++++++++++--------------------- web/po/mk.po | 414 ++++++++++++------------ web/po/nb_NO.po | 519 +++++++++++++++++------------- web/po/nl.po | 485 +++++++++++++++------------- web/po/pt_BR.po | 528 ++++++++++++++++++------------- web/po/ru.po | 520 +++++++++++++++++------------- web/po/sv.po | 524 +++++++++++++++++-------------- web/po/tr.po | 422 +++++++++++++------------ web/po/uk.po | 409 +++++++++++++----------- web/po/zh_Hans.po | 601 +++++++++++++++++++---------------- web/src/languages.json | 3 +- 18 files changed, 4860 insertions(+), 3844 deletions(-) diff --git a/web/po/ca.po b/web/po/ca.po index 33b9563672..87b43cece1 100644 --- a/web/po/ca.po +++ b/web/po/ca.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-23 02:24+0000\n" -"PO-Revision-Date: 2024-06-17 09:46+0000\n" +"POT-Creation-Date: 2024-06-30 02:27+0000\n" +"PO-Revision-Date: 2024-06-27 09:46+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.5.5\n" +"X-Generator: Weblate 5.6\n" #: src/MainLayout.jsx:40 msgid "Agama" @@ -57,8 +57,7 @@ msgstr "" "Per obtenir-ne més informació, visiteu el repositori del projecte a %s." #: src/components/core/About.jsx:94 src/components/core/FileViewer.jsx:81 -#: src/components/core/InstallerOptions.jsx:70 -#: src/components/network/WifiSelector.jsx:148 +#: src/components/core/LogsButton.jsx:123 #: src/components/software/SoftwarePatternsSelection.jsx:260 msgid "Close" msgstr "Tanca" @@ -96,7 +95,7 @@ msgstr "Continua" #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:97 #: src/components/core/Popup.jsx:136 -#: src/components/network/WifiConnectionForm.jsx:136 +#: src/components/network/WifiConnectionForm.jsx:131 msgid "Cancel" msgstr "Cancel·la" @@ -163,20 +162,47 @@ msgstr "Acaba" msgid "Reboot" msgstr "Reinicia" -#. TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) -#: src/components/core/InstallationProgress.jsx:37 -#, fuzzy, c-format -msgid "Installing %s, please wait ..." -msgstr "Carregant dades. Espereu, si us plau..." +#: src/components/core/InstallationProgress.jsx:30 +msgid "Installing the system, please wait ..." +msgstr "Instal·lant el sistema. Espereu, si us plau..." -#: src/components/core/InstallerOptions.jsx:57 +#: src/components/core/InstallerOptions.jsx:83 msgid "Show installer options" msgstr "Mostra les opcions de l'instal·lador" -#: src/components/core/InstallerOptions.jsx:62 +#: src/components/core/InstallerOptions.jsx:88 msgid "Installer options" msgstr "Opcions de l'instal·lador" +#: src/components/core/InstallerOptions.jsx:94 +#: src/components/core/InstallerOptions.jsx:99 +#: src/components/core/InstallerOptions.jsx:100 +#: src/components/l10n/L10nPage.jsx:67 +msgid "Language" +msgstr "Llengua" + +#: src/components/core/InstallerOptions.jsx:114 +#: src/components/core/InstallerOptions.jsx:121 +msgid "Keyboard layout" +msgstr "Disposició del teclat" + +#: src/components/core/InstallerOptions.jsx:130 +msgid "Cannot be changed in remote installation" +msgstr "No es pot canviar a la instal·lació remota." + +#: src/components/core/InstallerOptions.jsx:135 +#: src/components/network/IpSettingsForm.jsx:210 +#: src/components/product/ProductRegistrationPage.jsx:89 +#: src/components/storage/BootSelection.jsx:228 +#: src/components/storage/DeviceSelection.jsx:240 +#: src/components/storage/EncryptionSettingsDialog.jsx:138 +#: src/components/storage/SpacePolicySelection.jsx:198 +#: src/components/storage/VolumeDialog.jsx:781 +#: src/components/storage/ZFCPPage.jsx:503 +#: src/components/users/FirstUserForm.jsx:285 +msgid "Accept" +msgstr "Accepta-ho" + #: src/components/core/IssuesHint.jsx:34 msgid "" "Before starting the installation, you need to address the following problems:" @@ -232,15 +258,16 @@ msgstr "Inicia la sessió" msgid "More about this" msgstr "Més sobre això" -#: src/components/core/LogsButton.jsx:102 +#: src/components/core/LogsButton.jsx:103 msgid "Collecting logs..." msgstr "Recopilant registres..." -#: src/components/core/LogsButton.jsx:102 +#: src/components/core/LogsButton.jsx:103 +#: src/components/core/LogsButton.jsx:106 msgid "Download logs" msgstr "Baixa els registres" -#: src/components/core/LogsButton.jsx:110 +#: src/components/core/LogsButton.jsx:112 msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." @@ -248,7 +275,7 @@ msgstr "" "El navegador executarà la baixada de registres així que estiguin a punt. Si " "us plau, tingueu paciència." -#: src/components/core/LogsButton.jsx:118 +#: src/components/core/LogsButton.jsx:120 msgid "Something went wrong while collecting logs. Please, try again." msgstr "" "Hi ha hagut un error durant la recopilació de registres. Torneu-ho a provar." @@ -258,7 +285,7 @@ msgid "Passwords do not match" msgstr "Les contrasenyes no coincideixen." #: src/components/core/PasswordAndConfirmationInput.jsx:72 -#: src/components/network/WifiConnectionForm.jsx:125 +#: src/components/network/WifiConnectionForm.jsx:120 #: src/components/storage/iscsi/AuthFields.jsx:95 #: src/components/storage/iscsi/AuthFields.jsx:100 #: src/components/users/RootAuthMethods.jsx:163 @@ -282,13 +309,25 @@ msgstr "Confirmeu-ho" msgid "Loading data..." msgstr "Carregant dades..." -#: src/components/core/ProgressReport.jsx:122 -#, fuzzy +#: src/components/core/ProgressReport.jsx:50 +msgid "Finished" +msgstr "Acabada" + +#: src/components/core/ProgressReport.jsx:59 +msgid "In progress" +msgstr "En curs" + +#: src/components/core/ProgressReport.jsx:70 +msgid "Pending" +msgstr "Pendent" + +#: src/components/core/ProgressReport.jsx:134 msgid "Waiting for progress status..." -msgstr "Esperant l'informe de progrés" +msgstr "Esperant l'informe de progrés..." #: src/components/core/RowActions.jsx:64 #: src/components/storage/PartitionsField.jsx:454 +#: src/components/storage/ProposalActionsSummary.jsx:226 msgid "Actions" msgstr "Accions" @@ -319,28 +358,6 @@ msgstr "Si us plau, comproveu si s'executa." msgid "Reload" msgstr "Torna a carregar" -#: src/components/l10n/InstallerKeymapSwitcher.jsx:66 -#: src/components/l10n/L10nPage.jsx:78 -msgid "Keyboard" -msgstr "Teclat" - -#: src/components/l10n/InstallerKeymapSwitcher.jsx:75 -msgid "Choose a keyboard layout" -msgstr "Trieu una disposició de teclat" - -#: src/components/l10n/InstallerKeymapSwitcher.jsx:86 -msgid "Cannot be changed in remote installation" -msgstr "No es pot canviar a la instal·lació remota." - -#: src/components/l10n/InstallerLocaleSwitcher.jsx:57 -#: src/components/l10n/L10nPage.jsx:67 -msgid "Language" -msgstr "Llengua" - -#: src/components/l10n/InstallerLocaleSwitcher.jsx:63 -msgid "Choose a language" -msgstr "Trieu la llengua" - #: src/components/l10n/KeyboardSelection.jsx:45 msgid "Filter by description or keymap code" msgstr "Filtra per descripció o codi de mapa de tecles" @@ -374,13 +391,18 @@ msgstr "Encara no s'ha seleccionat." #: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 #: src/components/l10n/L10nPage.jsx:93 -#: src/components/storage/InstallationDeviceField.jsx:115 -#: src/components/storage/SpacePolicyField.jsx:91 +#: src/components/network/NetworkPage.jsx:102 +#: src/components/storage/InstallationDeviceField.jsx:105 +#: src/components/storage/ProposalActionsSummary.jsx:228 #: src/components/users/RootAuthMethods.jsx:94 #: src/components/users/RootAuthMethods.jsx:107 msgid "Change" msgstr "Canvia" +#: src/components/l10n/L10nPage.jsx:78 +msgid "Keyboard" +msgstr "Teclat" + #: src/components/l10n/L10nPage.jsx:89 msgid "Time zone" msgstr "Zona horària" @@ -469,6 +491,8 @@ msgid "IP addresses" msgstr "Adreces IP" #: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/WifiNetworksListPage.jsx:100 +#: src/components/network/WifiNetworksListPage.jsx:124 #: src/components/storage/PartitionsField.jsx:320 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 @@ -479,12 +503,13 @@ msgstr "Edita" #. TRANSLATORS: %s is replaced by a network connection name #: src/components/network/ConnectionsTable.jsx:80 #: src/components/network/IpSettingsForm.jsx:136 -#: src/components/network/routes.js:59 #, c-format msgid "Edit connection %s" msgstr "Edita la connexió %s" #: src/components/network/ConnectionsTable.jsx:84 +#: src/components/network/WifiNetworksListPage.jsx:103 +#: src/components/network/WifiNetworksListPage.jsx:127 msgid "Forget" msgstr "Oblida-la" @@ -556,57 +581,57 @@ msgstr "Passarel·la" msgid "Gateway can be defined only in 'Manual' mode" msgstr "La passarel·la només es pot definir en mode manual." -#: src/components/network/IpSettingsForm.jsx:210 -#: src/components/product/ProductRegistrationPage.jsx:89 -#: src/components/storage/BootSelection.jsx:215 -#: src/components/storage/DeviceSelection.jsx:240 -#: src/components/storage/EncryptionSettingsDialog.jsx:138 -#: src/components/storage/VolumeDialog.jsx:786 -#: src/components/storage/ZFCPPage.jsx:503 -#: src/components/users/FirstUserForm.jsx:286 -msgid "Accept" -msgstr "Accepta-ho" - -#: src/components/network/NetworkPage.jsx:44 -#: src/components/network/WifiSelector.jsx:129 -msgid "Connect to a Wi-Fi network" -msgstr "Connecteu-vos a una xarxa Wi-Fi" - -#: src/components/network/NetworkPage.jsx:57 -msgid "No wired connections found." -msgstr "No s'ha trobat cap connexió amb fil." +#: src/components/network/NetworkPage.jsx:85 +msgid "No Wi-Fi supported" +msgstr "No és compatible amb Wi-Fi." -#: src/components/network/NetworkPage.jsx:71 +#: src/components/network/NetworkPage.jsx:86 msgid "" -"The system has not been configured for connecting to a WiFi network yet." +"The system does not support Wi-Fi connections, probably because of missing " +"or disabled hardware." msgstr "" -"El sistema encara no s'ha configurat per connectar-se a una xarxa WiFi." +"El sistema no admet connexions de wifi, probablement a causa de maquinari " +"que manca o que està inhabilitat." + +#: src/components/network/NetworkPage.jsx:99 +msgid "Wi-Fi" +msgstr "Wifi" + +#. TRANSLATORS: button label, connect to a WiFi network +#: src/components/network/NetworkPage.jsx:102 +#: src/components/network/WifiConnectionForm.jsx:128 +#: src/components/network/WifiNetworksListPage.jsx:97 +msgid "Connect" +msgstr "Connecta't" + +#: src/components/network/NetworkPage.jsx:109 +#, c-format +msgid "Conected to %s" +msgstr "Connectat amb %s" -#: src/components/network/NetworkPage.jsx:72 +#: src/components/network/NetworkPage.jsx:114 +msgid "No connected yet" +msgstr "Encara no s'ha connetat." + +#: src/components/network/NetworkPage.jsx:115 msgid "" -"The system does not support WiFi connections, probably because of missing or " -"disabled hardware." +"The system has not been configured for connecting to a Wi-Fi network yet." msgstr "" -"El sistema no admet connexions WiFi, probablement a causa del maquinari que " -"manca o està desactivat." +"El sistema encara no s'ha configurat per connectar-se a una xarxa de wifi." + +#: src/components/network/NetworkPage.jsx:136 +msgid "Wired" +msgstr "Amb fil" -#: src/components/network/NetworkPage.jsx:76 -msgid "No WiFi connections found." -msgstr "No s'ha trobat cap connexió WiFi." +#: src/components/network/NetworkPage.jsx:139 +msgid "No wired connections found" +msgstr "No s'ha trobat cap connexió amb fil." -#: src/components/network/NetworkPage.jsx:170 -#: src/components/network/routes.js:49 +#: src/components/network/NetworkPage.jsx:149 +#: src/components/network/routes.js:59 msgid "Network" msgstr "Xarxa" -#: src/components/network/NetworkPage.jsx:178 -msgid "Wired connections" -msgstr "Connexions amb fil" - -#: src/components/network/NetworkPage.jsx:185 -msgid "WiFi connections" -msgstr "Connexions WiFi" - #. TRANSLATORS: WiFi authentication mode #: src/components/network/WifiConnectionForm.jsx:43 #: src/components/storage/iscsi/InitiatorPresenter.jsx:72 @@ -618,7 +643,7 @@ msgstr "Cap" msgid "WPA & WPA2 Personal" msgstr "WPA i WPA2 personal" -#: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/network/WifiConnectionForm.jsx:86 #: src/components/product/ProductRegistrationPage.jsx:69 #: src/components/storage/ZFCPDiskForm.jsx:108 #: src/components/storage/iscsi/DiscoverForm.jsx:110 @@ -627,82 +652,68 @@ msgstr "WPA i WPA2 personal" msgid "Something went wrong" msgstr "Alguna cosa ha anat malament." -#: src/components/network/WifiConnectionForm.jsx:92 +#: src/components/network/WifiConnectionForm.jsx:87 msgid "Please, review provided settings and try again." msgstr "" "Si us plau, reviseu la configuració proporcionada i torneu-ho a provar." #. TRANSLATORS: SSID (Wifi network name) configuration -#: src/components/network/WifiConnectionForm.jsx:97 -#: src/components/network/WifiConnectionForm.jsx:101 +#: src/components/network/WifiConnectionForm.jsx:92 +#: src/components/network/WifiConnectionForm.jsx:96 msgid "SSID" msgstr "SSID" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:109 -#: src/components/network/WifiConnectionForm.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:104 +#: src/components/network/WifiConnectionForm.jsx:107 msgid "Security" msgstr "Seguretat" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/network/WifiConnectionForm.jsx:116 msgid "WPA Password" msgstr "Contrasenya de WPA" -#. TRANSLATORS: button label, connect to a WiFi network -#. TRANSLATORS: menu label, connect to the selected WiFi network -#: src/components/network/WifiConnectionForm.jsx:133 -#: src/components/network/WifiNetworkMenu.jsx:58 -msgid "Connect" -msgstr "Connecta't" - -#. TRANSLATORS: button label -#: src/components/network/WifiHiddenNetworkForm.jsx:50 -msgid "Connect to hidden network" -msgstr "Connecta't a una xarxa oculta" - #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworkListItem.jsx:34 -#: src/components/network/WifiNetworkListItem.jsx:94 +#: src/components/network/WifiNetworksListPage.jsx:51 +#: src/components/network/WifiNetworksListPage.jsx:111 msgid "Connecting" msgstr "Connectant" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworkListItem.jsx:37 +#: src/components/network/WifiNetworksListPage.jsx:54 +#: src/components/network/WifiNetworksListPage.jsx:115 +#: src/components/network/WifiNetworksListPage.jsx:154 msgid "Connected" msgstr "Connectat" -#. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworkListItem.jsx:40 -msgid "Disconnecting" -msgstr "Desconnectant" - -#: src/components/network/WifiNetworkListItem.jsx:42 -msgid "Failed" -msgstr "Ha fallat" - #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworkListItem.jsx:45 +#: src/components/network/WifiNetworksListPage.jsx:59 +#: src/components/network/WifiNetworksListPage.jsx:113 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" msgstr "Desconnectat" -#. TRANSLATORS: %s is replaced by a WiFi network name -#: src/components/network/WifiNetworkListItem.jsx:92 -#, c-format -msgid "%s connection is waiting for an state change" -msgstr "La connexió %s espera un canvi d'estat." - -#. TRANSLATORS: menu label, disconnect from the selected WiFi network -#: src/components/network/WifiNetworkMenu.jsx:67 +#: src/components/network/WifiNetworksListPage.jsx:121 msgid "Disconnect" msgstr "Desconnecta" -#. TRANSLATORS: menu label, remove the selected WiFi network settings -#: src/components/network/WifiNetworkMenu.jsx:76 -msgid "Forget network" -msgstr "Oblida la xarxa" +#: src/components/network/WifiNetworksListPage.jsx:142 +msgid "Connect to a hidden network" +msgstr "Connecta't a una xarxa oculta" + +#: src/components/network/WifiNetworksListPage.jsx:153 +msgid "configured" +msgstr "configurat" + +#: src/components/network/WifiNetworksListPage.jsx:244 +msgid "Connect to hidden network" +msgstr "Connecta't a una xarxa oculta" + +#: src/components/network/WifiSelectorPage.jsx:131 +msgid "Connect to a Wi-Fi network" +msgstr "Connecteu-vos a una xarxa Wi-Fi" #. TRANSLATORS: %s will be replaced by a language name and territory, example: #. "English (United States)". @@ -718,9 +729,9 @@ msgstr "Usuaris" #: src/components/overview/OverviewPage.jsx:46 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:266 +#: src/components/storage/ProposalPage.jsx:290 #: src/components/storage/StoragePage.jsx:30 -#: src/components/storage/routes.js:57 +#: src/components/storage/routes.js:58 msgid "Storage" msgstr "Emmagatzematge" @@ -839,7 +850,7 @@ msgstr "" "amb una estratègia personalitzada per trobar l'espai necessari." #: src/components/overview/StorageSection.jsx:175 -#: src/components/storage/InstallationDeviceField.jsx:67 +#: src/components/storage/InstallationDeviceField.jsx:63 msgid "No device selected yet" msgstr "Encara no s'ha seleccionat cap dispositiu." @@ -984,7 +995,7 @@ msgstr "" msgid "Installation will configure partitions for booting at %s." msgstr "La instal·lació configurarà les particions per arrencar a %s." -#: src/components/storage/BootSelection.jsx:126 +#: src/components/storage/BootSelection.jsx:127 msgid "" "To ensure the new system is able to boot, the installer may need to create " "or configure some partitions in the appropriate disk." @@ -992,43 +1003,43 @@ msgstr "" "Per garantir que el sistema nou pugui arrencar, és possible que " "l'instal·lador hagi de crear o configurar algunes particions al disc adequat." -#: src/components/storage/BootSelection.jsx:132 +#: src/components/storage/BootSelection.jsx:133 msgid "Partitions to boot will be allocated at the installation disk." msgstr "Les particions per a l'arrencada s'assignaran al disc d'instal·lació." #. TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") -#: src/components/storage/BootSelection.jsx:137 +#: src/components/storage/BootSelection.jsx:138 #, c-format msgid "Partitions to boot will be allocated at the installation disk (%s)." msgstr "" "Les particions per a l'arrencada s'assignaran al disc d'instal·lació (%s)." -#: src/components/storage/BootSelection.jsx:153 +#: src/components/storage/BootSelection.jsx:154 msgid "Select booting partition" msgstr "Seleccioneu la partició d'arrencada" -#: src/components/storage/BootSelection.jsx:167 +#: src/components/storage/BootSelection.jsx:170 #: src/components/storage/iscsi/NodeStartupOptions.js:27 msgid "Automatic" msgstr "Automàtica" -#: src/components/storage/BootSelection.jsx:176 +#: src/components/storage/BootSelection.jsx:183 msgid "Select a disk" msgstr "Seleccioneu un disc" -#: src/components/storage/BootSelection.jsx:180 +#: src/components/storage/BootSelection.jsx:189 msgid "Partitions to boot will be allocated at the following device." msgstr "Les particions per a l'arrencada s'assignaran al dispositiu següent." -#: src/components/storage/BootSelection.jsx:183 +#: src/components/storage/BootSelection.jsx:192 msgid "Choose a disk for placing the boot loader" msgstr "Trieu un disc per posar-hi el carregador d'arrencada" -#: src/components/storage/BootSelection.jsx:199 +#: src/components/storage/BootSelection.jsx:210 msgid "Do not configure" msgstr "No ho configuris" -#: src/components/storage/BootSelection.jsx:202 +#: src/components/storage/BootSelection.jsx:215 msgid "" "No partitions will be automatically configured for booting. Use with caution." msgstr "" @@ -1376,28 +1387,28 @@ msgstr "" msgid "Encrypt the system" msgstr "Encripta el sistema" -#: src/components/storage/InstallationDeviceField.jsx:40 +#: src/components/storage/InstallationDeviceField.jsx:36 #: src/components/storage/VolumeLocationSelectorTable.jsx:58 msgid "Installation device" msgstr "Dispositiu d'instal·lació" #. TRANSLATORS: The storage "Installation device" field's description. -#: src/components/storage/InstallationDeviceField.jsx:42 +#: src/components/storage/InstallationDeviceField.jsx:38 msgid "Main disk or LVM Volume Group for installation." msgstr "Disc principal o grup de volums d'LVM per a la instal·lació." #. TRANSLATORS: %s is the installation disk (eg. "/dev/sda, 80 GiB) -#: src/components/storage/InstallationDeviceField.jsx:56 +#: src/components/storage/InstallationDeviceField.jsx:52 #, c-format msgid "File systems created as new partitions at %s" msgstr "Sistemes de fitxers creats com a particions noves a %s" -#: src/components/storage/InstallationDeviceField.jsx:59 +#: src/components/storage/InstallationDeviceField.jsx:55 msgid "File systems created at a new LVM volume group" msgstr "Sistemes de fitxers creats en un nou grup de volums d'LVM" #. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) -#: src/components/storage/InstallationDeviceField.jsx:63 +#: src/components/storage/InstallationDeviceField.jsx:59 #, c-format msgid "File systems created at a new LVM volume group on %s" msgstr "Sistemes de fitxers creats en un nou grup de volums d'LVM a %s" @@ -1610,7 +1621,7 @@ msgstr "Taula amb punts de muntatge" #: src/components/storage/PartitionsField.jsx:566 #: src/components/storage/PartitionsField.jsx:585 -#: src/components/storage/VolumeDialog.jsx:83 +#: src/components/storage/VolumeDialog.jsx:81 msgid "Add file system" msgstr "Afegeix-hi un sistema de fitxers" @@ -1654,47 +1665,81 @@ msgid_plural "Show %d subvolume actions" msgstr[0] "Mostra %d acció de subvolum" msgstr[1] "Mostra %d accions de subvolum" -#: src/components/storage/ProposalResultSection.jsx:64 +#: src/components/storage/ProposalActionsSummary.jsx:57 +msgid "Destructive actions are not allowed" +msgstr "No es permeten accions destructives." + +#: src/components/storage/ProposalActionsSummary.jsx:59 +msgid "Destructive actions are allowed" +msgstr "Es permeten accions destructives." + +#: src/components/storage/ProposalActionsSummary.jsx:66 #, c-format msgid "There is %d destructive action planned" msgid_plural "There are %d destructive actions planned" msgstr[0] "Hi ha %d acció destructiva planificada." msgstr[1] "Hi ha %d accions destructives planificades." -#: src/components/storage/ProposalResultSection.jsx:80 -msgid "Affecting" +#: src/components/storage/ProposalActionsSummary.jsx:79 +#: src/components/storage/ProposalActionsSummary.jsx:126 +msgid "affecting" msgstr "Això afecta" -#: src/components/storage/ProposalResultSection.jsx:96 +#: src/components/storage/ProposalActionsSummary.jsx:107 +msgid "Shrinking partitions is not allowed" +msgstr "No es permet encongir particions." + +#: src/components/storage/ProposalActionsSummary.jsx:111 +msgid "Shrinking partitions is allowed" +msgstr "Es permet encongir particions." + +#: src/components/storage/ProposalActionsSummary.jsx:113 +msgid "Shrinking some partitions is allowed but not needed" +msgstr "Es permet encongir algunes particions, però no cal." + +#: src/components/storage/ProposalActionsSummary.jsx:116 +#, c-format +msgid "%d partition will be shrunk" +msgid_plural "%d partitions will be shrunk" +msgstr[0] "S'encongirà %d partició." +msgstr[1] "S'encongiran %d particions." + +#: src/components/storage/ProposalActionsSummary.jsx:151 +msgid "Cannot accommodate the required file systems for installation" +msgstr "" +"No es poden acomodar els sistemes de fitxers necessaris per a la " +"instal·lació." + +#: src/components/storage/ProposalActionsSummary.jsx:160 #, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" msgstr[0] "Marca l'acció planificada" msgstr[1] "Marca les %d accions planificades" -#: src/components/storage/ProposalResultSection.jsx:111 +#: src/components/storage/ProposalActionsSummary.jsx:179 +msgid "Waiting for actions information..." +msgstr "Esperant la informació de les accions..." + +#: src/components/storage/ProposalPage.jsx:314 +msgid "Planned Actions" +msgstr "Accions planificades" + +#: src/components/storage/ProposalResultSection.jsx:42 msgid "Waiting for information about storage configuration" msgstr "Esperant informació sobre la configuració de l'emmagatzematge" -#: src/components/storage/ProposalResultSection.jsx:124 -msgid "Storage proposal not possible" -msgstr "La proposta d'emmagatzematge no és possible." +#: src/components/storage/ProposalResultSection.jsx:70 +msgid "Final layout" +msgstr "Disposició final" -#: src/components/storage/ProposalResultSection.jsx:188 -msgid "" -"During installation, some actions will be performed to configure the system " -"as displayed below." -msgstr "" -"Durant la instal·lació, es faran algunes accions per configurar el sistema " -"tal com es mostra a continuació." +#: src/components/storage/ProposalResultSection.jsx:71 +msgid "The systems will be configured as displayed below." +msgstr "Els sistemes es configuraran tal com es mostra a continuació." -#: src/components/storage/ProposalResultSection.jsx:196 -msgid "Planned Actions" -msgstr "Accions planificades" - -#: src/components/storage/ProposalResultSection.jsx:210 -msgid "Result" -msgstr "Resultat" +#: src/components/storage/ProposalResultSection.jsx:78 +msgid "Storage proposal not possible" +msgstr "La proposta d'emmagatzematge no és possible." #: src/components/storage/ProposalResultTable.jsx:74 msgid "New" @@ -1766,99 +1811,82 @@ msgstr "Acció" msgid "Actions to find space" msgstr "Accions per aconseguir espai" -#: src/components/storage/SpacePolicyDialog.jsx:143 -msgid "" -"Allocating the file systems might need to find free space in the devices " -"listed below. Choose how to do it." -msgstr "" -"L'assignació dels sistemes de fitxers pot necessitar trobar espai lliure als " -"dispositius que s'indiquen a continuació. Trieu com fer-ho." - -#: src/components/storage/SpacePolicyDialog.jsx:148 -#: src/components/storage/SpacePolicyField.jsx:86 -msgid "Find space" -msgstr "Troba espai" - -#: src/components/storage/SpacePolicyField.jsx:88 -msgid "" -"Allocating the file systems might need to find free space in the " -"installation device(s)." -msgstr "" -"L'assignació dels sistemes de fitxers pot necessitar trobar espai lliure als " -"dispositius d'instal·lació." +#: src/components/storage/SpacePolicySelection.jsx:170 +msgid "Space policy" +msgstr "Política espacial" -#: src/components/storage/VolumeDialog.jsx:80 +#: src/components/storage/VolumeDialog.jsx:78 #, c-format msgid "Add %s file system" msgstr "Afegeix-hi un sistema de fitxers %s" -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/VolumeDialog.jsx:79 #, c-format msgid "Edit %s file system" msgstr "Edita el sistema de fitxers %s" -#: src/components/storage/VolumeDialog.jsx:83 +#: src/components/storage/VolumeDialog.jsx:81 msgid "Edit file system" msgstr "Edita el sistema de fitxers" #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:98 +#: src/components/storage/VolumeDialog.jsx:96 msgid "The type and size of the file system cannot be edited." msgstr "El tipus i la mida del sistema de fitxers no es poden editar." #. TRANSLATORS: Description of a warning. The first %s is replaced by a device name (e.g., #. /dev/vda) and the second %s is replaced by a mount path (e.g., /home). -#: src/components/storage/VolumeDialog.jsx:101 +#: src/components/storage/VolumeDialog.jsx:99 #, c-format msgid "The current file system on %s is selected to be mounted at %s." msgstr "El sistema de fitxers actual a %s està seleccionat per muntar-lo a %s." #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:107 +#: src/components/storage/VolumeDialog.jsx:105 msgid "The size of the file system cannot be edited" msgstr "La mida del sistema de fitxers no es pot editar." #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). -#: src/components/storage/VolumeDialog.jsx:109 +#: src/components/storage/VolumeDialog.jsx:107 #, c-format msgid "The file system is allocated at the device %s." msgstr "El sistema de fitxers s'assigna al dispositiu %s." -#: src/components/storage/VolumeDialog.jsx:154 +#: src/components/storage/VolumeDialog.jsx:152 msgid "A mount point is required" msgstr "Cal un punt de muntatge." -#: src/components/storage/VolumeDialog.jsx:181 +#: src/components/storage/VolumeDialog.jsx:179 msgid "The mount point is invalid" msgstr "El punt de muntatge no és vàlid." -#: src/components/storage/VolumeDialog.jsx:209 +#: src/components/storage/VolumeDialog.jsx:207 msgid "A size value is required" msgstr "Cal un valor de mida" -#: src/components/storage/VolumeDialog.jsx:237 +#: src/components/storage/VolumeDialog.jsx:235 msgid "Minimum size is required" msgstr "Cal una mida mínima" -#: src/components/storage/VolumeDialog.jsx:269 +#: src/components/storage/VolumeDialog.jsx:267 msgid "Maximum must be greater than minimum" msgstr "El màxim ha de ser superior al mínim." -#: src/components/storage/VolumeDialog.jsx:311 +#: src/components/storage/VolumeDialog.jsx:309 #, c-format msgid "There is already a file system for %s." msgstr "Ja hi ha un sistema de fitxers per a %s." -#: src/components/storage/VolumeDialog.jsx:313 +#: src/components/storage/VolumeDialog.jsx:311 msgid "Do you want to edit it?" msgstr "El voleu editar?" -#: src/components/storage/VolumeDialog.jsx:358 +#: src/components/storage/VolumeDialog.jsx:356 #, c-format msgid "There is a predefined file system for %s." msgstr "Hi ha un sistema de fitxers predefinit per a %s." -#: src/components/storage/VolumeDialog.jsx:360 +#: src/components/storage/VolumeDialog.jsx:358 msgid "Do you want to add it?" msgstr "L'hi voleu afegir?" @@ -2433,8 +2461,8 @@ msgstr "Nom complet" #: src/components/users/FirstUser.jsx:55 #: src/components/users/FirstUserForm.jsx:224 -#: src/components/users/FirstUserForm.jsx:230 -#: src/components/users/FirstUserForm.jsx:233 +#: src/components/users/FirstUserForm.jsx:229 +#: src/components/users/FirstUserForm.jsx:232 msgid "Username" msgstr "Nom d'usuari" @@ -2474,16 +2502,16 @@ msgstr "Edita l'usuari" msgid "User full name" msgstr "Nom complet de l'usuari" -#: src/components/users/FirstUserForm.jsx:255 +#: src/components/users/FirstUserForm.jsx:254 msgid "Edit password too" msgstr "Edita també la contrasenya" -#: src/components/users/FirstUserForm.jsx:270 +#: src/components/users/FirstUserForm.jsx:269 msgid "user autologin" msgstr "entrada de sessió automàtica de l'usuari" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:274 +#: src/components/users/FirstUserForm.jsx:273 msgid "Auto-login" msgstr "Entrada automàtica" @@ -2582,13 +2610,63 @@ msgstr "Usuari primer" msgid "Root authentication" msgstr "Autenticació d'arrel" -#: src/components/users/routes.js:41 -msgid "Create or edit the first user" -msgstr "Creeu o editeu l'usuari primer" +#~ msgid "Choose a language" +#~ msgstr "Trieu la llengua" + +#~ msgid "No WiFi connections found." +#~ msgstr "No s'ha trobat cap connexió WiFi." + +#~ msgid "Wired connections" +#~ msgstr "Connexions amb fil" + +#~ msgid "WiFi connections" +#~ msgstr "Connexions WiFi" + +#~ msgid "Disconnecting" +#~ msgstr "Desconnectant" -#: src/components/users/routes.js:48 -msgid "Edit first user" -msgstr "Edita l'usuari primer" +#~ msgid "Failed" +#~ msgstr "Ha fallat" + +#, c-format +#~ msgid "%s connection is waiting for an state change" +#~ msgstr "La connexió %s espera un canvi d'estat." + +#~ msgid "Forget network" +#~ msgstr "Oblida la xarxa" + +#~ msgid "Create or edit the first user" +#~ msgstr "Creeu o editeu l'usuari primer" + +#~ msgid "Edit first user" +#~ msgstr "Edita l'usuari primer" + +#~ msgid "" +#~ "During installation, some actions will be performed to configure the " +#~ "system as displayed below." +#~ msgstr "" +#~ "Durant la instal·lació, es faran algunes accions per configurar el " +#~ "sistema tal com es mostra a continuació." + +#~ msgid "Result" +#~ msgstr "Resultat" + +#~ msgid "" +#~ "Allocating the file systems might need to find free space in the devices " +#~ "listed below. Choose how to do it." +#~ msgstr "" +#~ "L'assignació dels sistemes de fitxers pot necessitar trobar espai lliure " +#~ "als dispositius que s'indiquen a continuació. Trieu com fer-ho." + +#~ msgid "Find space" +#~ msgstr "Troba espai" + +#~ msgid "" +#~ "Allocating the file systems might need to find free space in the " +#~ "installation device(s)." +#~ msgstr "" +#~ "L'assignació dels sistemes de fitxers pot necessitar trobar espai lliure " +#~ "als dispositius d'instal·lació." #~ msgid "Analyze disks" #~ msgstr "Analitza els discs" @@ -2843,9 +2921,6 @@ msgstr "Edita l'usuari primer" #~ msgid "deleting all content of the %d selected disks" #~ msgstr "suprimint tot el contingut dels %d discs seleccionats." -#~ msgid "shrinking partitions of the installation device" -#~ msgstr "encongint les particions del dispositiu d'instal·lació." - #, c-format #~ msgid "shrinking partitions of the %d selected disks" #~ msgstr "encongint les particions dels %d discs seleccionats." @@ -3022,10 +3097,6 @@ msgstr "Edita l'usuari primer" #~ msgid "Current content" #~ msgstr "Contingut actual" -#, c-format -#~ msgid "%s partition table" -#~ msgstr "Taula de particions %s" - #~ msgid "EFI system partition" #~ msgstr "Partició de sistema EFI" diff --git a/web/po/cs.po b/web/po/cs.po index f10e2540e8..81a120ed48 100644 --- a/web/po/cs.po +++ b/web/po/cs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-23 02:24+0000\n" +"POT-Creation-Date: 2024-06-30 02:27+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: Ladislav Slezák \n" "Language-Team: Czech

    +
    {details}
    } - value={JSON.stringify(timezone)} - defaultChecked={timezone.id === selected} + value={id} + isChecked={id === selected} /> ); }); diff --git a/web/src/routes/l10n.js b/web/src/routes/l10n.js index 0df326b33a..b6d26c10ff 100644 --- a/web/src/routes/l10n.js +++ b/web/src/routes/l10n.js @@ -28,7 +28,6 @@ import { localesQuery, keymapsQuery, timezonesQuery, - useL10nConfigChanges } from "~/queries/l10n"; import { N_ } from "~/i18n"; @@ -48,13 +47,12 @@ const l10nLoader = async () => { const keymap = keymaps.find((k) => k.id === keymapId); const timezone = timezones.find((t) => t.id === timezoneId); - return { locale, keymap, timezone }; + return { locales, locale, keymaps, keymap, timezones, timezone }; }; const routes = { path: L10N_PATH, element: , - loader: l10nLoader, handle: { name: N_("Localization"), icon: "globe" @@ -62,18 +60,22 @@ const routes = { children: [ { index: true, + loader: l10nLoader, element: }, { path: LOCALE_SELECTION_PATH, + loader: l10nLoader, element: , }, { path: KEYMAP_SELECTION_PATH, + loader: l10nLoader, element: , }, { path: TIMEZONE_SELECTION_PATH, + loader: l10nLoader, element: , } ] From a8543eba036676caf6a63d9ecc2d1a1e280a4a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 3 Jul 2024 17:16:05 +0100 Subject: [PATCH 096/430] fix(web): listen for L10n changes from App --- web/src/App.jsx | 2 ++ web/src/components/l10n/L10nPage.jsx | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/src/App.jsx b/web/src/App.jsx index edee167beb..bef32b5860 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -30,6 +30,7 @@ import { useProduct } from "./context/product"; import { CONFIG, INSTALL, STARTUP } from "~/client/phase"; import { BUSY } from "~/client/status"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useL10nConfigChanges } from "~/queries/l10n"; const queryClient = new QueryClient(); @@ -45,6 +46,7 @@ function App() { const { connected, error, phase, status } = useInstallerClientStatus(); const { selectedProduct, products } = useProduct(); const { language } = useInstallerL10n(); + useL10nConfigChanges(); const Content = () => { if (error) return ; diff --git a/web/src/components/l10n/L10nPage.jsx b/web/src/components/l10n/L10nPage.jsx index c1a9a02db7..57a0c6b41e 100644 --- a/web/src/components/l10n/L10nPage.jsx +++ b/web/src/components/l10n/L10nPage.jsx @@ -23,9 +23,6 @@ import React from "react"; import { Gallery, GalleryItem, } from "@patternfly/react-core"; import { useLoaderData } from "react-router-dom"; import { ButtonLink, CardField, Page } from "~/components/core"; -import { - useL10nConfigChanges -} from "~/queries/l10n"; import { LOCALE_SELECTION_PATH, KEYMAP_SELECTION_PATH, TIMEZONE_SELECTION_PATH } from "~/routes/l10n"; import { _ } from "~/i18n"; @@ -48,7 +45,6 @@ const Section = ({ label, value, children }) => { * @component */ export default function L10nPage() { - useL10nConfigChanges(); const { locale, timezone, keymap } = useLoaderData(); return ( From bc6ff019c51ecb175a5fa1841a4de58237d4d61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 3 Jul 2024 17:16:43 +0100 Subject: [PATCH 097/430] fix(web): fix locales update --- web/src/components/l10n/LocaleSelection.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/l10n/LocaleSelection.jsx b/web/src/components/l10n/LocaleSelection.jsx index d256c0294a..9f143b76dd 100644 --- a/web/src/components/l10n/LocaleSelection.jsx +++ b/web/src/components/l10n/LocaleSelection.jsx @@ -40,7 +40,7 @@ export default function LocaleSelection() { const onSubmit = async (e) => { e.preventDefault(); - setConfig.mutate({ locales: selected }); + setConfig.mutate({ locales: [selected] }); navigate(-1); }; From ccaa0f07ce54f8f63f80c3577cb66f12395705fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 4 Jul 2024 10:44:49 +0100 Subject: [PATCH 098/430] fix(web): revalidate loader on config change By making use of https://reactrouter.com/en/main/hooks/use-revalidator See https://github.com/remix-run/react-router/discussions/10381#discussioncomment-5713606 too --- web/src/queries/l10n.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js index 1c288b3c67..bd7b9f9db0 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.js @@ -20,6 +20,7 @@ */ import React from "react"; +import { useRevalidator } from "react-router-dom"; import { useQueryClient, useMutation } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { timezoneUTCOffset } from "~/utils"; @@ -91,6 +92,7 @@ const useConfigMutation = () => { const useL10nConfigChanges = () => { const queryClient = useQueryClient(); const client = useInstallerClient(); + const revalidator = useRevalidator(); React.useEffect(() => { if (!client) return; @@ -98,9 +100,10 @@ const useL10nConfigChanges = () => { return client.ws().onEvent(event => { if (event.type === "L10nConfigChanged") { queryClient.invalidateQueries({ queryKey: ["l10n", "config"] }); + revalidator.revalidate(); } }); - }, [queryClient, client]); + }, [queryClient, client, revalidator]); }; export { From 396dfe92ea50919235bad67d49b549eafe588ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 10:55:07 +0100 Subject: [PATCH 099/430] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Knut Alejandro Anderssen González --- PACKAGING.md | 4 ++-- doc/obs_integration.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PACKAGING.md b/PACKAGING.md index 888eb16741..12272db0ce 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -2,7 +2,7 @@ This document summarizes the process we follow to build the Agama packages. -The Agama packages are available the +The Agama packages are available in the [systemsmanagement:Agama:Devel](https://build.opensuse.org/project/show/systemsmanagement:Agama:Devel) OBS project. These packages are automatically updated whenever the master branch is changed or when a new version is released. @@ -11,7 +11,7 @@ You can find more details the automatic OBS synchronization in the [obs_integration.md](doc/obs_integration.md) file. The process to build each package is slightly different depending on the technology we are using. -While the Ruby-based one (`rubygem-agama-yast`) is built as any other YaST package, Agama server +While the Ruby-based one (`rubygem-agama-yast`) is built as any other YaST package, the Agama server (`agama`), the CLI (`agama-cli`), and the web UI (`agama-web-ui`) rely on [OBS source services](https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.source_service.html). diff --git a/doc/obs_integration.md b/doc/obs_integration.md index ecf6e873d7..f4cef2bf34 100644 --- a/doc/obs_integration.md +++ b/doc/obs_integration.md @@ -47,7 +47,7 @@ the definition is shared in the [obs-staging-shared.yml]( The packages in the devel project are updated only when a respective source file is changed. That saves some resources for rebuilding and makes synchronization faster. But that also means the packages might not have exactly -same version. +the same version. The project to which the packages are submitted is configured in the `OBS_PROJECT` GitHub Actions variable. From f15926c617af15ad388a341aed9e8cf082ca1387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 4 Jul 2024 11:18:14 +0100 Subject: [PATCH 100/430] JSON schema improvements --- rust/agama-lib/share/profile.schema.json | 116 +++++++++++++++++------ 1 file changed, 88 insertions(+), 28 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 3534b3d443..721ed35267 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -262,26 +262,37 @@ "properties": { "target": { "title": "Target device", - "oneOf": [ + "anyOf": [ { "enum": ["disk", "newLvmVg"] }, { + "title": "Disk device for installing", "type": "object", "additionalProperties": false, - "properties": { "disk": { "type": "string" } }, - "required": ["disk"] + "required": ["disk"], + "properties": { + "disk": { + "title": "Disk device name (e.g., '/dev/vda')", + "type": "string" + } + } }, { + "title": "New LVM for installing", "type": "object", "additionalProperties": false, + "required": ["newLvmVg"], "properties": { "newLvmVg": { + "title": "Devices in which to create the physical volumes", "type": "array", - "items": { "type": "string" } + "items": { + "title": "Disk device name (e.g., '/dev/vda')", + "type": "string" + } } - }, - "required": ["newLvmVg"] + } } ] }, @@ -289,13 +300,15 @@ "title": "Configuration of the boot settings", "type": "object", "additionalProperties": false, + "required": ["configure"], "properties": { "configure": { "title": "Whether to configure partitions for booting", "type": "boolean" }, "device": { - "title": "Device to use for booting", + "title": "Device to use for booting (e.g., '/dev/vda')", + "description": "The installation device is used by default for booting", "type": "string" } } @@ -341,27 +354,32 @@ "required": ["policy", "actions"], "properties": { "actions": { + "title": "Actions to find space if policy is 'custom'", "type": "array", "items": { "anyOf": [ { + "title": "Delete device", + "description": "Force device deletion", "type": "object", "required": ["forceDelete"], "additionalProperties": false, "properties": { "forceDelete": { - "title": "Device to delete", + "title": "Device to delete (e.g., '/dev/vda')", "type": "string" } } }, { + "title": "Allow shinking", + "description": "Indicate the device can be shrunk in needed", "type": "object", "required": ["resize"], "additionalProperties": false, "properties": { "resize": { - "title": "Device to allow resizing", + "title": "Device to allow resizing (e.g., '/dev/vda')", "type": "string" } } @@ -390,6 +408,7 @@ "required": ["mount"], "properties": { "mount": { + "title": "Mount point", "type": "object", "additionalProperties": false, "required": ["path"], @@ -407,17 +426,20 @@ }, "filesystem": { "title": "File system of the volume", - "oneOf": [ + "anyOf": [ { - "description": "File system type", + "title": "File system type", "enum": [ "bcachefs", "btrfs", "exfat", "ext2", "ext3", "ext4", "f2fs", "jfs", "nfs", "nilfs2", "ntfs", "reiserfs", "swap", "tmpfs", "vfat", "xfs" ] }, { + "title": "Btrfs file system", + "description": "Indicates properties of the Btrfs file system", "type": "object", "additionalProperties": false, + "required": ["btrfs"], "properties": { "btrfs": { "title": "Specification of a Btrfs file system", @@ -430,18 +452,25 @@ } } } - }, - "required": ["btrfs"] + } } ] }, "size": { "title": "Size limits", - "oneOf": [ - { "const": "auto" }, - { "$ref": "#/$defs/sizeString" }, - { "$ref": "#/$defs/sizeInteger" }, + "description": "Options to indicate the size of a device", + "anyOf": [ + { + "title": "Automatic size", + "description": "The size is auto calculated according to the product", + "const": "auto" + }, + { + "title": "Size unit", + "$ref": "#/$defs/sizeValue" + }, { + "title": "Size range (e.g., [1024, '2 GiB'])", "description": "Lower size limit and optionally upper size limit", "type": "array", "items": { @@ -451,6 +480,7 @@ "maxItems": 2 }, { + "title": "Size range", "type": "object", "additionalProperties": false, "properties": { @@ -468,34 +498,63 @@ ] }, "target": { - "title": "Location of the resulting file system", - "oneOf": [ + "title": "Location of the file system", + "description": "Options to indicate the location of a file system", + "anyOf": [ { "const": "default" }, { + "title": "New partition", + "description": "The file system is created over a new partition", "type": "object", + "required": ["newPartition"], "additionalProperties": false, - "properties": { "newPartition": { "type": "string" } }, - "required": ["newPartition"] + "properties": { + "newPartition": { + "title": "Name of a disk device (e.g., '/dev/vda')", + "type": "string" + } + } }, { + "title": "Dedicated LVM volume group", + "description": "The file system is created over a dedicated LVM", "type": "object", "additionalProperties": false, - "properties": { "newVg": { "type": "string" } }, - "required": ["newVg"] + "required": ["newVg"], + "properties": { + "newVg": { + "title": "Name of a disk device (e.g., '/dev/vda')", + "type": "string" + } + } }, { + "title": "Re-used existing device", + "description": "The file system is created over an existing device", "type": "object", "additionalProperties": false, - "properties": { "device": { "type": "string" } }, - "required": ["device"] + "required": ["device"], + "properties": { + "device": { + "title": "Name of a device (e.g., '/dev/vda1')", + "type": "string" + } + } }, { + "title": "Re-used existing file system", + "description": "An existing file system is reused (without formatting)", "type": "object", "additionalProperties": false, - "properties": { "filesystem": { "type": "string" } }, - "required": ["filesystem"] + "required": ["filesystem"], + "properties": { + "filesystem": { + "title": "Name of a device containing the file system (e.g., '/dev/vda1')", + "type": "string" + } + } } ] } @@ -508,6 +567,7 @@ }, "legacyAutoyastStorage": { "title": "Legacy AutoYaST storage settings", + "description": "Accepts all options of the AutoYaST profile (XML to JSON)", "type": "object" } }, @@ -523,7 +583,7 @@ "minimum": 0 }, "sizeValue": { - "oneOf": [ + "anyOf": [ { "$ref": "#/$defs/sizeString" }, { "$ref": "#/$defs/sizeInteger" } ] From 3022fbf22863c74c90084f2b9ad18695aaea8e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 4 Jul 2024 11:36:12 +0100 Subject: [PATCH 101/430] Fix profile example --- rust/agama-lib/share/examples/profile_tw.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rust/agama-lib/share/examples/profile_tw.json b/rust/agama-lib/share/examples/profile_tw.json index 06a1d9215b..ab3f5d564f 100644 --- a/rust/agama-lib/share/examples/profile_tw.json +++ b/rust/agama-lib/share/examples/profile_tw.json @@ -12,7 +12,12 @@ "id": "Tumbleweed" }, "storage": { - "bootDevice": "/dev/dm-1" + "guided": { + "boot": { + "configure": true, + "device": "/dev/dm-1" + } + } }, "user": { "fullName": "Jane Doe", From 8c028391f6a2748ee2150cfe60d7e10d4a1e68ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 4 Jul 2024 11:36:28 +0100 Subject: [PATCH 102/430] Add storage config example --- rust/agama-lib/share/examples/storage.json | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 rust/agama-lib/share/examples/storage.json diff --git a/rust/agama-lib/share/examples/storage.json b/rust/agama-lib/share/examples/storage.json new file mode 100644 index 0000000000..573aed64d2 --- /dev/null +++ b/rust/agama-lib/share/examples/storage.json @@ -0,0 +1,63 @@ +{ + "storage": { + "guided": { + "target": { + "disk": "/dev/vdc" + }, + "boot": { + "configure": true, + "device": "/dev/vda" + }, + "encryption": { + "password": "notsecret", + "method": "luks2", + "pbkdFunction": "argon2i" + }, + "space": { + "policy": "custom", + "actions": [ + { "resize": "/dev/vda" }, + { "forceDelete": "/dev/vdb1" } + ] + }, + "volumes": [ + { + "mount": { + "path": "/", + "options": ["ro"] + }, + "filesystem": { + "btrfs": { + "snapshots": true + } + }, + "size": [1024, "5 Gib"], + "target": "default" + }, + { + "mount": { + "path": "/home" + }, + "filesystem": "xfs", + "size": { + "min": "5 GiB", + "max": "20 GiB" + }, + "target": { + "newVg": "/dev/vda" + } + }, + { + "mount": { + "path": "swap" + }, + "filesystem": "swap", + "size": "8 GiB", + "target": { + "newPartition": "/dev/vda" + } + } + ] + } + } +} From 9d66b85ae163f2f132d23f21068ce695571e8bfd Mon Sep 17 00:00:00 2001 From: Lubos Kocman Date: Thu, 4 Jul 2024 13:28:26 +0200 Subject: [PATCH 103/430] Update src/live to match agama-installer-openSUSE - Update README and PXE to reference to agama-installer-openSUSE - Update PXE instructions to create 25GB image instead of 20 --- live/Makefile | 2 +- live/PXE.md | 12 ++++++------ live/README.md | 2 +- ...live.changes => agama-installer-openSUSE.changes} | 12 ++++++++++++ ...agama-live.kiwi => agama-installer-openSUSE.kiwi} | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) rename live/src/{agama-live.changes => agama-installer-openSUSE.changes} (94%) rename live/src/{agama-live.kiwi => agama-installer-openSUSE.kiwi} (99%) diff --git a/live/Makefile b/live/Makefile index 85b1fc2f2a..d1609f0db3 100644 --- a/live/Makefile +++ b/live/Makefile @@ -52,7 +52,7 @@ $(DESTDIR)/%.tar.xz: % $$(shell find % -type f,l) # build the ISO locally build: $(DESTDIR) - if [ ! -e $(DESTDIR)/.osc ]; then make clean; osc co -o $(DESTDIR) $(OBS_PROJECT) agama-live; fi + if [ ! -e $(DESTDIR)/.osc ]; then make clean; osc co -o $(DESTDIR) $(OBS_PROJECT) agama-installer-openSUSE; fi $(MAKE) all (cd $(DESTDIR) && osc build -M $(FLAVOR) images) diff --git a/live/PXE.md b/live/PXE.md index f5e793d3bb..53df26a495 100644 --- a/live/PXE.md +++ b/live/PXE.md @@ -14,10 +14,10 @@ Extract the Linux kernel and the initrd from the archive: ```shell osc getbinaries images x86_64 -M ALP-PXE tar -C /srv/ftp/image -xf \ - binaries/agama-live.x86_64-5.0.0-ALP-PXE-Build4.1.install.tar + binaries/agama-installer-openSUSE.x86_64-5.0.0-ALP-PXE-Build4.1.install.tar -cp /srv/ftp/image/pxeboot.agama-live.x86_64-5.0.0.initrd /srv/tftpboot/boot -cp /srv/ftp/image/pxeboot.agama-live.x86_64-5.0.0.kernel /srv/tftpboot/boot +cp /srv/ftp/image/pxeboot.agama-installer-openSUSE.x86_64-9.0.0.initrd /srv/tftpboot/boot +cp /srv/ftp/image/pxeboot.agama-installer-openSUSE.x86_64-9.0.0.kernel /srv/tftpboot/boot ``` Update the PXE boot configuration in the `/srv/tftpboot/pxelinux.cfg/default` @@ -32,8 +32,8 @@ menu title PXE Menu label live menu label ^Agama - kernel /boot/pxeboot.agama-live.x86_64-5.0.0.kernel - append initrd=/boot/pxeboot.agama-live.x86_64-5.0.0.initrd rd.kiwi.install.pxe rd.kiwi.install.image=ftp://X.X.X.X/image/agama-live.x86_64-5.0.0.xz console=ttyS0,115200 rd.kiwi.ramdisk ramdisk_size=2097152 + kernel /boot/pxeboot.agama-installer-openSUSE.x86_64-9.0.0.kernel + append initrd=/boot/pxeboot.agama-installer-openSUSE.x86_64-9.0.0.initrd rd.kiwi.install.pxe rd.kiwi.install.image=ftp://X.X.X.X/image/agama-installer-openSUSE.x86_64-9.0.0.xz console=ttyS0,115200 rd.kiwi.ramdisk ramdisk_size=2097152 ``` ## Testing @@ -41,6 +41,6 @@ label live To test booting Agama in QEMU run these commands: ```shell -qemu-img create mydisk 20g +qemu-img create mydisk 25g qemu -boot n -m 4096 -hda mydisk ``` diff --git a/live/README.md b/live/README.md index b23f7034cb..9dde7de7be 100644 --- a/live/README.md +++ b/live/README.md @@ -103,7 +103,7 @@ build workflow and the `.kiwi` file format. The main Kiwi source files are located in the [src](src) subdirectory: -- [agama-live.kiwi](src/agama-live.kiwi) is the main KIWI file which drives the ISO image build. +- [agama-installer-openSUSE.kiwi](src/agama-installer-openSUSE.kiwi) is the main KIWI file which drives the ISO image build. - [config.sh](src/config.sh) is a KIWI hook script which is called and the end of the build process, after all packages are installed but before compressing and building the image. The script runs in the image chroot and is usually used to adjust the system configuration (enable/disable services, diff --git a/live/src/agama-live.changes b/live/src/agama-installer-openSUSE.changes similarity index 94% rename from live/src/agama-live.changes rename to live/src/agama-installer-openSUSE.changes index 6f93c64851..10f9e9635c 100644 --- a/live/src/agama-live.changes +++ b/live/src/agama-installer-openSUSE.changes @@ -1,3 +1,15 @@ +------------------------------------------------------------------- +Thu Jul 4 11:24:47 UTC 2024 - Lubos Kocman + +- Update src/live to match rename to agama-installer-openSUSE + fixes issue that bot updates wrong spec/changes file + +- Update README and PXE to reference to agama-installer-openSUSE + +- Update PXE instructions to create 25GB image instead of 20 + Current agama can't deploy e.g. Leap 16 on 20GB disk image + with default layout including swap + ------------------------------------------------------------------- Wed Jul 3 10:41:32 UTC 2024 - Knut Anderssen diff --git a/live/src/agama-live.kiwi b/live/src/agama-installer-openSUSE.kiwi similarity index 99% rename from live/src/agama-live.kiwi rename to live/src/agama-installer-openSUSE.kiwi index 867fb160c8..5814e63a74 100644 --- a/live/src/agama-live.kiwi +++ b/live/src/agama-installer-openSUSE.kiwi @@ -2,7 +2,7 @@ - + YaST Team yast2-maintainers@suse.de From c0367bea31b6dcc7903a2ce69a3f9ee4696d309e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 13:08:28 +0100 Subject: [PATCH 104/430] fix(web): add a role to CardField * The label is not properly set yet. --- web/src/components/core/CardField.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/components/core/CardField.jsx b/web/src/components/core/CardField.jsx index 5a2ca1b7c0..e38356a010 100644 --- a/web/src/components/core/CardField.jsx +++ b/web/src/components/core/CardField.jsx @@ -48,8 +48,9 @@ const CardField = ({ cardDescriptionProps = {} }) => { + // TODO: replace aria-label with the proper aria-labelledby return ( - + From 122bf2029d80ed410c93d3d520afd64c7b702b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 13:14:42 +0100 Subject: [PATCH 105/430] test(web): write a new test for L10nPage --- web/src/components/l10n/L10nPage.test.jsx | 385 +++------------------- 1 file changed, 45 insertions(+), 340 deletions(-) diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.jsx index 72613de5d5..8c9268da25 100644 --- a/web/src/components/l10n/L10nPage.test.jsx +++ b/web/src/components/l10n/L10nPage.test.jsx @@ -20,377 +20,82 @@ */ import React from "react"; -import { render, screen, waitFor, within } from "@testing-library/react"; - -import { installerRender, plainRender, queryRender } from "~/test-utils"; +import { render, screen, within } from "@testing-library/react"; import L10nPage from "~/components/l10n/L10nPage"; -import { QueryClient } from "@tanstack/query-core"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { MemoryRouter } from "react-router"; - -const locales = [ - { id: "de_DE.UTF8", name: "German", territory: "Germany" }, - { id: "en_US.UTF8", name: "English", territory: "United States" }, - { id: "es_ES.UTF8", name: "Spanish", territory: "Spain" } -]; -const keymaps = [ - { id: "de", name: "German" }, - { id: "us", name: "English" }, - { id: "es", name: "Spanish" } -]; +let mockLoadedData; -const timezones = [ - { id: "asia/bangkok", parts: ["Asia", "Bangkok"] }, - { id: "atlantic/canary", parts: ["Atlantic", "Canary"] }, - { id: "america/new_york", parts: ["Americas", "New York"] } -]; - -jest.mock("~/queries/l10n", () => ({ - localesQuery: () => ({ - queryKey: ["l10n", "locales"], - queryFn: jest.fn().mockResolvedValue(locales) - }), - timezonesQuery: () => ({ - queryKey: ["l10n", "timezones"], - queryFn: jest.fn().mockResolvedValue(timezones) - }), - keymapsQuery: () => ({ - queryKey: ["l10n", "keymaps"], - queryFn: jest.fn().mockResolvedValue(keymaps) - }), - configQuery: () => ({ - queryKey: ["l10n", "config"], - queryFn: jest.fn().mockResolvedValue({ - locales: mockSelectedLocales, - timezone: mockSelectedTimezone, - keymap: mockSelectedKeymap - }) - }), - useL10nConfigChanges: jest.fn() +jest.mock('react-router-dom', () => ({ + ...jest.requireActual("react-router-dom"), + useLoaderData: () => mockLoadedData, + // TODO: mock the link because it needs a working router. + Link: ({ children }) => })); -let mockL10nClient; -let mockSelectedLocales; -let mockSelectedKeymap; -let mockSelectedTimezone; - beforeEach(() => { - mockSelectedLocales = []; - mockSelectedKeymap = undefined; - mockSelectedTimezone = undefined; + mockLoadedData = { + locale: { id: "en_US.UTF-8", name: "English", territory: "United States" }, + keymap: { id: "us", name: "English" }, + timezone: { id: "Europe/Berlin", parts: ["Europe", "Berlin"]} + }; }); -it.only("renders a section for configuring the language", async () => { - queryRender(); - await screen.findByText("Language"); +it("renders a section for configuring the language", () => { + render(); + const region = screen.getByRole("region", { name: "Language" }) + within(region).getByText("English - United States"), + within(region).getByText("Change"); }); -describe.skip("if there is no selected language", () => { +describe("if there is no selected language", () => { beforeEach(() => { - mockSelectedLocales = []; + mockLoadedData.locale = undefined; }); it("renders a button for selecting a language", () => { - plainRender(); - screen.getByText("Language not selected yet"); - screen.getByRole("button", { name: "Select language" }); - }); -}); - -describe.skip("if there is a selected language", () => { - beforeEach(() => { - mockSelectedLocales = [{ id: "es_ES.UTF8", name: "Spanish", territory: "Spain" }]; - }); - - it("renders a button for changing the language", () => { - plainRender(); - screen.getByText("Spanish - Spain"); - screen.getByRole("button", { name: "Change language" }); - }); -}); - -describe.skip("when the button for changing the language is clicked", () => { - beforeEach(() => { - mockSelectedLocales = [{ id: "es_ES.UTF8", name: "Spanish", territory: "Spain" }]; - }); - - it("opens a popup for selecting the language", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change language" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - within(popup).getByText("Select language"); - within(popup).getByRole("row", { name: /German/ }); - within(popup).getByRole("row", { name: /English/ }); - within(popup).getByRole("row", { name: /Spanish/, selected: true }); - }); - - it("allows filtering languages", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change language" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const searchInput = within(popup).getByRole("search"); - - await user.type(searchInput, "ish"); - - await waitFor(() => ( - expect(within(popup).queryByRole("row", { name: /German/ })).not.toBeInTheDocument()) - ); - within(popup).getByRole("row", { name: /English/ }); - within(popup).getByRole("row", { name: /Spanish/ }); - }); - - describe("if the popup is canceled", () => { - it("closes the popup without selecting a new language", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change language" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("row", { name: /English/ }); - - await user.click(option); - const cancel = within(popup).getByRole("button", { name: "Cancel" }); - await user.click(cancel); - - expect(mockL10nClient.setLocales).not.toHaveBeenCalled(); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); - - describe("if the popup is accepted", () => { - it("closes the popup selecting the new language", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change language" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("row", { name: /English/ }); - - await user.click(option); - const accept = within(popup).getByRole("button", { name: "Accept" }); - await user.click(accept); - - expect(mockL10nClient.setLocales).toHaveBeenCalledWith(["en_US.UTF8"]); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); + render(); + const region = screen.getByRole("region", { name: "Language" }) + within(region).getByText("Not selected yet"); + within(region).getByText("Select"); }); }); -it.skip("renders a section for configuring the keyboard", () => { - plainRender(); - screen.getByText("Keyboard"); +it("renders a section for configuring the keyboard", () => { + render(); + const region = screen.getByRole("region", { name: "Keyboard" }) + within(region).getByText("English"), + within(region).getByText("Change"); }); -describe.skip("if there is no selected keyboard", () => { +describe("if there is no selected keyboard", () => { beforeEach(() => { - mockSelectedKeymap = undefined; + mockLoadedData.keymap = undefined; }); it("renders a button for selecting a keyboard", () => { - plainRender(); - screen.getByText("Keyboard not selected yet"); - screen.getByRole("button", { name: "Select keyboard" }); - }); -}); - -describe.skip("if there is a selected keyboard", () => { - beforeEach(() => { - mockSelectedKeymap = { id: "es", name: "Spanish" }; - }); - - it("renders a button for changing the keyboard", () => { - plainRender(); - screen.getByText("Spanish"); - screen.getByRole("button", { name: "Change keyboard" }); - }); -}); - -describe.skip("when the button for changing the keyboard is clicked", () => { - beforeEach(() => { - mockSelectedKeymap = { id: "es", name: "Spanish" }; - }); - - it("opens a popup for selecting the keyboard", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change keyboard" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - within(popup).getByText("Select keyboard"); - within(popup).getByRole("row", { name: /German/ }); - within(popup).getByRole("row", { name: /English/ }); - within(popup).getByRole("row", { name: /Spanish/, selected: true }); - }); - - it("allows filtering keyboards", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change keyboard" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const searchInput = within(popup).getByRole("search"); - - await user.type(searchInput, "ish"); - - await waitFor(() => ( - expect(within(popup).queryByRole("row", { name: /German/ })).not.toBeInTheDocument()) - ); - within(popup).getByRole("row", { name: /English/ }); - within(popup).getByRole("row", { name: /Spanish/ }); - }); - - describe("if the popup is canceled", () => { - it("closes the popup without selecting a new keyboard", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change keyboard" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("row", { name: /English/ }); - - await user.click(option); - const cancel = within(popup).getByRole("button", { name: "Cancel" }); - await user.click(cancel); - - expect(mockL10nClient.setKeymap).not.toHaveBeenCalled(); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); - - describe("if the popup is accepted", () => { - it("closes the popup selecting the new keyboard", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change keyboard" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("row", { name: /English/ }); - - await user.click(option); - const accept = within(popup).getByRole("button", { name: "Accept" }); - await user.click(accept); - - expect(mockL10nClient.setKeymap).toHaveBeenCalledWith("us"); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); + render(); + const region = screen.getByRole("region", { name: "Keyboard" }) + within(region).getByText("Not selected yet"); + within(region).getByText("Select"); }); }); -it.skip("renders a section for configuring the time zone", () => { - plainRender(); - screen.getByText("Time zone"); +it("renders a section for configuring the time zone", () => { + render(); + const region = screen.getByRole("region", { name: "Time zone" }) + within(region).getByText("Europe - Berlin"), + within(region).getByText("Change"); }); -describe.skip("if there is no selected time zone", () => { +describe("if there is no selected time zone", () => { beforeEach(() => { - mockSelectedTimezone = undefined; + mockLoadedData.timezone = undefined; }); it("renders a button for selecting a time zone", () => { - plainRender(); - screen.getByText("Time zone not selected yet"); - screen.getByRole("button", { name: "Select time zone" }); - }); -}); - -describe.skip("if there is a selected time zone", () => { - beforeEach(() => { - mockSelectedTimezone = { id: "atlantic/canary", parts: ["Atlantic", "Canary"] }; - }); - - it("renders a button for changing the time zone", () => { - plainRender(); - screen.getByText("Atlantic - Canary"); - screen.getByRole("button", { name: "Change time zone" }); - }); -}); - -describe.skip("when the button for changing the time zone is clicked", () => { - beforeEach(() => { - mockSelectedTimezone = { id: "atlantic/canary", parts: ["Atlantic", "Canary"] }; - }); - - it("opens a popup for selecting the time zone", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change time zone" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - within(popup).getByText("Select time zone"); - within(popup).getByRole("row", { name: /Bangkok/ }); - within(popup).getByRole("row", { name: /Canary/, selected: true }); - within(popup).getByRole("row", { name: /New York/ }); - }); - - it("allows filtering time zones", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change time zone" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const searchInput = within(popup).getByRole("search"); - - await user.type(searchInput, "new"); - - await waitFor(() => ( - expect(within(popup).queryByRole("row", { name: /Bangkok/ })).not.toBeInTheDocument()) - ); - await waitFor(() => ( - expect(within(popup).queryByRole("row", { name: /Canary/ })).not.toBeInTheDocument()) - ); - within(popup).getByRole("row", { name: /New York/ }); - }); - - describe("if the popup is canceled", () => { - it("closes the popup without selecting a new time zone", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change time zone" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("row", { name: /New York/ }); - - await user.click(option); - const cancel = within(popup).getByRole("button", { name: "Cancel" }); - await user.click(cancel); - - expect(mockL10nClient.setTimezone).not.toHaveBeenCalled(); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); - - describe("if the popup is accepted", () => { - it("closes the popup selecting the new time zone", async () => { - const { user } = installerRender(); - - const button = screen.getByRole("button", { name: "Change time zone" }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("row", { name: /Bangkok/ }); - - await user.click(option); - const accept = within(popup).getByRole("button", { name: "Accept" }); - await user.click(accept); - - expect(mockL10nClient.setTimezone).toHaveBeenCalledWith("asia/bangkok"); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); + render(); + const region = screen.getByRole("region", { name: "Time zone" }) + within(region).getByText("Not selected yet"); + within(region).getByText("Select"); }); }); From 0fd96ffec28d3103f045d21fdae845c55a5ce572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 13:17:32 +0100 Subject: [PATCH 106/430] test: drop the queryRender helper --- web/src/test-utils.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/web/src/test-utils.js b/web/src/test-utils.js index cc9c3fd988..22e46e64aa 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -161,25 +161,6 @@ const plainRender = (ui, options = {}) => { ); }; -/** - * Wrapper around react-testing-library#render for rendering components with the - * QueryClientProvider from TanStack Query. - * - * @todo Unify the render functions once the HTTP client has been replaced with - * TanStack Query. - */ -const queryRender = (ui, options = {}) => { - const queryClient = new QueryClient({}); - - const wrapper = ({ children }) => ( - - {children} - - ); - - return plainRender(ui, {...options, wrapper}); -} - /** * Creates a function to register callbacks * @@ -232,7 +213,6 @@ const resetLocalStorage = (initialState) => { export { plainRender, installerRender, - queryRender, createCallbackMock, mockGettext, mockNavigateFn, From 25585aca5a4fb41adf66169bed6dbc02dba0b0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez?= Date: Thu, 4 Jul 2024 13:19:51 +0100 Subject: [PATCH 107/430] Add comment Co-authored-by: Martin Vidner --- rust/agama-lib/share/profile.schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 721ed35267..f260bebe39 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -1,4 +1,5 @@ { + "$comment": "based on doc/auto_storage.md", "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://github.com/openSUSE/agama/blob/master/rust/agama-lib/share/profile.schema.json", "title": "Profile", From e018d5f6ad3fa7708b14614a781961abddc3c947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 14:15:06 +0100 Subject: [PATCH 108/430] doc(web): document l10n queries --- web/src/queries/l10n.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js index bd7b9f9db0..d644a5c2d8 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.js @@ -25,6 +25,9 @@ import { useQueryClient, useMutation } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { timezoneUTCOffset } from "~/utils"; +/** + * Returns a query for retrieving the localization configuration + */ const configQuery = () => { return { queryKey: ["l10n", "config"], @@ -32,6 +35,9 @@ const configQuery = () => { }; }; +/** + * Returns a query for retrieving the list of known locales + */ const localesQuery = () => ({ queryKey: ["l10n", "locales"], queryFn: async () => { @@ -44,6 +50,9 @@ const localesQuery = () => ({ staleTime: Infinity }); +/** + * Returns a query for retrieving the list of known timezones + */ const timezonesQuery = () => ({ queryKey: ["l10n", "timezones"], queryFn: async () => { @@ -57,6 +66,9 @@ const timezonesQuery = () => ({ staleTime: Infinity }); +/** + * Returns a query for retrieving the list of known keymaps + */ const keymapsQuery = () => ({ queryKey: ["l10n", "keymaps"], queryFn: async () => { @@ -70,6 +82,11 @@ const keymapsQuery = () => ({ staleTime: Infinity }); +/** + * Hook that builds a mutation to update the l10n configuration + * + * It does not require to call `useMutation`. + */ const useConfigMutation = () => { const queryClient = useQueryClient(); @@ -89,6 +106,13 @@ const useConfigMutation = () => { return useMutation(query); }; + +/** + * Hook that returns a useEffect to listen for L10nConfigChanged events + * + * When the configuration changes, it invalidates the config query and forces the router to + * revalidate its data (executing the loaders again). + */ const useL10nConfigChanges = () => { const queryClient = useQueryClient(); const client = useInstallerClient(); From 3cdcaf68886edbef26857444546b07674e2cb4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 14:17:52 +0100 Subject: [PATCH 109/430] fix(web): make ESLint happy --- web/src/components/l10n/L10nPage.test.jsx | 20 ++++++++++---------- web/src/queries/l10n.js | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.jsx index 8c9268da25..1406d0a99c 100644 --- a/web/src/components/l10n/L10nPage.test.jsx +++ b/web/src/components/l10n/L10nPage.test.jsx @@ -36,14 +36,14 @@ beforeEach(() => { mockLoadedData = { locale: { id: "en_US.UTF-8", name: "English", territory: "United States" }, keymap: { id: "us", name: "English" }, - timezone: { id: "Europe/Berlin", parts: ["Europe", "Berlin"]} + timezone: { id: "Europe/Berlin", parts: ["Europe", "Berlin"] } }; }); it("renders a section for configuring the language", () => { render(); - const region = screen.getByRole("region", { name: "Language" }) - within(region).getByText("English - United States"), + const region = screen.getByRole("region", { name: "Language" }); + within(region).getByText("English - United States"); within(region).getByText("Change"); }); @@ -54,7 +54,7 @@ describe("if there is no selected language", () => { it("renders a button for selecting a language", () => { render(); - const region = screen.getByRole("region", { name: "Language" }) + const region = screen.getByRole("region", { name: "Language" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); }); @@ -62,8 +62,8 @@ describe("if there is no selected language", () => { it("renders a section for configuring the keyboard", () => { render(); - const region = screen.getByRole("region", { name: "Keyboard" }) - within(region).getByText("English"), + const region = screen.getByRole("region", { name: "Keyboard" }); + within(region).getByText("English"); within(region).getByText("Change"); }); @@ -74,7 +74,7 @@ describe("if there is no selected keyboard", () => { it("renders a button for selecting a keyboard", () => { render(); - const region = screen.getByRole("region", { name: "Keyboard" }) + const region = screen.getByRole("region", { name: "Keyboard" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); }); @@ -82,8 +82,8 @@ describe("if there is no selected keyboard", () => { it("renders a section for configuring the time zone", () => { render(); - const region = screen.getByRole("region", { name: "Time zone" }) - within(region).getByText("Europe - Berlin"), + const region = screen.getByRole("region", { name: "Time zone" }); + within(region).getByText("Europe - Berlin"); within(region).getByText("Change"); }); @@ -94,7 +94,7 @@ describe("if there is no selected time zone", () => { it("renders a button for selecting a time zone", () => { render(); - const region = screen.getByRole("region", { name: "Time zone" }) + const region = screen.getByRole("region", { name: "Time zone" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); }); diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js index d644a5c2d8..d75d25f37a 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.js @@ -106,7 +106,6 @@ const useConfigMutation = () => { return useMutation(query); }; - /** * Hook that returns a useEffect to listen for L10nConfigChanged events * From 58a27ff5eefea1a3290405cf3d473b972026c883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 14:46:46 +0100 Subject: [PATCH 110/430] refactor(web): remove unused methods from L10nClient --- web/src/client/l10n.js | 241 ----------------------------------------- 1 file changed, 241 deletions(-) diff --git a/web/src/client/l10n.js b/web/src/client/l10n.js index 2cdb210969..29e5344f80 100644 --- a/web/src/client/l10n.js +++ b/web/src/client/l10n.js @@ -20,28 +20,6 @@ */ // @ts-check -import { timezoneUTCOffset } from "~/utils"; - -/** - * @typedef {object} Timezone - * @property {string} id - Timezone id (e.g., "Atlantic/Canary"). - * @property {Array} parts - Name of the timezone parts (e.g., ["Atlantic", "Canary"]). - * @property {string} country - Name of the country associated to the zone or empty string (e.g., "Spain"). - * @property {number} utcOffset - UTC offset. - */ - -/** - * @typedef {object} Locale - * @property {string} id - Language id (e.g., "en_US.UTF-8"). - * @property {string} name - Language name (e.g., "English"). - * @property {string} territory - Territory name (e.g., "United States"). - */ - -/** - * @typedef {object} Keymap - * @property {string} id - Keyboard id (e.g., "us"). - * @property {string} name - Keyboard name (e.g., "English (US)"). - */ /** * Manages localization. @@ -107,225 +85,6 @@ class L10nClient { async setUIKeymap(id) { return this.client.patch("/l10n/config", { uiKeymap: id }); } - - /** - * All possible timezones for the target system. - * - * @return {Promise>} - */ - async timezones() { - const response = await this.client.get("/l10n/timezones"); - if (!response.ok) { - console.error("Failed to get localization config: ", response); - return []; - } - const timezones = await response.json(); - return timezones.map(this.buildTimezone); - } - - /** - * Timezone selected for the target system. - * - * @return {Promise} Id of the timezone. - */ - async getTimezone() { - const config = await this.getConfig(); - return config.timezone; - } - - /** - * Sets the timezone for the target system. - * - * @param {string} id - Id of the timezone. - * @return {Promise} - */ - async setTimezone(id) { - this.setConfig({ timezone: id }); - } - - /** - * Available locales to install in the target system. - * - * TODO: find a better name because it is rather confusing (e.g., 'locales' and 'getLocales'). - * - * @return {Promise>} - */ - async locales() { - const response = await this.client.get("/l10n/locales"); - if (!response.ok) { - console.error("Failed to get localization config: ", response); - return []; - } - const locales = await response.json(); - return locales.map(this.buildLocale); - } - - /** - * Locales selected to install in the target system. - * - * @return {Promise>} Ids of the locales. - */ - async getLocales() { - const config = await this.getConfig(); - return config.locales; - } - - /** - * Sets the locales to install in the target system. - * - * @param {Array} ids - Ids of the locales. - * @return {Promise} - */ - async setLocales(ids) { - this.setConfig({ locales: ids }); - } - - /** - * Available keymaps to install in the target system. - * - * Note that name is localized to the current selected UI language: - * { id: "es", name: "Spanish (ES)" } - * - * @return {Promise>} - */ - async keymaps() { - const response = await this.client.get("/l10n/keymaps"); - if (!response.ok) { - console.error("Failed to get localization config: ", response); - return []; - } - const keymaps = await response.json(); - return keymaps.map(this.buildKeymap); - } - - /** - * Keymap selected to install in the target system. - * - * @return {Promise} Id of the keymap. - */ - async getKeymap() { - const config = await this.getConfig(); - return config.keymap; - } - - /** - * Sets the keymap to install in the target system. - * - * @param {string} id - Id of the keymap. - * @return {Promise} - */ - async setKeymap(id) { - this.setConfig({ keymap: id }); - } - - /** - * Register a callback to run when the timezone configuration changes. - * - * @param {(timezone: string) => void} handler - Function to call when Timezone changes. - * @return {import ("./http").RemoveFn} Function to disable the callback. - */ - onTimezoneChange(handler) { - return this.client.onEvent("L10nConfigChanged", ({ timezone }) => { - if (timezone) { - handler(timezone); - } - }); - } - - /** - * Register a callback to run when the locales configuration changes. - * - * @param {(locales: string[]) => void} handler - Function to call when Locales changes. - * @return {import ("./http").RemoveFn} Function to disable the callback. - */ - onLocalesChange(handler) { - return this.client.onEvent("L10nConfigChanged", ({ locales }) => { - if (locales) { - handler(locales); - } - }); - } - - /** - * Register a callback to run when the keymap configuration changes. - * - * @param {(keymap: string) => void} handler - Function to call when Keymap changes. - * @return {import ("./http").RemoveFn} Function to disable the callback. - */ - onKeymapChange(handler) { - return this.client.onEvent("L10nConfigChanged", ({ keymap }) => { - if (keymap) { - handler(keymap); - } - }); - } - - /** - * @private - * Convenience method to get l10n the configuration. - * - * @return {Promise} Localization configuration. - */ - async getConfig() { - const response = await this.client.get("/l10n/config"); - if (!response.ok) { - console.error("Failed to get localization config: ", response); - return {}; - } - return await response.json(); - } - - /** - * @private - * - * Convenience method to set l10n the configuration. - * - * @param {object} data - Configuration to update. It can just part of the configuration. - * @return {Promise} - */ - async setConfig(data) { - return this.client.patch("/l10n/config", data); - } - - /** - * @private - * - * @param {object} timezone - Timezone data. - * @param {string} timezone.code - Timezone identifier. - * @param {Array} timezone.parts - Localized parts of the timezone identifier. - * @param {string} timezone.country - Timezone country. - * @return {Timezone} - */ - buildTimezone({ code, parts, country }) { - const utcOffset = timezoneUTCOffset(code); - - return ({ id: code, parts, country, utcOffset }); - } - - /** - * @private - * - * @param {object} locale - Locale data. - * @param {string} locale.id - Identifier. - * @param {string} locale.language - Name. - * @param {string} locale.territory - Territory. - * @return {Locale} - */ - buildLocale({ id, language, territory }) { - return ({ id, name: language, territory }); - } - - /** - * @private - * - * @param {object} keymap - Keymap data - * @param {string} keymap.id - Id (e.g., "us"). - * @param {string} keymap.description - Keymap description (e.g., "English (US)"). - * @return {Keymap} - */ - buildKeymap({ id, description }) { - return ({ id, name: description }); - } } export { L10nClient }; From f633c9517857995d4c73af25fc992339bf530f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 14:54:34 +0100 Subject: [PATCH 111/430] test(web): fix App tests --- web/src/App.test.jsx | 17 +++++++---------- web/src/test-utils.js | 12 +++++++++++- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index 303891708f..d3fe46be4b 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -20,13 +20,14 @@ */ import React from "react"; -import { act, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import App from "./App"; import { createClient } from "~/client"; import { STARTUP, CONFIG, INSTALL } from "~/client/phase"; import { IDLE, BUSY } from "~/client/status"; +import { useL10nConfigChanges } from "./queries/l10n"; jest.mock("~/client"); @@ -44,6 +45,11 @@ jest.mock("~/context/product", () => ({ } })); +jest.mock("~/queries/l10n", () => ({ + ...jest.requireActual("~/queries/l10n"), + useL10nConfigChanges: () => jest.fn() +})); + const mockClientStatus = { connected: true, error: false, @@ -70,18 +76,9 @@ describe("App", () => { createClient.mockImplementation(() => { return { l10n: { - locales: jest.fn().mockResolvedValue([["en_us", "English", "United States"]]), - getLocales: jest.fn().mockResolvedValue(["en_us"]), - timezones: jest.fn().mockResolvedValue([]), - getTimezone: jest.fn().mockResolvedValue("Europe/Berlin"), - keymaps: jest.fn().mockResolvedValue([]), - getKeymap: jest.fn().mockResolvedValue(undefined), getUIKeymap: jest.fn().mockResolvedValue("en"), getUILocale: jest.fn().mockResolvedValue("en_us"), setUILocale: jest.fn().mockResolvedValue("en_us"), - onTimezoneChange: jest.fn(), - onLocalesChange: jest.fn(), - onKeymapChange: jest.fn() } }; }); diff --git a/web/src/test-utils.js b/web/src/test-utils.js index 22e46e64aa..162418cd52 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -51,6 +51,14 @@ const initialRoutes = jest.fn().mockReturnValue(["/"]); */ const mockNavigateFn = jest.fn(); +/** + * Allows checking when the useRevalidator function has been called + * + * @example + * expect(mockUseRevalidator).toHaveBeenCalled() + */ +const mockUseRevalidator = jest.fn(); + /** * Allows manipulating MemoryRouter routes for testing purpose * @@ -68,7 +76,8 @@ jest.mock('react-router-dom', () => ({ ...jest.requireActual("react-router-dom"), useNavigate: () => mockNavigateFn, Navigate: ({ to: route }) => <>Navigating to {route}, - Outlet: () => <>Outlet Content + Outlet: () => <>Outlet Content, + useRevalidator: () => mockUseRevalidator })); const Providers = ({ children, withL10n }) => { @@ -217,5 +226,6 @@ export { mockGettext, mockNavigateFn, mockRoutes, + mockUseRevalidator, resetLocalStorage }; From 6f16921808672c8787293fbe905aa32e29a2e6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 14:59:07 +0100 Subject: [PATCH 112/430] refactor(web): drop unused L10nClient tests --- web/src/client/l10n.test.js | 129 ------------------------------------ 1 file changed, 129 deletions(-) delete mode 100644 web/src/client/l10n.test.js diff --git a/web/src/client/l10n.test.js b/web/src/client/l10n.test.js deleted file mode 100644 index e24dbb7192..0000000000 --- a/web/src/client/l10n.test.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -import { HTTPClient } from "./http"; -import { L10nClient } from "./l10n"; - -jest.mock("./dbus"); - -const mockJsonFn = jest.fn(); -const mockGetFn = jest.fn().mockImplementation(() => { - return { ok: true, json: mockJsonFn }; -}); -const mockPatchFn = jest.fn().mockImplementation(() => { - return { ok: true }; -}); - -jest.mock("./http", () => { - return { - HTTPClient: jest.fn().mockImplementation(() => { - return { - get: mockGetFn, - patch: mockPatchFn, - }; - }), - }; -}); - -let client; - -const locales = [ - { - id: "en_US.UTF-8", - language: "English", - territory: "United States", - }, - { - id: "es_ES.UTF-8", - language: "Spanish", - territory: "Spain", - }, -]; - -const config = { - locales: [ - "en_US.UTF-8", - ], - keymap: "us", - timezone: "Europe/Berlin", - uiLocale: "en_US.UTF-8", - uiKeymap: "us", -}; - -beforeEach(() => { - client = new L10nClient(new HTTPClient(new URL("http://localhost"))); -}); - -describe("#locales", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(locales); - }); - - it("returns the list of available locales", async () => { - const locales = await client.locales(); - - expect(locales).toEqual([ - { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }, - ]); - expect(mockGetFn).toHaveBeenCalledWith("/l10n/locales"); - }); -}); - -describe("#getConfig", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(config); - }); - - it("returns the list of selected locales", async () => { - const l10nConfig = await client.getConfig(); - - expect(l10nConfig).toEqual(config); - expect(mockGetFn).toHaveBeenCalledWith("/l10n/config"); - }); -}); - -describe("#setConfig", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(config); - }); - - it("updates the l10n configuration", async () => { - await client.setConfig(config); - client.setConfig(config); - expect(mockPatchFn).toHaveBeenCalledWith("/l10n/config", config); - }); -}); - -describe("#getLocales", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(config); - }); - - it("returns the list of selected locales", async () => { - const locales = await client.getLocales(); - - expect(locales).toEqual(["en_US.UTF-8"]); - expect(mockGetFn).toHaveBeenCalledWith("/l10n/config"); - }); -}); From 4766a2d95d103ec33a04f1c752b1b9e5463d9e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 15:15:53 +0100 Subject: [PATCH 113/430] feat(web): wrap plainRender on a QueryClientProvider * In case you need something really minimal, use the "render" function from React Testing Library. --- web/src/test-utils.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/test-utils.js b/web/src/test-utils.js index 162418cd52..7aaa06f560 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -162,10 +162,17 @@ const installerRender = (ui, options = {}) => { * core/Sidebar as part of the layout. */ const plainRender = (ui, options = {}) => { + const queryClient = new QueryClient({}); + + const Wrapper = ({ children }) => ( + + {children} + + ); return ( { user: userEvent.setup(), - ...render(ui, options) + ...render(ui, { wrapper: Wrapper, ...options }) } ); }; From b893146dbaf3c571d2a134784bb75d9a739dec9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 15:17:00 +0100 Subject: [PATCH 114/430] test(web): fix L10nSectino tests --- .../components/overview/L10nSection.test.jsx | 60 +++++-------------- 1 file changed, 16 insertions(+), 44 deletions(-) diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.jsx index 4a813225bb..7ddf3347f2 100644 --- a/web/src/components/overview/L10nSection.test.jsx +++ b/web/src/components/overview/L10nSection.test.jsx @@ -20,58 +20,30 @@ */ import React from "react"; -import { act, screen } from "@testing-library/react"; -import { createCallbackMock, installerRender } from "~/test-utils"; +import { screen } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; import { L10nSection } from "~/components/overview"; -import { createClient } from "~/client"; jest.mock("~/client"); +jest.mock("~/queries/l10n", () => ({ + localesQuery: () => ({ + queryKey: ["l10n", "locales"], + queryFn: jest.fn().mockResolvedValue(locales) + }), + configQuery: () => ({ + queryKey: ["l10n", "config"], + queryFn: jest.fn().mockResolvedValue({ locales: ["en_US.UTF-8"]}) + }) +})); + const locales = [ - { id: "en_US", name: "English", territory: "United States" }, - { id: "de_DE", name: "German", territory: "Germany" } + { id: "en_US.UTF-8", name: "English", territory: "United States" }, + { id: "de_DE.UTF-8", name: "German", territory: "Germany" } ]; -const l10nClientMock = { - locales: jest.fn().mockResolvedValue(locales), - getLocales: jest.fn().mockResolvedValue(["en_US"]), - getUILocale: jest.fn().mockResolvedValue("en_US"), - getUIKeymap: jest.fn().mockResolvedValue("en"), - keymaps: jest.fn().mockResolvedValue([]), - getKeymap: jest.fn().mockResolvedValue(undefined), - timezones: jest.fn().mockResolvedValue([]), - getTimezone: jest.fn().mockResolvedValue(undefined), - onLocalesChange: jest.fn(), - onKeymapChange: jest.fn(), - onTimezoneChange: jest.fn() -}; - -beforeEach(() => { - // if defined outside, the mock is cleared automatically - createClient.mockImplementation(() => { - return { - l10n: l10nClientMock - }; - }); -}); - it("displays the selected locale", async () => { - installerRender(, { withL10n: true }); + plainRender(, { withL10n: true }); await screen.findByText("English (United States)"); }); - -describe("when the selected locales change", () => { - it("updates the proposal", async () => { - const [mockFunction, callbacks] = createCallbackMock(); - l10nClientMock.onLocalesChange = mockFunction; - - installerRender(, { withL10n: true }); - await screen.findByText("English (United States)"); - - const [cb] = callbacks; - act(() => cb(["de_DE"])); - - await screen.findByText("German (Germany)"); - }); -}); From 5236f86caadc17d41dc8167acf82c7d1cae16318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 4 Jul 2024 15:20:11 +0100 Subject: [PATCH 115/430] test(web): remove uneeded mock from L10nSection test --- web/src/components/overview/L10nSection.test.jsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.jsx index 7ddf3347f2..dad4cde147 100644 --- a/web/src/components/overview/L10nSection.test.jsx +++ b/web/src/components/overview/L10nSection.test.jsx @@ -24,7 +24,10 @@ import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { L10nSection } from "~/components/overview"; -jest.mock("~/client"); +const locales = [ + { id: "en_US.UTF-8", name: "English", territory: "United States" }, + { id: "de_DE.UTF-8", name: "German", territory: "Germany" } +]; jest.mock("~/queries/l10n", () => ({ localesQuery: () => ({ @@ -33,15 +36,10 @@ jest.mock("~/queries/l10n", () => ({ }), configQuery: () => ({ queryKey: ["l10n", "config"], - queryFn: jest.fn().mockResolvedValue({ locales: ["en_US.UTF-8"]}) + queryFn: jest.fn().mockResolvedValue({ locales: ["en_US.UTF-8"] }) }) })); -const locales = [ - { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "de_DE.UTF-8", name: "German", territory: "Germany" } -]; - it("displays the selected locale", async () => { plainRender(, { withL10n: true }); From 82267a95a97e62f7d7efdb6bb21db06f888b7608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 4 Jul 2024 16:11:43 +0100 Subject: [PATCH 116/430] feat(web): add a hook for invalidating cached data --- web/src/queries/hooks.js | 65 +++++++++++++++++++++++++++++++++++ web/src/queries/hooks.test.js | 49 ++++++++++++++++++++++++++ web/src/queries/l10n.js | 10 +++--- 3 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 web/src/queries/hooks.js create mode 100644 web/src/queries/hooks.test.js diff --git a/web/src/queries/hooks.js b/web/src/queries/hooks.js new file mode 100644 index 0000000000..1c64660b2f --- /dev/null +++ b/web/src/queries/hooks.js @@ -0,0 +1,65 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import { useRevalidator } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; + +/** + * Allows invalidating cached data + * + * This hook is useful for marking data as outdated and retrieve it again. To do so, it performs two important steps + * - ask @tanstack/react-query to invalidate query matching given key + * - ask react-router-dom for a revalidation of loaded data + * + * TODO: rethink the revalidation; we may decide to keep the outdated data + * instead, but warning the user about it (as Github does when reviewing a PR, + * for example) + * + * TODO: allow to specify more than one queryKey + * + * To know more, please visit the documentation of these dependencies + * + * - https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation + * - https://reactrouter.com/en/main/hooks/use-revalidator#userevalidator + * + * @example + * + * const dataInvalidator = useDataInvalidator(); + * + * useEffect(() => { + * dataInvalidator({ queryKey: ["user", "auth"] }) + * }, [dataInvalidator]); + */ +const useDataInvalidator = () => { + const queryClient = useQueryClient(); + const revalidator = useRevalidator(); + + const dataInvalidator = ({ queryKey }) => { + if (queryKey) queryClient.invalidateQueries({ queryKey }); + revalidator.revalidate(); + }; + + return dataInvalidator; +}; + +export { + useDataInvalidator +}; diff --git a/web/src/queries/hooks.test.js b/web/src/queries/hooks.test.js new file mode 100644 index 0000000000..ad3602f2fe --- /dev/null +++ b/web/src/queries/hooks.test.js @@ -0,0 +1,49 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { renderHook } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useDataInvalidator } from "~/queries/hooks.js"; + +const mockRevalidateFn = jest.fn(); +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useRevalidator: () => ({ revalidate: mockRevalidateFn }) +})); + +const queryClient = new QueryClient(); +jest.spyOn(queryClient, "invalidateQueries"); +const wrapper = ({ children }) => ( + + {children} + +); + +describe("useDataInvalidator", () => { + it("forces a data/cache refresh", () => { + const { result } = renderHook(() => useDataInvalidator(), { wrapper }); + const { current: dataInvalidator } = result; + dataInvalidator({ queryKey: "fakeQuery" }) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ queryKey: "fakeQuery" }); + expect(mockRevalidateFn).toHaveBeenCalled(); + }); +}); diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js index d75d25f37a..59af07a1f4 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.js @@ -20,9 +20,9 @@ */ import React from "react"; -import { useRevalidator } from "react-router-dom"; import { useQueryClient, useMutation } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; +import { useDataInvalidator } from "~/queries/hooks"; import { timezoneUTCOffset } from "~/utils"; /** @@ -113,20 +113,18 @@ const useConfigMutation = () => { * revalidate its data (executing the loaders again). */ const useL10nConfigChanges = () => { - const queryClient = useQueryClient(); + const dataInvalidator = useDataInvalidator(); const client = useInstallerClient(); - const revalidator = useRevalidator(); React.useEffect(() => { if (!client) return; return client.ws().onEvent(event => { if (event.type === "L10nConfigChanged") { - queryClient.invalidateQueries({ queryKey: ["l10n", "config"] }); - revalidator.revalidate(); + dataInvalidator({ queryKey: ["l10n", "config"] }); } }); - }, [queryClient, client, revalidator]); + }, [client, dataInvalidator]); }; export { From 1318cd26b4a6126ce09acecf7d1f14827228923a Mon Sep 17 00:00:00 2001 From: Lubos Kocman Date: Thu, 4 Jul 2024 17:31:33 +0200 Subject: [PATCH 117/430] Ensure that we reference %doc and %license in specs --- autoinstallation/LICENSE | 339 +++++++++++++++++++++++ autoinstallation/package/agama-auto.spec | 2 + playwright/LICENSE | 339 +++++++++++++++++++++++ playwright/package/agama-playwright.spec | 1 + products.d/LICENSE | 339 +++++++++++++++++++++++ products.d/agama-products-opensuse.spec | 2 + rust/package/agama.spec | 2 + web/LICENSE | 339 +++++++++++++++++++++++ web/package/agama-web-ui.spec | 1 + 9 files changed, 1364 insertions(+) create mode 100644 autoinstallation/LICENSE create mode 100644 playwright/LICENSE create mode 100644 products.d/LICENSE create mode 100644 web/LICENSE diff --git a/autoinstallation/LICENSE b/autoinstallation/LICENSE new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/autoinstallation/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/autoinstallation/package/agama-auto.spec b/autoinstallation/package/agama-auto.spec index 55d2a5cfc3..b669489d1b 100644 --- a/autoinstallation/package/agama-auto.spec +++ b/autoinstallation/package/agama-auto.spec @@ -51,6 +51,8 @@ install -D -m 0644 %{_builddir}/agama/systemd/agama-auto.service %{buildroot}%{_ %service_del_preun agama-auto.service %files +%doc README.md +%license LICENSE %{_bindir}/agama-auto %{_unitdir}/agama-auto.service diff --git a/playwright/LICENSE b/playwright/LICENSE new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/playwright/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/playwright/package/agama-playwright.spec b/playwright/package/agama-playwright.spec index d15e827b9c..35d2be7dd7 100644 --- a/playwright/package/agama-playwright.spec +++ b/playwright/package/agama-playwright.spec @@ -46,6 +46,7 @@ mv %{buildroot}%{_datadir}/agama %{buildroot}%{_datadir}/agama-playwright %files %defattr(-,root,root,-) %doc README.md +%license LICENSE %{_datadir}/agama-playwright %changelog diff --git a/products.d/LICENSE b/products.d/LICENSE new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/products.d/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/products.d/agama-products-opensuse.spec b/products.d/agama-products-opensuse.spec index d82c58dfad..31b90f3ed2 100644 --- a/products.d/agama-products-opensuse.spec +++ b/products.d/agama-products-opensuse.spec @@ -38,6 +38,8 @@ install -D -d -m 0755 %{buildroot}%{_datadir}/agama/products.d install -m 0644 *.yaml %{buildroot}%{_datadir}/agama/products.d %files +%doc README.md +%license LICENSE %dir %{_datadir}/agama %dir %{_datadir}/agama/products.d %{_datadir}/agama/products.d/microos.yaml diff --git a/rust/package/agama.spec b/rust/package/agama.spec index 3d7d5292b5..db38ce4665 100644 --- a/rust/package/agama.spec +++ b/rust/package/agama.spec @@ -116,6 +116,8 @@ echo $PATH %service_del_postun_with_restart agama-web-server.service %files +%doc README.md +%license LICENSE %{_bindir}/agama-dbus-server %{_bindir}/agama-web-server %{_datadir}/dbus-1/agama-services diff --git a/web/LICENSE b/web/LICENSE new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/web/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/package/agama-web-ui.spec b/web/package/agama-web-ui.spec index fb91e0ff26..ac91e82976 100644 --- a/web/package/agama-web-ui.spec +++ b/web/package/agama-web-ui.spec @@ -50,6 +50,7 @@ install -D -m 0644 --target-directory=%{buildroot}%{_datadir}/agama/web_ui/fonts %files %doc README.md +%license LICENSE %dir %{_datadir}/agama %{_datadir}/agama/web_ui From 2ead88474d30f3d53baeca531c40e9460f31c399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 4 Jul 2024 17:10:33 +0100 Subject: [PATCH 118/430] fix(web): please ESLint --- web/src/queries/hooks.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/queries/hooks.test.js b/web/src/queries/hooks.test.js index ad3602f2fe..6db7d4b3ee 100644 --- a/web/src/queries/hooks.test.js +++ b/web/src/queries/hooks.test.js @@ -42,7 +42,7 @@ describe("useDataInvalidator", () => { it("forces a data/cache refresh", () => { const { result } = renderHook(() => useDataInvalidator(), { wrapper }); const { current: dataInvalidator } = result; - dataInvalidator({ queryKey: "fakeQuery" }) + dataInvalidator({ queryKey: "fakeQuery" }); expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ queryKey: "fakeQuery" }); expect(mockRevalidateFn).toHaveBeenCalled(); }); From b8412721b0005065400f4f7e106abbe40f513321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 5 Jul 2024 09:53:58 +0100 Subject: [PATCH 119/430] test(web): add tests for l10n selectors --- .../l10n/KeyboardSelection.test.jsx | 57 +++++++++++++++++++ .../components/l10n/LocaleSelection.test.jsx | 57 +++++++++++++++++++ .../l10n/TimezoneSelection.test.jsx | 57 +++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 web/src/components/l10n/KeyboardSelection.test.jsx create mode 100644 web/src/components/l10n/LocaleSelection.test.jsx create mode 100644 web/src/components/l10n/TimezoneSelection.test.jsx diff --git a/web/src/components/l10n/KeyboardSelection.test.jsx b/web/src/components/l10n/KeyboardSelection.test.jsx new file mode 100644 index 0000000000..fd733dde25 --- /dev/null +++ b/web/src/components/l10n/KeyboardSelection.test.jsx @@ -0,0 +1,57 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import KeyboardSelection from "./KeyboardSelection"; +import userEvent from "@testing-library/user-event"; +import { screen } from "@testing-library/react"; +import { mockNavigateFn, plainRender } from "~/test-utils"; + +const keymaps = [ + { id: "us", name: "English" }, + { id: "es", name: "Spanish" } +]; + +const mockConfigMutation = { + mutate: jest.fn() +}; + +jest.mock("~/queries/l10n", () => ({ + ...jest.requireActual("~/queries/l10n"), + useConfigMutation: () => mockConfigMutation +})); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: () => mockNavigateFn, + useLoaderData: () => ({ keymaps, keymap: keymaps[0] }) +})); + +it("allows changing the keyboard", async () => { + plainRender(); + + const option = await screen.findByText("Spanish"); + await userEvent.click(option); + const button = await screen.findByRole("button", { name: "Select" }); + await userEvent.click(button); + expect(mockConfigMutation.mutate).toHaveBeenCalledWith({ keymap: "es" }); + expect(mockNavigateFn).toHaveBeenCalledWith(-1); +}); diff --git a/web/src/components/l10n/LocaleSelection.test.jsx b/web/src/components/l10n/LocaleSelection.test.jsx new file mode 100644 index 0000000000..1fc001578f --- /dev/null +++ b/web/src/components/l10n/LocaleSelection.test.jsx @@ -0,0 +1,57 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import LocaleSelection from "./LocaleSelection"; +import userEvent from "@testing-library/user-event"; +import { screen } from "@testing-library/react"; +import { mockNavigateFn, plainRender } from "~/test-utils"; + +const locales = [ + { id: "en_US.UTF-8", name: "English", territory: "United States" }, + { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" } +]; + +const mockConfigMutation = { + mutate: jest.fn() +}; + +jest.mock("~/queries/l10n", () => ({ + ...jest.requireActual("~/queries/l10n"), + useConfigMutation: () => mockConfigMutation +})); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: () => mockNavigateFn, + useLoaderData: () => ({ locales, locale: locales[0] }) +})); + +it("allows changing the keyboard", async () => { + plainRender(); + + const option = await screen.findByText("Spanish"); + await userEvent.click(option); + const button = await screen.findByRole("button", { name: "Select" }); + await userEvent.click(button); + expect(mockConfigMutation.mutate).toHaveBeenCalledWith({ locales: ["es_ES.UTF-8"] }); + expect(mockNavigateFn).toHaveBeenCalledWith(-1); +}); diff --git a/web/src/components/l10n/TimezoneSelection.test.jsx b/web/src/components/l10n/TimezoneSelection.test.jsx new file mode 100644 index 0000000000..efecd089fb --- /dev/null +++ b/web/src/components/l10n/TimezoneSelection.test.jsx @@ -0,0 +1,57 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import TimezoneSelection from "./TimezoneSelection"; +import userEvent from "@testing-library/user-event"; +import { screen } from "@testing-library/react"; +import { mockNavigateFn, plainRender } from "~/test-utils"; + +const timezones = [ + { id: "Europe/Berlin", parts: ["Europe", "Berlin"], country: "Germany", utcOffset: 1 }, + { id: "Europe/Madrid", parts: ["Europe", "Madrid"], country: "Spain", utfOffset: 1 } +]; + +const mockConfigMutation = { + mutate: jest.fn() +}; + +jest.mock("~/queries/l10n", () => ({ + ...jest.requireActual("~/queries/l10n"), + useConfigMutation: () => mockConfigMutation +})); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: () => mockNavigateFn, + useLoaderData: () => ({ timezones, timezone: timezones[0] }) +})); + +it("allows changing the keyboard", async () => { + plainRender(); + + const option = await screen.findByText("Europe-Madrid"); + await userEvent.click(option); + const button = await screen.findByRole("button", { name: "Select" }); + await userEvent.click(button); + expect(mockConfigMutation.mutate).toHaveBeenCalledWith({ timezone: "Europe/Madrid" }); + expect(mockNavigateFn).toHaveBeenCalledWith(-1); +}); From fd907f3fc111265954ad52e539c90efe5fb3a2d8 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 7 Jul 2024 02:50:45 +0000 Subject: [PATCH 120/430] Update web PO files Agama-weblate commit: 2531ecb584319cdf2c0eedd5c1e7b163be2f9670 --- web/po/de.po | 88 +++++------ web/po/es.po | 263 ++++++++++++------------------- web/po/ja.po | 79 ++++------ web/po/pt_BR.po | 253 ++++++++++++------------------ web/po/zh_Hans.po | 391 +++++++++++++++++++--------------------------- 5 files changed, 432 insertions(+), 642 deletions(-) diff --git a/web/po/de.po b/web/po/de.po index 7d1eda62f7..205932bce5 100644 --- a/web/po/de.po +++ b/web/po/de.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-22 21:46+0000\n" +"PO-Revision-Date: 2024-07-06 01:47+0000\n" "Last-Translator: Ettore Atalan \n" "Language-Team: German \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6\n" +"X-Generator: Weblate 5.6.2\n" #: src/MainLayout.jsx:40 msgid "Agama" @@ -167,9 +167,8 @@ msgid "Reboot" msgstr "Neustart" #: src/components/core/InstallationProgress.jsx:30 -#, fuzzy msgid "Installing the system, please wait ..." -msgstr "Daten werden geladen, bitte warten Sie eine Sekunde ..." +msgstr "Das System wird installiert, bitte warten ..." #: src/components/core/InstallerOptions.jsx:83 msgid "Show installer options" @@ -188,9 +187,8 @@ msgstr "Sprache" #: src/components/core/InstallerOptions.jsx:114 #: src/components/core/InstallerOptions.jsx:121 -#, fuzzy msgid "Keyboard layout" -msgstr "Wählen Sie eine Tastaturbelegung aus" +msgstr "Tastaturbelegung" #: src/components/core/InstallerOptions.jsx:130 msgid "Cannot be changed in remote installation" @@ -318,22 +316,20 @@ msgid "Loading data..." msgstr "Daten werden gelesen ..." #: src/components/core/ProgressReport.jsx:50 -#, fuzzy msgid "Finished" -msgstr "Fertigstellen" +msgstr "Fertiggestellt" #: src/components/core/ProgressReport.jsx:59 msgid "In progress" -msgstr "" +msgstr "In Bearbeitung" #: src/components/core/ProgressReport.jsx:70 msgid "Pending" -msgstr "" +msgstr "Ausstehend" #: src/components/core/ProgressReport.jsx:134 -#, fuzzy msgid "Waiting for progress status..." -msgstr "Warten auf den Fortschrittsbericht" +msgstr "Warten auf den Fortschrittsstatus ..." #: src/components/core/RowActions.jsx:64 #: src/components/storage/PartitionsField.jsx:454 @@ -373,9 +369,8 @@ msgid "Filter by description or keymap code" msgstr "Nach Beschreibung oder Tastenzuordnungscode filtern" #: src/components/l10n/KeyboardSelection.jsx:85 -#, fuzzy msgid "None of the keymaps match the filter." -msgstr "Keines der Muster entspricht dem Filter." +msgstr "Keine der Tastenzuordnungen entspricht dem Filter." #: src/components/l10n/KeyboardSelection.jsx:92 msgid "Keyboard selection" @@ -591,11 +586,11 @@ msgstr "Gateway" #: src/components/network/IpSettingsForm.jsx:178 msgid "Gateway can be defined only in 'Manual' mode" -msgstr "" +msgstr "Gateway kann nur im Modus ‚Manuell‘ definiert werden" #: src/components/network/NetworkPage.jsx:85 msgid "No Wi-Fi supported" -msgstr "" +msgstr "Kein Wi-Fi unterstützt" #: src/components/network/NetworkPage.jsx:86 #, fuzzy @@ -607,8 +602,9 @@ msgstr "" "fehlender oder deaktivierter Hardware." #: src/components/network/NetworkPage.jsx:99 +#, fuzzy msgid "Wi-Fi" -msgstr "" +msgstr "Wi-Fi" #. TRANSLATORS: button label, connect to a WiFi network #: src/components/network/NetworkPage.jsx:102 @@ -618,14 +614,13 @@ msgid "Connect" msgstr "Verbinden" #: src/components/network/NetworkPage.jsx:109 -#, fuzzy, c-format +#, c-format msgid "Conected to %s" -msgstr "Verbunden (%s)" +msgstr "Verbunden mit %s" #: src/components/network/NetworkPage.jsx:114 -#, fuzzy msgid "No connected yet" -msgstr "Noch nicht ausgewählt" +msgstr "Noch nicht verbunden" #: src/components/network/NetworkPage.jsx:115 #, fuzzy @@ -636,8 +631,9 @@ msgstr "" "konfiguriert." #: src/components/network/NetworkPage.jsx:136 +#, fuzzy msgid "Wired" -msgstr "" +msgstr "Kabelgebunden" #: src/components/network/NetworkPage.jsx:139 #, fuzzy @@ -720,12 +716,11 @@ msgstr "Trennen" #: src/components/network/WifiNetworksListPage.jsx:142 #, fuzzy msgid "Connect to a hidden network" -msgstr "Mit verborgenem Netzwerk verbinden" +msgstr "Mit einem verborgenem Netzwerk verbinden" #: src/components/network/WifiNetworksListPage.jsx:153 -#, fuzzy msgid "configured" -msgstr "Nicht konfigurieren" +msgstr "konfiguriert" #: src/components/network/WifiNetworksListPage.jsx:244 msgid "Connect to hidden network" @@ -1707,14 +1702,12 @@ msgstr[0] "" msgstr[1] "" #: src/components/storage/ProposalActionsSummary.jsx:57 -#, fuzzy msgid "Destructive actions are not allowed" -msgstr "Es ist %d zerstörerische Aktion geplant" +msgstr "Destruktive Aktionen sind nicht erlaubt" #: src/components/storage/ProposalActionsSummary.jsx:59 -#, fuzzy msgid "Destructive actions are allowed" -msgstr "Es ist %d zerstörerische Aktion geplant" +msgstr "Destruktive Aktionen sind erlaubt" #: src/components/storage/ProposalActionsSummary.jsx:66 #, c-format @@ -1727,34 +1720,33 @@ msgstr[1] "Es sind %d zerstörerische Aktionen geplant" #: src/components/storage/ProposalActionsSummary.jsx:126 #, fuzzy msgid "affecting" -msgstr "Beeinflusst" +msgstr "beeinflusst" #: src/components/storage/ProposalActionsSummary.jsx:107 -#, fuzzy msgid "Shrinking partitions is not allowed" -msgstr "Partitionen verkleinern" +msgstr "Verkleinern von Partitionen ist nicht erlaubt" #: src/components/storage/ProposalActionsSummary.jsx:111 -#, fuzzy msgid "Shrinking partitions is allowed" -msgstr "Partitionen verkleinern" +msgstr "Verkleinern von Partitionen ist erlaubt" #: src/components/storage/ProposalActionsSummary.jsx:113 -#, fuzzy msgid "Shrinking some partitions is allowed but not needed" -msgstr "und Partitionen des Installationsgeräts verkleinern" +msgstr "" +"Das Verkleinern einiger Partitionen ist erlaubt, aber nicht erforderlich" #: src/components/storage/ProposalActionsSummary.jsx:116 -#, fuzzy, c-format +#, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" -msgstr[0] "%s-Partitionstabelle" -msgstr[1] "%s-Partitionstabelle" +msgstr[0] "%d Partition wird verkleinert" +msgstr[1] "%d Partitionen werden verkleinert" #: src/components/storage/ProposalActionsSummary.jsx:151 -#, fuzzy msgid "Cannot accommodate the required file systems for installation" -msgstr "Kann bei der Ferninstallation nicht geändert werden" +msgstr "" +"Die für die Installation erforderlichen Dateisysteme können nicht " +"untergebracht werden" #: src/components/storage/ProposalActionsSummary.jsx:160 #, c-format @@ -1764,9 +1756,8 @@ msgstr[0] "Geplante Aktion überprüfen" msgstr[1] "Geplante %d Aktionen überprüfen" #: src/components/storage/ProposalActionsSummary.jsx:179 -#, fuzzy msgid "Waiting for actions information..." -msgstr "Warten auf Informationen zur Speicherkonfiguration" +msgstr "Warten auf Informationen zu Aktionen ..." #: src/components/storage/ProposalPage.jsx:314 msgid "Planned Actions" @@ -1778,11 +1769,11 @@ msgstr "Warten auf Informationen zur Speicherkonfiguration" #: src/components/storage/ProposalResultSection.jsx:70 msgid "Final layout" -msgstr "" +msgstr "Endgültige Anordnung" #: src/components/storage/ProposalResultSection.jsx:71 msgid "The systems will be configured as displayed below." -msgstr "" +msgstr "Die Systeme werden wie unten dargestellt konfiguriert." #: src/components/storage/ProposalResultSection.jsx:78 msgid "Storage proposal not possible" @@ -1831,9 +1822,9 @@ msgstr "" #. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) #: src/components/storage/SpaceActionsTable.jsx:75 -#, c-format +#, c-format, fuzzy msgid "Space action selector for %s" -msgstr "" +msgstr "Speicherplatz-Aktionsselektor für %s" #: src/components/storage/SpaceActionsTable.jsx:79 msgid "Allow resize" @@ -1860,8 +1851,9 @@ msgid "Actions to find space" msgstr "Aktionen, um Platz zu finden" #: src/components/storage/SpacePolicySelection.jsx:170 +#, fuzzy msgid "Space policy" -msgstr "" +msgstr "Speicherplatzrichtlinie" #: src/components/storage/VolumeDialog.jsx:78 #, c-format diff --git a/web/po/es.po b/web/po/es.po index 6279b9b7a9..9f01eecdf3 100644 --- a/web/po/es.po +++ b/web/po/es.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-05-12 20:43+0000\n" -"Last-Translator: Alejandro Jiménez \n" +"PO-Revision-Date: 2024-07-02 11:46+0000\n" +"Last-Translator: Victor hck \n" "Language-Team: Spanish \n" "Language: es\n" @@ -17,21 +17,19 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.9.1\n" +"X-Generator: Weblate 5.6.2\n" #: src/MainLayout.jsx:40 -#, fuzzy msgid "Agama" -msgstr "Acerca de Agama" +msgstr "Agama" #: src/MainLayout.jsx:82 msgid "Change product" msgstr "Cambiar de producto" #: src/components/core/About.jsx:49 -#, fuzzy msgid "About" -msgstr "Acerca de Agama" +msgstr "Acerca de" #: src/components/core/About.jsx:71 msgid "About Agama" @@ -165,17 +163,14 @@ msgid "Reboot" msgstr "Reiniciar" #: src/components/core/InstallationProgress.jsx:30 -#, fuzzy msgid "Installing the system, please wait ..." -msgstr "Cargando productos disponibles, por favor espere..." +msgstr "Instalando el sistema, por favor espere..." #: src/components/core/InstallerOptions.jsx:83 -#, fuzzy msgid "Show installer options" -msgstr "Ocultar las opciones del instalador" +msgstr "Mostrar las opciones del instalador" #: src/components/core/InstallerOptions.jsx:88 -#, fuzzy msgid "Installer options" msgstr "Opciones del instalador" @@ -188,15 +183,12 @@ msgstr "Idioma" #: src/components/core/InstallerOptions.jsx:114 #: src/components/core/InstallerOptions.jsx:121 -#, fuzzy msgid "Keyboard layout" -msgstr "Escoger un producto" +msgstr "Esquema del teclado" #: src/components/core/InstallerOptions.jsx:130 -#, fuzzy msgid "Cannot be changed in remote installation" -msgstr "" -"La distribución del teclado no se puede cambiar en la instalación remota" +msgstr "No se puede cambiar en instalación remota" #: src/components/core/InstallerOptions.jsx:135 #: src/components/network/IpSettingsForm.jsx:210 @@ -215,6 +207,7 @@ msgstr "Aceptar" msgid "" "Before starting the installation, you need to address the following problems:" msgstr "" +"Antes de comenzar la instalación, debe solucionar los siguientes problemas:" #: src/components/core/ListSearch.jsx:51 msgid "Search" @@ -228,7 +221,7 @@ msgstr "" #: src/components/core/LoginPage.jsx:63 msgid "Could not authenticate against the server, please check it." -msgstr "" +msgstr "No se pudo autenticar en el servidor, por favor verifíquelo." #. TRANSLATORS: Title for a form to provide the password for the root user. %s #. will be replaced by "root" @@ -242,14 +235,11 @@ msgstr "Iniciar sesión como %s" #. it and keep the brackets. #: src/components/core/LoginPage.jsx:76 msgid "The installer requires [root] user privileges." -msgstr "" +msgstr "El instalador requiere privilegios de usuario [root]." #: src/components/core/LoginPage.jsx:95 -#, fuzzy msgid "Please, provide its password to log in to the system." -msgstr "" -"El instalador requiere privilegios del usuario %s. Por favor, introduce su " -"contraseña para iniciar sesión en el sistema." +msgstr "Por favor, proporcione su contraseña para iniciar sesión en el sistema." #: src/components/core/LoginPage.jsx:98 msgid "Login form" @@ -265,7 +255,7 @@ msgstr "Iniciar sesión" #: src/components/core/LoginPage.jsx:124 msgid "More about this" -msgstr "" +msgstr "Más acerca de esto" #: src/components/core/LogsButton.jsx:103 msgid "Collecting logs..." @@ -318,22 +308,20 @@ msgid "Loading data..." msgstr "Cargando los datos..." #: src/components/core/ProgressReport.jsx:50 -#, fuzzy msgid "Finished" -msgstr "Finalizar" +msgstr "Finalizado" #: src/components/core/ProgressReport.jsx:59 msgid "In progress" -msgstr "" +msgstr "En progreso" #: src/components/core/ProgressReport.jsx:70 msgid "Pending" -msgstr "" +msgstr "Pendiente" #: src/components/core/ProgressReport.jsx:134 -#, fuzzy msgid "Waiting for progress status..." -msgstr "Esperando el informe de progreso" +msgstr "Esperando el estado del progreso..." #: src/components/core/RowActions.jsx:64 #: src/components/storage/PartitionsField.jsx:454 @@ -353,19 +341,15 @@ msgstr "seleccionado automáticamente" #. TRANSLATORS: page title #: src/components/core/ServerError.jsx:34 msgid "Agama Error" -msgstr "" +msgstr "Error de Agama" #: src/components/core/ServerError.jsx:38 -#, fuzzy msgid "Cannot connect to Agama server" -msgstr "No se puede conectar al servidor Cockpit" +msgstr "No se pud conectar al servidor de Agama" #: src/components/core/ServerError.jsx:43 -#, fuzzy msgid "Please, check whether it is running." -msgstr "" -"No se pudo conectar al servicio D-Bus. Por favor, compruebe si está " -"funcionando." +msgstr "Por favor, compruebe si está funcionando." #. TRANSLATORS: button label #: src/components/core/ServerError.jsx:51 @@ -378,12 +362,11 @@ msgstr "Filtrar por descripción o código de mapa de teclas" #: src/components/l10n/KeyboardSelection.jsx:85 msgid "None of the keymaps match the filter." -msgstr "" +msgstr "Ninguno de los mapas de teclas coincide con el filtro." #: src/components/l10n/KeyboardSelection.jsx:92 -#, fuzzy msgid "Keyboard selection" -msgstr "Selección de software" +msgstr "Selección de teclado" #: src/components/l10n/KeyboardSelection.jsx:107 #: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 @@ -401,9 +384,8 @@ msgstr "Localización" #: src/components/l10n/L10nPage.jsx:68 src/components/l10n/L10nPage.jsx:79 #: src/components/l10n/L10nPage.jsx:90 -#, fuzzy msgid "Not selected yet" -msgstr "Ningún dispositivo seleccionado todavía" +msgstr "Aún no seleccionado" #: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 #: src/components/l10n/L10nPage.jsx:93 @@ -429,12 +411,11 @@ msgstr "Filtrar por idioma, territorio o código local" #: src/components/l10n/LocaleSelection.jsx:84 msgid "None of the locales match the filter." -msgstr "" +msgstr "Ninguna de las configuraciones regionales coincide con el filtro." #: src/components/l10n/LocaleSelection.jsx:91 -#, fuzzy msgid "Locale selection" -msgstr "Selección de software" +msgstr "Selección de configuración regional" #: src/components/l10n/TimezoneSelection.jsx:71 msgid "Filter by territory, time zone code or UTC offset" @@ -442,12 +423,11 @@ msgstr "Filtrar por territorio, código de zona horaria o compensación UTC" #: src/components/l10n/TimezoneSelection.jsx:122 msgid "None of the time zones match the filter." -msgstr "" +msgstr "Ninguna de las zonas horarias coincide con el filtro." #: src/components/l10n/TimezoneSelection.jsx:129 -#, fuzzy msgid " Timezone selection" -msgstr "Cambiar selección" +msgstr " Selección de zona horaria" #: src/components/layout/Loading.jsx:31 msgid "Loading installation environment, please wait." @@ -597,14 +577,13 @@ msgstr "Puerta de enlace" #: src/components/network/IpSettingsForm.jsx:178 msgid "Gateway can be defined only in 'Manual' mode" -msgstr "" +msgstr "La puerta de enlace sólo se puede definir en modo 'Manual'" #: src/components/network/NetworkPage.jsx:85 msgid "No Wi-Fi supported" -msgstr "" +msgstr "Wi-Fi no admitida" #: src/components/network/NetworkPage.jsx:86 -#, fuzzy msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." @@ -614,7 +593,7 @@ msgstr "" #: src/components/network/NetworkPage.jsx:99 msgid "Wi-Fi" -msgstr "" +msgstr "WiFi" #. TRANSLATORS: button label, connect to a WiFi network #: src/components/network/NetworkPage.jsx:102 @@ -624,29 +603,26 @@ msgid "Connect" msgstr "Conectar" #: src/components/network/NetworkPage.jsx:109 -#, fuzzy, c-format +#, c-format msgid "Conected to %s" -msgstr "Conectado (%s)" +msgstr "Conectado a %s" #: src/components/network/NetworkPage.jsx:114 -#, fuzzy msgid "No connected yet" -msgstr "Ningún dispositivo seleccionado todavía" +msgstr "Aún no conectado" #: src/components/network/NetworkPage.jsx:115 -#, fuzzy msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." msgstr "El sistema aún no se ha configurado para conectarse a una red WiFi." #: src/components/network/NetworkPage.jsx:136 msgid "Wired" -msgstr "" +msgstr "Cableada" #: src/components/network/NetworkPage.jsx:139 -#, fuzzy msgid "No wired connections found" -msgstr "No se encontraron conexiones por cable." +msgstr "No se encontraron conexiones por cable" #: src/components/network/NetworkPage.jsx:149 #: src/components/network/routes.js:59 @@ -717,19 +693,16 @@ msgid "Disconnected" msgstr "Desconectado" #: src/components/network/WifiNetworksListPage.jsx:121 -#, fuzzy msgid "Disconnect" -msgstr "Desconectado" +msgstr "Desconectar" #: src/components/network/WifiNetworksListPage.jsx:142 -#, fuzzy msgid "Connect to a hidden network" msgstr "Conectar a una red oculta" #: src/components/network/WifiNetworksListPage.jsx:153 -#, fuzzy msgid "configured" -msgstr "No configurar" +msgstr "Configurado" #: src/components/network/WifiNetworksListPage.jsx:244 msgid "Connect to hidden network" @@ -767,44 +740,42 @@ msgid "Software" msgstr "Software" #: src/components/overview/OverviewPage.jsx:52 -#, fuzzy msgid "Ready for installation" -msgstr "Confirmar instalación" +msgstr "Preparado para la instalación" #: src/components/overview/OverviewPage.jsx:102 -#, fuzzy msgid "Installation" -msgstr "Instalando" +msgstr "Instalación" #: src/components/overview/OverviewPage.jsx:103 msgid "Before installing, please check the following problems." -msgstr "" +msgstr "Antes de instalar, verifique los siguientes problemas." #: src/components/overview/OverviewPage.jsx:114 -#, fuzzy msgid "" "Take your time to check your configuration before starting the installation " "process." msgstr "" -"La instalación configurará las particiones para arrancar en el disco de " -"instalación." +"Dedica un tiempo para verificar la configuración antes de iniciar el proceso " +"de instalación." #: src/components/overview/OverviewPage.jsx:123 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." msgstr "" +"Estas son las configuraciones de instalación más relevantes. No dude en " +"explorar las secciones del menú para obtener más detalles." #: src/components/overview/SoftwareSection.jsx:60 -#, fuzzy msgid "The installation will take" -msgstr "La instalación ocupará %s" +msgstr "La instalación ocupará" #. TRANSLATORS: %s will be replaced with the installation size, example: "5GiB". #: src/components/overview/SoftwareSection.jsx:67 -#, fuzzy, c-format +#, c-format msgid "The installation will take %s including:" -msgstr "La instalación ocupará %s" +msgstr "La instalación ocupará %s incluyendo:" #: src/components/overview/StorageSection.jsx:53 msgid "" @@ -917,7 +888,7 @@ msgstr "" #: src/components/overview/routes.js:30 msgid "Overview" -msgstr "" +msgstr "Descripción general" #: src/components/product/ProductRegistrationPage.jsx:66 #, c-format @@ -937,9 +908,8 @@ msgid "Loading available products, please wait..." msgstr "Cargando productos disponibles, por favor espere..." #: src/components/product/ProductSelectionProgress.jsx:53 -#, fuzzy msgid "Configuring the product, please wait ..." -msgstr "Cargando productos disponibles, por favor espere..." +msgstr "Configurando el producto, por favor espere..." #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 @@ -970,9 +940,8 @@ msgstr "" "Los siguientes patrones de software están seleccionados para la instalación:" #: src/components/software/SoftwarePage.jsx:165 -#, fuzzy msgid "Selected patterns" -msgstr "Selecciona la zona horaria" +msgstr "Seleccione los patrones" #: src/components/software/SoftwarePage.jsx:168 msgid "Change selection" @@ -980,7 +949,7 @@ msgstr "Cambiar selección" #: src/components/software/SoftwarePatternsSelection.jsx:230 msgid "None of the patterns match the filter." -msgstr "" +msgstr "Ninguno de los patrones coincide con el filtro." #: src/components/software/SoftwarePatternsSelection.jsx:238 msgid "Software selection" @@ -990,18 +959,20 @@ msgstr "Selección de software" #: src/components/software/SoftwarePatternsSelection.jsx:241 #: src/components/software/SoftwarePatternsSelection.jsx:242 msgid "Filter by pattern title or description" -msgstr "" +msgstr "Filtrar por título o descripción del patrón" #. TRANSLATORS: %s will be replaced by the estimated installation size, #. example: "728.8 MiB" #: src/components/software/UsedSize.jsx:33 -#, fuzzy, c-format +#, c-format msgid "Installation will take %s." -msgstr "La instalación ocupará %s" +msgstr "La instalación ocupará %s." #: src/components/software/UsedSize.jsx:38 msgid "This space includes the base system and the selected software patterns." msgstr "" +"Este espacio incluye el sistema base y los patrones de software " +"seleccionados." #: src/components/storage/BootConfigField.jsx:43 msgid "Change boot options" @@ -1044,9 +1015,8 @@ msgstr "" "Las particiones para arrancar se asignarán en el disco de instalación (%s)." #: src/components/storage/BootSelection.jsx:154 -#, fuzzy msgid "Select booting partition" -msgstr "Seleccione qué hacer con cada partición." +msgstr "Seleccion la partición de arranque" #: src/components/storage/BootSelection.jsx:170 #: src/components/storage/iscsi/NodeStartupOptions.js:27 @@ -1193,9 +1163,8 @@ msgid "Remove max channel filter" msgstr "Eliminar filtro de canal máximo" #: src/components/storage/DeviceSelection.jsx:101 -#, fuzzy msgid "Loading data, please wait a second..." -msgstr "Cargando productos disponibles, por favor espere..." +msgstr "Cargando los datos, por favor espere..." #. TRANSLATORS: description for using plain partitions for installing the #. system, the text in the square brackets [] is displayed in bold, use only @@ -1223,24 +1192,20 @@ msgstr "" "los dispositivos seleccionados." #: src/components/storage/DeviceSelection.jsx:149 -#, fuzzy msgid "Select installation device" -msgstr "Dispositivo de instalación" +msgstr "Seleccionar el dispositivo de instalación" #: src/components/storage/DeviceSelection.jsx:155 -#, fuzzy msgid "Install new system on" -msgstr "Opciones del instalador" +msgstr "Instalar nuevo sistema en" #: src/components/storage/DeviceSelection.jsx:158 -#, fuzzy msgid "An existing disk" -msgstr "Reducir las particiones existentes" +msgstr "Un disco existente" #: src/components/storage/DeviceSelection.jsx:167 -#, fuzzy msgid "A new LVM Volume Group" -msgstr "nuevo grupo de volúmenes LVM" +msgstr "Un nuevo Grupo de Volúmen LVM" #: src/components/storage/DeviceSelection.jsx:192 msgid "Device selector for target disk" @@ -1255,7 +1220,6 @@ msgid "Prepare more devices by configuring advanced" msgstr "Preparar más dispositivos configurando de forma avanzada" #: src/components/storage/DeviceSelection.jsx:229 -#, fuzzy msgid "storage techs" msgstr "tecnologías de almacenamiento" @@ -1366,13 +1330,12 @@ msgid "Encryption" msgstr "Cifrado" #: src/components/storage/EncryptionField.jsx:39 -#, fuzzy msgid "" "Protection for the information stored at the device, including data, " "programs, and system files." msgstr "" -"Full Disk Encryption (FDE) permite proteger la información almacenada en el " -"dispositivo, incluidos datos, programas y archivos del sistema." +"Protección de la información almacenada en el dispositivo, incluidos datos, " +"programas y archivos del sistema." #: src/components/storage/EncryptionField.jsx:42 msgid "disabled" @@ -1387,13 +1350,12 @@ msgid "using TPM unlocking" msgstr "usando el desbloqueo TPM" #: src/components/storage/EncryptionField.jsx:58 -#, fuzzy msgid "Enable" -msgstr "activado" +msgstr "Habilitado" #: src/components/storage/EncryptionField.jsx:58 msgid "Modify" -msgstr "" +msgstr "Modificar" #: src/components/storage/EncryptionSettingsDialog.jsx:37 msgid "" @@ -1434,28 +1396,24 @@ msgstr "Dispositivo de instalación" #. TRANSLATORS: The storage "Installation device" field's description. #: src/components/storage/InstallationDeviceField.jsx:38 -#, fuzzy msgid "Main disk or LVM Volume Group for installation." -msgstr "" -"Seleccionar el disco principal o el grupo de volúmenes LVM para la " -"instalación." +msgstr "Disco principal o el grupo de volúmenes LVM para la instalación." #. TRANSLATORS: %s is the installation disk (eg. "/dev/sda, 80 GiB) #: src/components/storage/InstallationDeviceField.jsx:52 -#, fuzzy, c-format +#, c-format msgid "File systems created as new partitions at %s" -msgstr "Crear una nueva partición" +msgstr "Sistemas de archivos creados como particiones nuevas en %s" #: src/components/storage/InstallationDeviceField.jsx:55 -#, fuzzy msgid "File systems created at a new LVM volume group" -msgstr "Selector de dispositivo para nuevo grupo de volúmenes LVM" +msgstr "Sistemas de archivos creados en un nuevo grupo de volúmenes LVM" #. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) #: src/components/storage/InstallationDeviceField.jsx:59 -#, fuzzy, c-format +#, c-format msgid "File systems created at a new LVM volume group on %s" -msgstr "nuevo grupo de volúmenes LVM en %s" +msgstr "Sistemas de archivos creados en un nuevo grupo de volúmenes LVM en %s" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:73 @@ -1682,18 +1640,16 @@ msgid "Partitions and file systems" msgstr "Particiones y sistemas de archivos" #: src/components/storage/PartitionsField.jsx:802 -#, fuzzy msgid "" "Structure of the new system, including any additional partition needed for " "booting" msgstr "" "Estructura del nuevo sistema, incluida cualquier partición adicional " -"necesaria para el arranque," +"necesaria para el arranque" #: src/components/storage/PartitionsField.jsx:808 -#, fuzzy msgid "Show partitions and file-systems actions" -msgstr "Particiones y sistemas de archivos" +msgstr "Mostrar acciones de particiones y sistemas de archivos" #. TRANSLATORS: show/hide toggle action, this is a clickable link #: src/components/storage/ProposalActionsDialog.jsx:62 @@ -1712,14 +1668,12 @@ msgstr[0] "Mostrar %d acción de subvolumen" msgstr[1] "Mostrar %d acciones de subvolumen" #: src/components/storage/ProposalActionsSummary.jsx:57 -#, fuzzy msgid "Destructive actions are not allowed" -msgstr "Hay %d acción destructiva planeada" +msgstr "No se permiten acciones destructivas" #: src/components/storage/ProposalActionsSummary.jsx:59 -#, fuzzy msgid "Destructive actions are allowed" -msgstr "Hay %d acción destructiva planeada" +msgstr "Se permiten acciones destructivas" #: src/components/storage/ProposalActionsSummary.jsx:66 #, c-format @@ -1730,49 +1684,43 @@ msgstr[1] "Hay %d acciones destructivas planeadas" #: src/components/storage/ProposalActionsSummary.jsx:79 #: src/components/storage/ProposalActionsSummary.jsx:126 -#, fuzzy msgid "affecting" -msgstr "Sistemas afectados" +msgstr "afectados" #: src/components/storage/ProposalActionsSummary.jsx:107 -#, fuzzy msgid "Shrinking partitions is not allowed" -msgstr "Reducir las particiones existentes" +msgstr "No se permite reducir las particiones existentes" #: src/components/storage/ProposalActionsSummary.jsx:111 -#, fuzzy msgid "Shrinking partitions is allowed" -msgstr "Reducir las particiones existentes" +msgstr "Se permite reducir las particiones existentes" #: src/components/storage/ProposalActionsSummary.jsx:113 -#, fuzzy msgid "Shrinking some partitions is allowed but not needed" -msgstr "reducir las particiones del dispositivo de instalación" +msgstr "Se permite reducir algunas particiones, pero no es necesario" #: src/components/storage/ProposalActionsSummary.jsx:116 -#, fuzzy, c-format +#, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" -msgstr[0] "%s tabla de partición" -msgstr[1] "%s tabla de partición" +msgstr[0] "%d partición se reducirá" +msgstr[1] "%d particiones se reducirán" #: src/components/storage/ProposalActionsSummary.jsx:151 -#, fuzzy msgid "Cannot accommodate the required file systems for installation" msgstr "" -"La distribución del teclado no se puede cambiar en la instalación remota" +"No se pueden acomodar los sistemas de archivos necesarios para la instalación" #: src/components/storage/ProposalActionsSummary.jsx:160 -#, fuzzy, c-format +#, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" -msgstr[0] "Comprueba todas las acciones planeadas" -msgstr[1] "Comprueba todas las acciones planeadas" +msgstr[0] "Comprueba la acción planeada" +msgstr[1] "Comprueba las %d acciones planeadas" #: src/components/storage/ProposalActionsSummary.jsx:179 -#, fuzzy msgid "Waiting for actions information..." -msgstr "Esperando información sobre la configuración de almacenamiento" +msgstr "Esperando información de acciones..." #: src/components/storage/ProposalPage.jsx:314 msgid "Planned Actions" @@ -1784,15 +1732,15 @@ msgstr "Esperando información sobre la configuración de almacenamiento" #: src/components/storage/ProposalResultSection.jsx:70 msgid "Final layout" -msgstr "" +msgstr "Diseño final" #: src/components/storage/ProposalResultSection.jsx:71 msgid "The systems will be configured as displayed below." -msgstr "" +msgstr "Los sistemas se configurarán como se muestra a continuación." #: src/components/storage/ProposalResultSection.jsx:78 msgid "Storage proposal not possible" -msgstr "" +msgstr "Propuesta de almacenamiento no posible" #: src/components/storage/ProposalResultTable.jsx:74 msgid "New" @@ -1865,7 +1813,6 @@ msgid "Actions to find space" msgstr "Acciones para encontrar espacio" #: src/components/storage/SpacePolicySelection.jsx:170 -#, fuzzy msgid "Space policy" msgstr "Política de espacio" @@ -2370,7 +2317,7 @@ msgstr "Conectado (%s)" #: src/components/storage/iscsi/NodesPresenter.jsx:82 msgid "Login" -msgstr "Iniciar sesión" +msgstr "Acceder" #: src/components/storage/iscsi/NodesPresenter.jsx:86 msgid "Logout" @@ -2413,7 +2360,7 @@ msgstr "Objetivos" #: src/components/storage/routes.js:36 msgid "Proposal" -msgstr "" +msgstr "Propuesta" #: src/components/storage/utils.js:64 msgid "KiB" @@ -2448,9 +2395,8 @@ msgstr "" #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space deleting current content". Keep it short #: src/components/storage/utils.js:82 -#, fuzzy msgid "deleting current content" -msgstr "Eliminar el contenido actual" +msgstr "eliminando el contenido actual" #: src/components/storage/utils.js:87 msgid "Shrink existing partitions" @@ -2465,9 +2411,8 @@ msgstr "" #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space shrinking partitions". Keep it short. #: src/components/storage/utils.js:92 -#, fuzzy msgid "shrinking partitions" -msgstr "Reducir las particiones existentes" +msgstr "reduciendo las particiones" #: src/components/storage/utils.js:97 msgid "Use available space" @@ -2497,9 +2442,8 @@ msgstr "Seleccione qué hacer con cada partición." #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space with custom actions". Keep it short. #: src/components/storage/utils.js:112 -#, fuzzy msgid "with custom actions" -msgstr "realizar un conjunto personalizado de acciones" +msgstr "con acciones personalizadas" #: src/components/users/FirstUser.jsx:35 msgid "No user defined yet." @@ -2545,23 +2489,20 @@ msgid "Use suggested username" msgstr "Usar nombre de usuario sugerido" #: src/components/users/FirstUserForm.jsx:140 -#, fuzzy msgid "All fields are required" -msgstr "Se requiere un valor de tamaño" +msgstr "Todos los campos son obligatorios" #: src/components/users/FirstUserForm.jsx:147 msgid "Please, try again." -msgstr "" +msgstr "Por favor, inténtelo de nuevo." #: src/components/users/FirstUserForm.jsx:197 -#, fuzzy msgid "Create user" -msgstr "Crear cuenta de usuario" +msgstr "Crear usuario" #: src/components/users/FirstUserForm.jsx:197 -#, fuzzy msgid "Edit user" -msgstr "Editar %s" +msgstr "Editar usuario" #: src/components/users/FirstUserForm.jsx:214 #: src/components/users/FirstUserForm.jsx:216 @@ -2670,7 +2611,7 @@ msgstr "Limpiar" #: src/components/users/UsersPage.jsx:45 msgid "First user" -msgstr "" +msgstr "Primer usuario" #: src/components/users/UsersPage.jsx:52 msgid "Root authentication" diff --git a/web/po/ja.po b/web/po/ja.po index 208c725fd7..c5119e445a 100644 --- a/web/po/ja.po +++ b/web/po/ja.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-20 00:46+0000\n" +"PO-Revision-Date: 2024-07-04 00:46+0000\n" "Last-Translator: Yasuhiko Kamata \n" "Language-Team: Japanese \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.5.5\n" +"X-Generator: Weblate 5.6.2\n" #: src/MainLayout.jsx:40 msgid "Agama" @@ -161,9 +161,8 @@ msgid "Reboot" msgstr "再起動" #: src/components/core/InstallationProgress.jsx:30 -#, fuzzy msgid "Installing the system, please wait ..." -msgstr "データを読み込んでいます。しばらくお待ちください..." +msgstr "システムをインストールしています。しばらくお待ちください..." #: src/components/core/InstallerOptions.jsx:83 msgid "Show installer options" @@ -182,9 +181,8 @@ msgstr "言語" #: src/components/core/InstallerOptions.jsx:114 #: src/components/core/InstallerOptions.jsx:121 -#, fuzzy msgid "Keyboard layout" -msgstr "キーボードレイアウトを選択してください" +msgstr "キーボードレイアウト" #: src/components/core/InstallerOptions.jsx:130 msgid "Cannot be changed in remote installation" @@ -307,22 +305,20 @@ msgid "Loading data..." msgstr "データを読み込んでいます..." #: src/components/core/ProgressReport.jsx:50 -#, fuzzy msgid "Finished" msgstr "完了" #: src/components/core/ProgressReport.jsx:59 msgid "In progress" -msgstr "" +msgstr "処理中" #: src/components/core/ProgressReport.jsx:70 msgid "Pending" -msgstr "" +msgstr "保留中" #: src/components/core/ProgressReport.jsx:134 -#, fuzzy msgid "Waiting for progress status..." -msgstr "経過レポートを待機しています" +msgstr "進捗状態を待機しています..." #: src/components/core/RowActions.jsx:64 #: src/components/storage/PartitionsField.jsx:454 @@ -583,20 +579,18 @@ msgstr "'手動' モードの場合にのみゲートウエイを設定できま #: src/components/network/NetworkPage.jsx:85 msgid "No Wi-Fi supported" -msgstr "" +msgstr "Wi-Fi サポートがありません" #: src/components/network/NetworkPage.jsx:86 -#, fuzzy msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." -msgstr "" -"このシステムは WiFi 接続には対応していません。無線ハードウエアが存在していな" +msgstr "このシステムは WiFi 接続には対応していません。無線ハードウエアが存在していな" "いか、無効化されているものと思われます。" #: src/components/network/NetworkPage.jsx:99 msgid "Wi-Fi" -msgstr "" +msgstr "Wi-Fi" #. TRANSLATORS: button label, connect to a WiFi network #: src/components/network/NetworkPage.jsx:102 @@ -606,30 +600,26 @@ msgid "Connect" msgstr "接続" #: src/components/network/NetworkPage.jsx:109 -#, fuzzy, c-format +#, c-format msgid "Conected to %s" -msgstr "接続済み (%s)" +msgstr "%s に接続済み" #: src/components/network/NetworkPage.jsx:114 -#, fuzzy msgid "No connected yet" -msgstr "まだ何も選択していません" +msgstr "まだ接続していません" #: src/components/network/NetworkPage.jsx:115 -#, fuzzy msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." -msgstr "" -"このシステムでは、まだ WiFi ネットワークへの接続設定を実施していません。" +msgstr "このシステムでは、まだ WiFi ネットワークへの接続設定を実施していません。" #: src/components/network/NetworkPage.jsx:136 msgid "Wired" -msgstr "" +msgstr "有線" #: src/components/network/NetworkPage.jsx:139 -#, fuzzy msgid "No wired connections found" -msgstr "有線接続が見つかりませんでした。" +msgstr "有線接続が見つかりませんでした" #: src/components/network/NetworkPage.jsx:149 #: src/components/network/routes.js:59 @@ -703,14 +693,12 @@ msgid "Disconnect" msgstr "切断" #: src/components/network/WifiNetworksListPage.jsx:142 -#, fuzzy msgid "Connect to a hidden network" msgstr "ステルスネットワークへの接続" #: src/components/network/WifiNetworksListPage.jsx:153 -#, fuzzy msgid "configured" -msgstr "設定しない" +msgstr "設定済み" #: src/components/network/WifiNetworksListPage.jsx:244 msgid "Connect to hidden network" @@ -1660,14 +1648,12 @@ msgid_plural "Show %d subvolume actions" msgstr[0] "%d 個のサブボリューム処理を表示" #: src/components/storage/ProposalActionsSummary.jsx:57 -#, fuzzy msgid "Destructive actions are not allowed" -msgstr "%d 個の破壊的な処理が提案されています" +msgstr "破壊的な処理は許可しません" #: src/components/storage/ProposalActionsSummary.jsx:59 -#, fuzzy msgid "Destructive actions are allowed" -msgstr "%d 個の破壊的な処理が提案されています" +msgstr "破壊的な処理を許可します" #: src/components/storage/ProposalActionsSummary.jsx:66 #, c-format @@ -1677,35 +1663,30 @@ msgstr[0] "%d 個の破壊的な処理が提案されています" #: src/components/storage/ProposalActionsSummary.jsx:79 #: src/components/storage/ProposalActionsSummary.jsx:126 -#, fuzzy msgid "affecting" -msgstr "下記に影響があります:" +msgstr "下記に影響があります" #: src/components/storage/ProposalActionsSummary.jsx:107 -#, fuzzy msgid "Shrinking partitions is not allowed" -msgstr "パーティションの縮小" +msgstr "パーティションの縮小を許可しません" #: src/components/storage/ProposalActionsSummary.jsx:111 -#, fuzzy msgid "Shrinking partitions is allowed" -msgstr "パーティションの縮小" +msgstr "パーティションの縮小を許可します" #: src/components/storage/ProposalActionsSummary.jsx:113 -#, fuzzy msgid "Shrinking some partitions is allowed but not needed" -msgstr "インストール先のディスクのパーティションを縮小します" +msgstr "パーティションの縮小を許可していますが不要です" #: src/components/storage/ProposalActionsSummary.jsx:116 -#, fuzzy, c-format +#, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" -msgstr[0] "%s パーティションテーブル" +msgstr[0] "%d 個のパーティションを縮小します" #: src/components/storage/ProposalActionsSummary.jsx:151 -#, fuzzy msgid "Cannot accommodate the required file systems for installation" -msgstr "リモートインストールの場合は変更できません" +msgstr "インストールに必要なファイルシステムを調整できません" #: src/components/storage/ProposalActionsSummary.jsx:160 #, c-format @@ -1714,9 +1695,8 @@ msgid_plural "Check the %d planned actions" msgstr[0] "%d 個の処理計画を確認する" #: src/components/storage/ProposalActionsSummary.jsx:179 -#, fuzzy msgid "Waiting for actions information..." -msgstr "ストレージ設定に関する情報を待機しています" +msgstr "処理に関する情報を待機しています..." #: src/components/storage/ProposalPage.jsx:314 msgid "Planned Actions" @@ -1728,11 +1708,11 @@ msgstr "ストレージ設定に関する情報を待機しています" #: src/components/storage/ProposalResultSection.jsx:70 msgid "Final layout" -msgstr "" +msgstr "最終形態" #: src/components/storage/ProposalResultSection.jsx:71 msgid "The systems will be configured as displayed below." -msgstr "" +msgstr "システムは下記に表示されているとおりに設定されます。" #: src/components/storage/ProposalResultSection.jsx:78 msgid "Storage proposal not possible" @@ -1809,7 +1789,6 @@ msgid "Actions to find space" msgstr "領域の検出処理" #: src/components/storage/SpacePolicySelection.jsx:170 -#, fuzzy msgid "Space policy" msgstr "領域ポリシー" diff --git a/web/po/pt_BR.po b/web/po/pt_BR.po index 4d72a8d652..b94c5f6155 100644 --- a/web/po/pt_BR.po +++ b/web/po/pt_BR.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-14 00:46+0000\n" +"PO-Revision-Date: 2024-07-05 14:47+0000\n" "Last-Translator: Rodrigo Macedo \n" "Language-Team: Portuguese (Brazil) \n" @@ -17,21 +17,19 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 5.5.5\n" +"X-Generator: Weblate 5.6.2\n" #: src/MainLayout.jsx:40 -#, fuzzy msgid "Agama" -msgstr "Sobre o Agama" +msgstr "Agama" #: src/MainLayout.jsx:82 msgid "Change product" msgstr "Alterar produto" #: src/components/core/About.jsx:49 -#, fuzzy msgid "About" -msgstr "Sobre o Agama" +msgstr "Sobre" #: src/components/core/About.jsx:71 msgid "About Agama" @@ -162,17 +160,14 @@ msgid "Reboot" msgstr "Reiniciar" #: src/components/core/InstallationProgress.jsx:30 -#, fuzzy msgid "Installing the system, please wait ..." -msgstr "Carregando produtos disponíveis, aguarde..." +msgstr "Instalando o sistema, aguarde..." #: src/components/core/InstallerOptions.jsx:83 -#, fuzzy msgid "Show installer options" -msgstr "Ocultar opções do instalador" +msgstr "Mostrar opções do instalador" #: src/components/core/InstallerOptions.jsx:88 -#, fuzzy msgid "Installer options" msgstr "Opções do instalador" @@ -185,14 +180,12 @@ msgstr "Idioma" #: src/components/core/InstallerOptions.jsx:114 #: src/components/core/InstallerOptions.jsx:121 -#, fuzzy msgid "Keyboard layout" -msgstr "Escolha um produto" +msgstr "Layout do teclado" #: src/components/core/InstallerOptions.jsx:130 -#, fuzzy msgid "Cannot be changed in remote installation" -msgstr "O layout do teclado não pode ser alterado na instalação remota" +msgstr "Não pode ser alterado na instalação remota" #: src/components/core/InstallerOptions.jsx:135 #: src/components/network/IpSettingsForm.jsx:210 @@ -211,6 +204,7 @@ msgstr "Aceitar" msgid "" "Before starting the installation, you need to address the following problems:" msgstr "" +"Antes de iniciar a instalação, você precisa resolver os seguintes problemas:" #: src/components/core/ListSearch.jsx:51 msgid "Search" @@ -236,14 +230,11 @@ msgstr "Faça login como %s" #. it and keep the brackets. #: src/components/core/LoginPage.jsx:76 msgid "The installer requires [root] user privileges." -msgstr "" +msgstr "O instalador requer privilégios de usuário [root]." #: src/components/core/LoginPage.jsx:95 -#, fuzzy msgid "Please, provide its password to log in to the system." -msgstr "" -"O instalador requer privilégios de usuário %s. Por favor, forneça sua senha " -"para fazer login no sistema." +msgstr "Por favor, forneça sua senha para fazer login no sistema." #: src/components/core/LoginPage.jsx:98 msgid "Login form" @@ -259,7 +250,7 @@ msgstr "Faça login" #: src/components/core/LoginPage.jsx:124 msgid "More about this" -msgstr "" +msgstr "Mais sobre isso" #: src/components/core/LogsButton.jsx:103 msgid "Collecting logs..." @@ -311,22 +302,20 @@ msgid "Loading data..." msgstr "Carregando dados..." #: src/components/core/ProgressReport.jsx:50 -#, fuzzy msgid "Finished" -msgstr "Concluir" +msgstr "Concluído" #: src/components/core/ProgressReport.jsx:59 msgid "In progress" -msgstr "" +msgstr "Em progresso" #: src/components/core/ProgressReport.jsx:70 msgid "Pending" -msgstr "" +msgstr "Pendente" #: src/components/core/ProgressReport.jsx:134 -#, fuzzy msgid "Waiting for progress status..." -msgstr "Aguardando o relatório do progresso" +msgstr "Aguardando status de progresso..." #: src/components/core/RowActions.jsx:64 #: src/components/storage/PartitionsField.jsx:454 @@ -366,14 +355,12 @@ msgid "Filter by description or keymap code" msgstr "Filtrar por descrição ou código do mapa de teclado" #: src/components/l10n/KeyboardSelection.jsx:85 -#, fuzzy msgid "None of the keymaps match the filter." -msgstr "Nenhum dos padrões corresponde ao filtro." +msgstr "Nenhum dos mapas de teclado corresponde ao filtro." #: src/components/l10n/KeyboardSelection.jsx:92 -#, fuzzy msgid "Keyboard selection" -msgstr "Seleção de software" +msgstr "Seleção de teclado" #: src/components/l10n/KeyboardSelection.jsx:107 #: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 @@ -391,9 +378,8 @@ msgstr "Localização" #: src/components/l10n/L10nPage.jsx:68 src/components/l10n/L10nPage.jsx:79 #: src/components/l10n/L10nPage.jsx:90 -#, fuzzy msgid "Not selected yet" -msgstr "Nenhum dispositivo selecionado ainda" +msgstr "Ainda não selecionado" #: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 #: src/components/l10n/L10nPage.jsx:93 @@ -418,28 +404,24 @@ msgid "Filter by language, territory or locale code" msgstr "Filtrar por idioma, território ou código de localidade" #: src/components/l10n/LocaleSelection.jsx:84 -#, fuzzy msgid "None of the locales match the filter." -msgstr "Nenhum dos padrões corresponde ao filtro." +msgstr "Nenhum dos locais corresponde ao filtro." #: src/components/l10n/LocaleSelection.jsx:91 -#, fuzzy msgid "Locale selection" -msgstr "Seleção de software" +msgstr "Seleção local" #: src/components/l10n/TimezoneSelection.jsx:71 msgid "Filter by territory, time zone code or UTC offset" msgstr "Filtrar por território, código de fuso horário ou deslocamento UTC" #: src/components/l10n/TimezoneSelection.jsx:122 -#, fuzzy msgid "None of the time zones match the filter." -msgstr "Nenhum dos padrões corresponde ao filtro." +msgstr "Nenhum dos fusos horários corresponde ao filtro." #: src/components/l10n/TimezoneSelection.jsx:129 -#, fuzzy msgid " Timezone selection" -msgstr "Alterar seleção" +msgstr " Seleção de fuso horário" #: src/components/layout/Loading.jsx:31 msgid "Loading installation environment, please wait." @@ -589,24 +571,23 @@ msgstr "Gateway" #: src/components/network/IpSettingsForm.jsx:178 msgid "Gateway can be defined only in 'Manual' mode" -msgstr "" +msgstr "O gateway só pode ser definido no modo 'Manual'" #: src/components/network/NetworkPage.jsx:85 msgid "No Wi-Fi supported" -msgstr "" +msgstr "Não há suporte para Wi-Fi" #: src/components/network/NetworkPage.jsx:86 -#, fuzzy msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." msgstr "" -"O sistema não suporta conexões WiFi, provavelmente devido a hardware ausente " -"ou desativado." +"O sistema não suporta conexões Wi-Fi, provavelmente devido a hardware " +"ausente ou desativado." #: src/components/network/NetworkPage.jsx:99 msgid "Wi-Fi" -msgstr "" +msgstr "Wi-Fi" #. TRANSLATORS: button label, connect to a WiFi network #: src/components/network/NetworkPage.jsx:102 @@ -616,29 +597,26 @@ msgid "Connect" msgstr "Conectar" #: src/components/network/NetworkPage.jsx:109 -#, fuzzy, c-format +#, c-format msgid "Conected to %s" -msgstr "Conectado (%s)" +msgstr "Conectado a (%s)" #: src/components/network/NetworkPage.jsx:114 -#, fuzzy msgid "No connected yet" -msgstr "Nenhum dispositivo selecionado ainda" +msgstr "Ainda não conectado" #: src/components/network/NetworkPage.jsx:115 -#, fuzzy msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." -msgstr "O sistema ainda não foi configurado para se conectar a uma rede WiFi." +msgstr "O sistema ainda não foi configurado para se conectar a uma rede Wi-Fi." #: src/components/network/NetworkPage.jsx:136 msgid "Wired" -msgstr "" +msgstr "Cabeada" #: src/components/network/NetworkPage.jsx:139 -#, fuzzy msgid "No wired connections found" -msgstr "Nenhuma conexão com fio encontrada." +msgstr "Nenhuma conexão com fio encontrada" #: src/components/network/NetworkPage.jsx:149 #: src/components/network/routes.js:59 @@ -708,19 +686,16 @@ msgid "Disconnected" msgstr "Desconectado" #: src/components/network/WifiNetworksListPage.jsx:121 -#, fuzzy msgid "Disconnect" -msgstr "Desconectado" +msgstr "Desconectar" #: src/components/network/WifiNetworksListPage.jsx:142 -#, fuzzy msgid "Connect to a hidden network" -msgstr "Conectar-se à rede oculta" +msgstr "Conecte-se a uma rede oculta" #: src/components/network/WifiNetworksListPage.jsx:153 -#, fuzzy msgid "configured" -msgstr "Não configurar" +msgstr "Configurado" #: src/components/network/WifiNetworksListPage.jsx:244 msgid "Connect to hidden network" @@ -758,43 +733,42 @@ msgid "Software" msgstr "Software" #: src/components/overview/OverviewPage.jsx:52 -#, fuzzy msgid "Ready for installation" -msgstr "Confirmar instalação" +msgstr "Pronto para instalação" #: src/components/overview/OverviewPage.jsx:102 -#, fuzzy msgid "Installation" -msgstr "Instalando" +msgstr "Instalação" #: src/components/overview/OverviewPage.jsx:103 msgid "Before installing, please check the following problems." -msgstr "" +msgstr "Antes de instalar, verifique os seguintes problemas." #: src/components/overview/OverviewPage.jsx:114 -#, fuzzy msgid "" "Take your time to check your configuration before starting the installation " "process." msgstr "" -"A instalação configurará partições para inicialização no disco de instalação." +"Reserve um tempo para verificar sua configuração antes de iniciar o processo " +"de instalação." #: src/components/overview/OverviewPage.jsx:123 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." msgstr "" +"Estas são as configurações de instalação mais relevantes. Sinta-se à vontade " +"para navegar pelas seções do menu para obter mais detalhes." #: src/components/overview/SoftwareSection.jsx:60 -#, fuzzy msgid "The installation will take" -msgstr "A instalação levará %s" +msgstr "A instalação levará" #. TRANSLATORS: %s will be replaced with the installation size, example: "5GiB". #: src/components/overview/SoftwareSection.jsx:67 -#, fuzzy, c-format +#, c-format msgid "The installation will take %s including:" -msgstr "A instalação levará %s" +msgstr "A instalação levará %s incluindo:" #: src/components/overview/StorageSection.jsx:53 msgid "" @@ -905,7 +879,7 @@ msgstr "" #: src/components/overview/routes.js:30 msgid "Overview" -msgstr "" +msgstr "Visão geral" #: src/components/product/ProductRegistrationPage.jsx:66 #, c-format @@ -925,9 +899,8 @@ msgid "Loading available products, please wait..." msgstr "Carregando produtos disponíveis, aguarde..." #: src/components/product/ProductSelectionProgress.jsx:53 -#, fuzzy msgid "Configuring the product, please wait ..." -msgstr "Carregando produtos disponíveis, aguarde..." +msgstr "Configurando o produto, aguarde..." #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 @@ -957,9 +930,8 @@ msgid "The following software patterns are selected for installation:" msgstr "Os seguintes padrões de software são selecionados para instalação:" #: src/components/software/SoftwarePage.jsx:165 -#, fuzzy msgid "Selected patterns" -msgstr "Selecionar fuso horário" +msgstr "Selecionar padrões" #: src/components/software/SoftwarePage.jsx:168 msgid "Change selection" @@ -977,18 +949,19 @@ msgstr "Seleção de software" #: src/components/software/SoftwarePatternsSelection.jsx:241 #: src/components/software/SoftwarePatternsSelection.jsx:242 msgid "Filter by pattern title or description" -msgstr "" +msgstr "Filtrar por título ou descrição do padrão" #. TRANSLATORS: %s will be replaced by the estimated installation size, #. example: "728.8 MiB" #: src/components/software/UsedSize.jsx:33 -#, fuzzy, c-format +#, c-format msgid "Installation will take %s." -msgstr "A instalação levará %s" +msgstr "A instalação levará %s." #: src/components/software/UsedSize.jsx:38 msgid "This space includes the base system and the selected software patterns." msgstr "" +"Este espaço inclui o sistema base e os padrões de software selecionados." #: src/components/storage/BootConfigField.jsx:43 msgid "Change boot options" @@ -1030,9 +1003,8 @@ msgstr "" "As partições para inicialização serão alocadas no disco de instalação (%s)." #: src/components/storage/BootSelection.jsx:154 -#, fuzzy msgid "Select booting partition" -msgstr "Selecione o que fazer com cada partição." +msgstr "Selecione a partição de inicialização" #: src/components/storage/BootSelection.jsx:170 #: src/components/storage/iscsi/NodeStartupOptions.js:27 @@ -1179,9 +1151,8 @@ msgid "Remove max channel filter" msgstr "Remover filtro de canal máximo" #: src/components/storage/DeviceSelection.jsx:101 -#, fuzzy msgid "Loading data, please wait a second..." -msgstr "Carregando produtos disponíveis, aguarde..." +msgstr "Carregando dados, aguarde um segundo..." #. TRANSLATORS: description for using plain partitions for installing the #. system, the text in the square brackets [] is displayed in bold, use only @@ -1208,24 +1179,20 @@ msgstr "" "sob demanda como novas partições nos dispositivos selecionados." #: src/components/storage/DeviceSelection.jsx:149 -#, fuzzy msgid "Select installation device" -msgstr "Dispositivo de instalação" +msgstr "Selecione o dispositivo de instalação" #: src/components/storage/DeviceSelection.jsx:155 -#, fuzzy msgid "Install new system on" -msgstr "Opções do instalador" +msgstr "Instalar novo sistema em" #: src/components/storage/DeviceSelection.jsx:158 -#, fuzzy msgid "An existing disk" -msgstr "Diminuir partições existentes" +msgstr "Um disco existente" #: src/components/storage/DeviceSelection.jsx:167 -#, fuzzy msgid "A new LVM Volume Group" -msgstr "novo grupo de volume LVM" +msgstr "Um novo grupo de volume LVM" #: src/components/storage/DeviceSelection.jsx:192 msgid "Device selector for target disk" @@ -1240,7 +1207,6 @@ msgid "Prepare more devices by configuring advanced" msgstr "Prepare mais dispositivos configurando o avançado" #: src/components/storage/DeviceSelection.jsx:229 -#, fuzzy msgid "storage techs" msgstr "tecnologias de armazenamento" @@ -1351,13 +1317,12 @@ msgid "Encryption" msgstr "Criptografia" #: src/components/storage/EncryptionField.jsx:39 -#, fuzzy msgid "" "Protection for the information stored at the device, including data, " "programs, and system files." msgstr "" -"A Criptografia Completa de Disco (FDE) permite proteger as informações " -"armazenadas no dispositivo, incluindo dados, programas e arquivos de sistema." +"Proteção para as informações armazenadas no dispositivo, incluindo dados, " +"programas e arquivos de sistema." #: src/components/storage/EncryptionField.jsx:42 msgid "disabled" @@ -1372,13 +1337,12 @@ msgid "using TPM unlocking" msgstr "usando desbloqueio TPM" #: src/components/storage/EncryptionField.jsx:58 -#, fuzzy msgid "Enable" -msgstr "ativado" +msgstr "Ativado" #: src/components/storage/EncryptionField.jsx:58 msgid "Modify" -msgstr "" +msgstr "Modificar" #: src/components/storage/EncryptionSettingsDialog.jsx:37 msgid "" @@ -1419,26 +1383,24 @@ msgstr "Dispositivo de instalação" #. TRANSLATORS: The storage "Installation device" field's description. #: src/components/storage/InstallationDeviceField.jsx:38 -#, fuzzy msgid "Main disk or LVM Volume Group for installation." -msgstr "Selecione o disco principal ou o grupo de volumes LVM para instalação." +msgstr "Disco principal ou o grupo de volumes LVM para instalação." #. TRANSLATORS: %s is the installation disk (eg. "/dev/sda, 80 GiB) #: src/components/storage/InstallationDeviceField.jsx:52 -#, fuzzy, c-format +#, c-format msgid "File systems created as new partitions at %s" -msgstr "Criar uma nova partição" +msgstr "Sistemas de arquivos criados como novas partições em %s" #: src/components/storage/InstallationDeviceField.jsx:55 -#, fuzzy msgid "File systems created at a new LVM volume group" -msgstr "Seletor de dispositivo para novo grupo de volume LVM" +msgstr "Sistemas de arquivos criados em um novo grupo de volumes LVM" #. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) #: src/components/storage/InstallationDeviceField.jsx:59 -#, fuzzy, c-format +#, c-format msgid "File systems created at a new LVM volume group on %s" -msgstr "novo grupo de volume LVM em %s" +msgstr "Sistemas de arquivos criados em um novo grupo de volumes LVM em %s" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:73 @@ -1665,18 +1627,16 @@ msgid "Partitions and file systems" msgstr "Partições e sistemas de arquivos" #: src/components/storage/PartitionsField.jsx:802 -#, fuzzy msgid "" "Structure of the new system, including any additional partition needed for " "booting" msgstr "" "Estrutura do novo sistema, incluindo qualquer partição adicional necessária " -"para inicialização," +"para inicialização" #: src/components/storage/PartitionsField.jsx:808 -#, fuzzy msgid "Show partitions and file-systems actions" -msgstr "Partições e sistemas de arquivos" +msgstr "Mostrar partições e ações de sistemas de arquivos" #. TRANSLATORS: show/hide toggle action, this is a clickable link #: src/components/storage/ProposalActionsDialog.jsx:62 @@ -1695,14 +1655,12 @@ msgstr[0] "Mostrar ação do subvolume %d" msgstr[1] "Mostrar %d ações de subvolume" #: src/components/storage/ProposalActionsSummary.jsx:57 -#, fuzzy msgid "Destructive actions are not allowed" -msgstr "Há %d ação destrutiva planejada" +msgstr "Ações destrutivas não são permitidas" #: src/components/storage/ProposalActionsSummary.jsx:59 -#, fuzzy msgid "Destructive actions are allowed" -msgstr "Há %d ação destrutiva planejada" +msgstr "Ações destrutivas são permitidas" #: src/components/storage/ProposalActionsSummary.jsx:66 #, c-format @@ -1713,48 +1671,43 @@ msgstr[1] "Há %d ações destrutivas planejadas" #: src/components/storage/ProposalActionsSummary.jsx:79 #: src/components/storage/ProposalActionsSummary.jsx:126 -#, fuzzy msgid "affecting" -msgstr "Afetando" +msgstr "afetando" #: src/components/storage/ProposalActionsSummary.jsx:107 -#, fuzzy msgid "Shrinking partitions is not allowed" -msgstr "Diminuir partições existentes" +msgstr "Não é permitido reduzir partições" #: src/components/storage/ProposalActionsSummary.jsx:111 -#, fuzzy msgid "Shrinking partitions is allowed" -msgstr "Diminuir partições existentes" +msgstr "Reduzir partições é permitido" #: src/components/storage/ProposalActionsSummary.jsx:113 -#, fuzzy msgid "Shrinking some partitions is allowed but not needed" -msgstr "diminuindo partições do dispositivo de instalação" +msgstr "Reduzir algumas partições é permitido, mas não é necessário" #: src/components/storage/ProposalActionsSummary.jsx:116 -#, fuzzy, c-format +#, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" -msgstr[0] "partição" -msgstr[1] "partição" +msgstr[0] "%d partição será reduzida" +msgstr[1] "%d partições serão reduzidas" #: src/components/storage/ProposalActionsSummary.jsx:151 -#, fuzzy msgid "Cannot accommodate the required file systems for installation" -msgstr "O layout do teclado não pode ser alterado na instalação remota" +msgstr "" +"Não é possível acomodar os sistemas de arquivos necessários para instalação" #: src/components/storage/ProposalActionsSummary.jsx:160 -#, fuzzy, c-format +#, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" -msgstr[0] "Verifique todas as ações planejadas" -msgstr[1] "Verifique todas as ações planejadas" +msgstr[0] "Confira a ação planejada" +msgstr[1] "Verifique as %d ações planejadas" #: src/components/storage/ProposalActionsSummary.jsx:179 -#, fuzzy msgid "Waiting for actions information..." -msgstr "Aguardando informações sobre configuração de armazenamento" +msgstr "Aguardando informações sobre ações..." #: src/components/storage/ProposalPage.jsx:314 msgid "Planned Actions" @@ -1766,15 +1719,15 @@ msgstr "Aguardando informações sobre configuração de armazenamento" #: src/components/storage/ProposalResultSection.jsx:70 msgid "Final layout" -msgstr "" +msgstr "Layout final" #: src/components/storage/ProposalResultSection.jsx:71 msgid "The systems will be configured as displayed below." -msgstr "" +msgstr "Os sistemas serão configurados conforme mostrado abaixo." #: src/components/storage/ProposalResultSection.jsx:78 msgid "Storage proposal not possible" -msgstr "" +msgstr "Proposta de armazenamento não é possível" #: src/components/storage/ProposalResultTable.jsx:74 msgid "New" @@ -1848,7 +1801,7 @@ msgstr "Ações para encontrar espaço" #: src/components/storage/SpacePolicySelection.jsx:170 msgid "Space policy" -msgstr "" +msgstr "Política espacial" #: src/components/storage/VolumeDialog.jsx:78 #, c-format @@ -2393,7 +2346,7 @@ msgstr "Destinos" #: src/components/storage/routes.js:36 msgid "Proposal" -msgstr "" +msgstr "Proposta" #: src/components/storage/utils.js:64 msgid "KiB" @@ -2428,9 +2381,8 @@ msgstr "" #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space deleting current content". Keep it short #: src/components/storage/utils.js:82 -#, fuzzy msgid "deleting current content" -msgstr "Excluir conteúdo atual" +msgstr "Excluindo o conteúdo atual" #: src/components/storage/utils.js:87 msgid "Shrink existing partitions" @@ -2445,9 +2397,8 @@ msgstr "" #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space shrinking partitions". Keep it short. #: src/components/storage/utils.js:92 -#, fuzzy msgid "shrinking partitions" -msgstr "Diminuir partições existentes" +msgstr "Diminuindo partições" #: src/components/storage/utils.js:97 msgid "Use available space" @@ -2477,9 +2428,8 @@ msgstr "Selecione o que fazer com cada partição." #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space with custom actions". Keep it short. #: src/components/storage/utils.js:112 -#, fuzzy msgid "with custom actions" -msgstr "executando um conjunto personalizado de ações" +msgstr "com ações personalizadas" #: src/components/users/FirstUser.jsx:35 msgid "No user defined yet." @@ -2525,23 +2475,20 @@ msgid "Use suggested username" msgstr "Use o nome de usuário sugerido" #: src/components/users/FirstUserForm.jsx:140 -#, fuzzy msgid "All fields are required" -msgstr "Um valor de tamanho é necessário" +msgstr "Todos os campos são necessários" #: src/components/users/FirstUserForm.jsx:147 msgid "Please, try again." -msgstr "" +msgstr "Por favor, tente de novo." #: src/components/users/FirstUserForm.jsx:197 -#, fuzzy msgid "Create user" -msgstr "Criar conta de usuário" +msgstr "Criar usuário" #: src/components/users/FirstUserForm.jsx:197 -#, fuzzy msgid "Edit user" -msgstr "Editar %s" +msgstr "Editar usuário" #: src/components/users/FirstUserForm.jsx:214 #: src/components/users/FirstUserForm.jsx:216 @@ -2650,7 +2597,7 @@ msgstr "Limpar" #: src/components/users/UsersPage.jsx:45 msgid "First user" -msgstr "" +msgstr "Primeiro usuário" #: src/components/users/UsersPage.jsx:52 msgid "Root authentication" diff --git a/web/po/zh_Hans.po b/web/po/zh_Hans.po index 666a1ba590..6be01e70cd 100644 --- a/web/po/zh_Hans.po +++ b/web/po/zh_Hans.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-24 12:46+0000\n" +"PO-Revision-Date: 2024-07-03 14:46+0000\n" "Last-Translator: Monstorix \n" "Language-Team: Chinese (Simplified) \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.6\n" +"X-Generator: Weblate 5.6.2\n" #: src/MainLayout.jsx:40 msgid "Agama" @@ -152,9 +152,8 @@ msgid "Reboot" msgstr "重启" #: src/components/core/InstallationProgress.jsx:30 -#, fuzzy msgid "Installing the system, please wait ..." -msgstr "正在安装 %s,请稍候……" +msgstr "正在安装系统,请稍候……" #: src/components/core/InstallerOptions.jsx:83 msgid "Show installer options" @@ -173,9 +172,8 @@ msgstr "语言" #: src/components/core/InstallerOptions.jsx:114 #: src/components/core/InstallerOptions.jsx:121 -#, fuzzy msgid "Keyboard layout" -msgstr "选择键盘布局" +msgstr "键盘布局" #: src/components/core/InstallerOptions.jsx:130 msgid "Cannot be changed in remote installation" @@ -294,17 +292,16 @@ msgid "Loading data..." msgstr "正在读取数据……" #: src/components/core/ProgressReport.jsx:50 -#, fuzzy msgid "Finished" -msgstr "完成" +msgstr "已完成" #: src/components/core/ProgressReport.jsx:59 msgid "In progress" -msgstr "" +msgstr "进行中" #: src/components/core/ProgressReport.jsx:70 msgid "Pending" -msgstr "" +msgstr "等待中" #: src/components/core/ProgressReport.jsx:134 msgid "Waiting for progress status..." @@ -568,10 +565,9 @@ msgstr "网关只能在“手动”模式下配置" #: src/components/network/NetworkPage.jsx:85 msgid "No Wi-Fi supported" -msgstr "" +msgstr "没有 Wi-Fi 支持" #: src/components/network/NetworkPage.jsx:86 -#, fuzzy msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." @@ -579,7 +575,7 @@ msgstr "系统不支持 WiFi 连接,可能由于硬件缺失或已被禁用。 #: src/components/network/NetworkPage.jsx:99 msgid "Wi-Fi" -msgstr "" +msgstr "Wi-Fi" #. TRANSLATORS: button label, connect to a WiFi network #: src/components/network/NetworkPage.jsx:102 @@ -589,29 +585,26 @@ msgid "Connect" msgstr "连接" #: src/components/network/NetworkPage.jsx:109 -#, fuzzy, c-format +#, c-format msgid "Conected to %s" -msgstr "已连接(%s)" +msgstr "已连接到 %s" #: src/components/network/NetworkPage.jsx:114 -#, fuzzy msgid "No connected yet" -msgstr "尚未选择" +msgstr "尚未连接" #: src/components/network/NetworkPage.jsx:115 -#, fuzzy msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." msgstr "系统尚未配置为连接到 WiFi 网络。" #: src/components/network/NetworkPage.jsx:136 msgid "Wired" -msgstr "" +msgstr "有线连接" #: src/components/network/NetworkPage.jsx:139 -#, fuzzy msgid "No wired connections found" -msgstr "未找到有线连接。" +msgstr "未找到有线连接" #: src/components/network/NetworkPage.jsx:149 #: src/components/network/routes.js:59 @@ -685,14 +678,12 @@ msgid "Disconnect" msgstr "断开连接" #: src/components/network/WifiNetworksListPage.jsx:142 -#, fuzzy msgid "Connect to a hidden network" msgstr "连接到隐藏网络" #: src/components/network/WifiNetworksListPage.jsx:153 -#, fuzzy msgid "configured" -msgstr "不要配置" +msgstr "已配置" #: src/components/network/WifiNetworksListPage.jsx:244 msgid "Connect to hidden network" @@ -797,25 +788,26 @@ msgstr "" "安装到 %s 上新建的逻辑卷管理器 (LVM) 卷组中,并在需要时缩小磁盘中现有的分区" #: src/components/overview/StorageSection.jsx:92 -#, fuzzy, c-format +#, c-format msgid "" "Install in a new Logical Volume Manager (LVM) volume group on %s without " "modifying existing partitions" -msgstr "在新建的 %1$s 上进行安装但不修改磁盘中已存在的分区" +msgstr "安装在 %s 上新建的逻辑卷管理 (LVM) 卷组中,但不修改磁盘中已存在的分区" #: src/components/overview/StorageSection.jsx:98 -#, fuzzy, c-format +#, c-format msgid "" "Install in a new Logical Volume Manager (LVM) volume group on %s deleting " "all its content" -msgstr "在新建的 %1$s 上进行安装并删除磁盘的现有内容" +msgstr "安装在 %s 上新建的逻辑卷管理 (LVM) 卷组中,并删除磁盘的现有内容" #: src/components/overview/StorageSection.jsx:104 -#, fuzzy, c-format +#, c-format msgid "" "Install in a new Logical Volume Manager (LVM) volume group on %s using a " "custom strategy to find the needed space" -msgstr "在新建的 %1$s 上进行安装,采用自定义策略寻找磁盘上的所需空间" +msgstr "安装在 %s 上新建的逻辑卷管理 (LVM) " +"卷组中,并采用自定义策略寻找磁盘上的所需空间" #: src/components/overview/StorageSection.jsx:175 #: src/components/storage/InstallationDeviceField.jsx:63 @@ -852,7 +844,7 @@ msgstr "使用设备 %s 进行安装,并采用自定义策略寻找所需空 #: src/components/overview/routes.js:30 msgid "Overview" -msgstr "" +msgstr "概览" #: src/components/product/ProductRegistrationPage.jsx:66 #, c-format @@ -872,9 +864,8 @@ msgid "Loading available products, please wait..." msgstr "正在载入可用产品,请稍候……" #: src/components/product/ProductSelectionProgress.jsx:53 -#, fuzzy msgid "Configuring the product, please wait ..." -msgstr "正在载入可用产品,请稍候……" +msgstr "正在配置产品,请稍候……" #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 @@ -897,68 +888,63 @@ msgstr "加密密码" #: src/components/software/SoftwarePage.jsx:85 msgid "No additional software was selected." -msgstr "" +msgstr "没有选择附加软件。" #: src/components/software/SoftwarePage.jsx:90 msgid "The following software patterns are selected for installation:" -msgstr "" +msgstr "下列软件合集已被选择以进行安装:" #: src/components/software/SoftwarePage.jsx:165 -#, fuzzy msgid "Selected patterns" -msgstr "选择时区" +msgstr "已选择合集" #: src/components/software/SoftwarePage.jsx:168 -#, fuzzy msgid "Change selection" -msgstr "更改时区" +msgstr "修改选择" #: src/components/software/SoftwarePatternsSelection.jsx:230 msgid "None of the patterns match the filter." -msgstr "" +msgstr "没有合集与筛选器匹配。" #: src/components/software/SoftwarePatternsSelection.jsx:238 -#, fuzzy msgid "Software selection" -msgstr "软件 %s" +msgstr "软件选择" #. TRANSLATORS: search field placeholder text #: src/components/software/SoftwarePatternsSelection.jsx:241 #: src/components/software/SoftwarePatternsSelection.jsx:242 msgid "Filter by pattern title or description" -msgstr "" +msgstr "按合集名称或描述筛选" #. TRANSLATORS: %s will be replaced by the estimated installation size, #. example: "728.8 MiB" #: src/components/software/UsedSize.jsx:33 -#, fuzzy, c-format +#, c-format msgid "Installation will take %s." -msgstr "安装将会占用 %s" +msgstr "安装将会占用 %s。" #: src/components/software/UsedSize.jsx:38 msgid "This space includes the base system and the selected software patterns." -msgstr "" +msgstr "此空间占用包含基础系统以及所选的软件合集。" #: src/components/storage/BootConfigField.jsx:43 -#, fuzzy msgid "Change boot options" -msgstr "更改时区" +msgstr "更改启动选项" #: src/components/storage/BootConfigField.jsx:87 msgid "Installation will not configure partitions for booting." -msgstr "" +msgstr "安装程序将不会配置用于启动的分区。" #: src/components/storage/BootConfigField.jsx:89 -#, fuzzy msgid "" "Installation will configure partitions for booting at the installation disk." -msgstr "缩小安装设备上的分区" +msgstr "安装程序将在所选安装磁盘上配置启动所需的分区。" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) #: src/components/storage/BootConfigField.jsx:92 #, c-format msgid "Installation will configure partitions for booting at %s." -msgstr "" +msgstr "安装程序将在 %s 上配置启动所需的分区。" #: src/components/storage/BootSelection.jsx:127 msgid "" @@ -977,9 +963,8 @@ msgid "Partitions to boot will be allocated at the installation disk (%s)." msgstr "引导分区将在安装磁盘(%s)上进行分配。" #: src/components/storage/BootSelection.jsx:154 -#, fuzzy msgid "Select booting partition" -msgstr "选择对每个分区执行的操作。" +msgstr "选择启动分区" #: src/components/storage/BootSelection.jsx:170 #: src/components/storage/iscsi/NodeStartupOptions.js:27 @@ -1062,7 +1047,7 @@ msgstr "类型" #. usually keep untranslated #: src/components/storage/DASDTable.jsx:70 msgid "DIAG" -msgstr "" +msgstr "DIAG" #: src/components/storage/DASDTable.jsx:71 msgid "Formatted" @@ -1123,50 +1108,42 @@ msgid "Remove max channel filter" msgstr "移除最大通道过滤器" #: src/components/storage/DeviceSelection.jsx:101 -#, fuzzy msgid "Loading data, please wait a second..." -msgstr "正在载入可用产品,请稍候……" +msgstr "正在载入数据,请稍候……" #. TRANSLATORS: description for using plain partitions for installing the #. system, the text in the square brackets [] is displayed in bold, use only #. one pair in the translation #: src/components/storage/DeviceSelection.jsx:136 -#, fuzzy msgid "" "The file systems will be allocated by default as [new partitions in the " "selected device]." -msgstr "默认情况下,文件系统将被分配为所选设备上的新分区。" +msgstr "默认情况下,文件系统将被分配为[所选设备上的新分区]。" #. TRANSLATORS: description for using logical volumes for installing the #. system, the text in the square brackets [] is displayed in bold, use only #. one pair in the translation #: src/components/storage/DeviceSelection.jsx:141 -#, fuzzy msgid "" "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." -msgstr "" -"默认情况下,文件系统将被分配为新建 LVM 卷组中的逻辑卷。相应的物理卷将" -"作为新分区按需在所选设备上被创建。" +msgstr "默认情况下,文件系统将被分配为[新建 LVM " +"卷组中的逻辑卷]。相应的物理卷将作为新分区按需创建在所选设备上。" #: src/components/storage/DeviceSelection.jsx:149 -#, fuzzy msgid "Select installation device" -msgstr "在安装设备上" +msgstr "选择安装设备" #: src/components/storage/DeviceSelection.jsx:155 -#, fuzzy msgid "Install new system on" -msgstr "安装程序选项" +msgstr "安装新系统到" #: src/components/storage/DeviceSelection.jsx:158 -#, fuzzy msgid "An existing disk" -msgstr "缩小现有分区" +msgstr "现存磁盘" #: src/components/storage/DeviceSelection.jsx:167 -#, fuzzy msgid "A new LVM Volume Group" msgstr "新建 LVM 卷组" @@ -1180,12 +1157,11 @@ msgstr "新建 LVM 卷组的设备选择器" #: src/components/storage/DeviceSelection.jsx:228 msgid "Prepare more devices by configuring advanced" -msgstr "" +msgstr "通过高级配置来准备更多设备" #: src/components/storage/DeviceSelection.jsx:229 -#, fuzzy msgid "storage techs" -msgstr "存储问题" +msgstr "存储技术" #. TRANSLATORS: multipath device type #: src/components/storage/DeviceSelectorTable.jsx:57 @@ -1290,66 +1266,60 @@ msgstr "iSCSI" #: src/components/storage/EncryptionField.jsx:38 #: src/components/storage/EncryptionSettingsDialog.jsx:36 -#, fuzzy msgid "Encryption" -msgstr "使用加密" +msgstr "加密" #: src/components/storage/EncryptionField.jsx:39 msgid "" "Protection for the information stored at the device, including data, " "programs, and system files." -msgstr "" +msgstr "保护存储在设备上的信息,包括数据、程序以及系统文件。" #: src/components/storage/EncryptionField.jsx:42 msgid "disabled" -msgstr "" +msgstr "已禁用" #: src/components/storage/EncryptionField.jsx:43 msgid "enabled" -msgstr "" +msgstr "已启用" #: src/components/storage/EncryptionField.jsx:44 msgid "using TPM unlocking" -msgstr "" +msgstr "使用 TPM 解锁" #: src/components/storage/EncryptionField.jsx:58 -#, fuzzy msgid "Enable" -msgstr "可缩小" +msgstr "启用" #: src/components/storage/EncryptionField.jsx:58 msgid "Modify" -msgstr "" +msgstr "修改" #: src/components/storage/EncryptionSettingsDialog.jsx:37 msgid "" "Full Disk Encryption (FDE) allows to protect the information stored at the " "device, including data, programs, and system files." -msgstr "" +msgstr "全盘加密 (FDE) 允许设备上存储的信息受到保护,包括数据、程序以及系统文件。" #. TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation #: src/components/storage/EncryptionSettingsDialog.jsx:40 -#, fuzzy msgid "" "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot" -msgstr "使用 TPM 在每次启动时自动解密" +msgstr "使用可信平台模块 (TPM) 在每次启动时自动解密" #. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing #. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. #: src/components/storage/EncryptionSettingsDialog.jsx:43 -#, fuzzy msgid "" "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." -msgstr "" -"若 TPM 可以验证系统的完整性,启" -"动和访问数据的时候将无需密码。 TPM 密封要求新系统在首次运行时直接引导。" +msgstr "若 TPM 可以验证系统的完整性,启动和访问数据的时候将无需使用密码。 TPM " +"密封要求新系统在首次启动时直接开始引导。" #: src/components/storage/EncryptionSettingsDialog.jsx:114 -#, fuzzy msgid "Encrypt the system" -msgstr "编辑文件系统" +msgstr "加密系统" #: src/components/storage/InstallationDeviceField.jsx:36 #: src/components/storage/VolumeLocationSelectorTable.jsx:58 @@ -1358,112 +1328,109 @@ msgstr "安装设备" #. TRANSLATORS: The storage "Installation device" field's description. #: src/components/storage/InstallationDeviceField.jsx:38 -#, fuzzy msgid "Main disk or LVM Volume Group for installation." -msgstr "" -"选择主磁盘或 LVM 卷组进行安装。" +msgstr "用于安装的主磁盘或 LVM 卷组。" #. TRANSLATORS: %s is the installation disk (eg. "/dev/sda, 80 GiB) #: src/components/storage/InstallationDeviceField.jsx:52 -#, fuzzy, c-format +#, c-format msgid "File systems created as new partitions at %s" -msgstr "默认情况下,文件系统将被分配为所选设备上的新分区。" +msgstr "已在 %s 上将文件系统创建为新的分区" #: src/components/storage/InstallationDeviceField.jsx:55 -#, fuzzy msgid "File systems created at a new LVM volume group" -msgstr "新建 LVM 卷组的设备选择器" +msgstr "已在新建 LVM 卷组上创建文件系统" #. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) #: src/components/storage/InstallationDeviceField.jsx:59 -#, fuzzy, c-format +#, c-format msgid "File systems created at a new LVM volume group on %s" -msgstr "在 %s 上新建的 LVM 卷组" +msgstr "已在 %s 上新建的 LVM 卷组中创建文件系统" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:73 -#, fuzzy, c-format +#, c-format msgid "at least %s" msgstr "至少 %s" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:97 -#, fuzzy, c-format +#, c-format msgid "Transactional Btrfs root volume (%s)" -msgstr "事务性根文件系统" +msgstr "事务性 Btrfs 根卷 (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:99 -#, fuzzy, c-format +#, c-format msgid "Transactional Btrfs root partition (%s)" -msgstr "事务性 Btrfs" +msgstr "事务性 Btrfs 根分区 (%s)" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:104 -#, fuzzy, c-format +#, c-format msgid "Btrfs root volume with snapshots (%s)" -msgstr "带快照的 Btrfs" +msgstr "带快照的 Btrfs 根卷 (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:106 -#, fuzzy, c-format +#, c-format msgid "Btrfs root partition with snapshots (%s)" -msgstr "带快照的 Btrfs" +msgstr "带快照的 Btrfs 根分区 (%s)" #. TRANSLATORS: This results in something like "Mount /dev/sda3 at /home (25 GiB)" since #. %1$s is replaced by the device name, %2$s by the mount point and %3$s by the size #: src/components/storage/PartitionsField.jsx:115 #, c-format msgid "Mount %1$s at %2$s (%3$s)" -msgstr "" +msgstr "挂载 %1$s 到 %2$s (%3$s)" #. TRANSLATORS: This results in something like "Swap at /dev/sda3 (2 GiB)" since #. %1$s is replaced by the device name, and %2$s by the size #: src/components/storage/PartitionsField.jsx:121 #, c-format msgid "Swap at %1$s (%2$s)" -msgstr "" +msgstr "位于 %1$s 上的 Swap (%2$s)" #. TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" #: src/components/storage/PartitionsField.jsx:125 #, c-format msgid "Swap volume (%s)" -msgstr "" +msgstr "Swap 卷 (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "8 GiB" #: src/components/storage/PartitionsField.jsx:127 -#, fuzzy, c-format +#, c-format msgid "Swap partition (%s)" -msgstr "位于 %s 的分区" +msgstr "Swap 分区 (%s)" #. TRANSLATORS: This results in something like "Btrfs root at /dev/sda3 (20 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size #: src/components/storage/PartitionsField.jsx:136 #, c-format msgid "%1$s root at %2$s (%3$s)" -msgstr "" +msgstr "位于 %2$s 上的 %1$s 根文件系统 (%3$s)" #. TRANSLATORS: "/" is in an LVM logical volume. #. Results in something like "Btrfs root volume (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description #: src/components/storage/PartitionsField.jsx:142 -#, fuzzy, c-format +#, c-format msgid "%1$s root volume (%2$s)" -msgstr "有其他卷存在(%s)" +msgstr "%1$s 根卷 (%2$s)" #. TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description #: src/components/storage/PartitionsField.jsx:145 -#, fuzzy, c-format +#, c-format msgid "%1$s root partition (%2$s)" -msgstr "%s (包含 %d 个分区)" +msgstr "%1$s 根分区 (%2$s)" #. TRANSLATORS: This results in something like "Ext4 /home at /dev/sda3 (20 GiB)" since #. %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size #: src/components/storage/PartitionsField.jsx:151 #, c-format msgid "%1$s %2$s at %3$s (%4$s)" -msgstr "" +msgstr "位于 %3$s 上的 %1$s %2$s (%4$s)" #. TRANSLATORS: The filesystem is in an LVM logical volume. #. Results in something like "Ext4 /home volume (at least 10 GiB)" since @@ -1471,30 +1438,28 @@ msgstr "" #: src/components/storage/PartitionsField.jsx:157 #, c-format msgid "%1$s %2$s volume (%3$s)" -msgstr "" +msgstr "%1$s %2$s 卷 (%3$s)" #. TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description #: src/components/storage/PartitionsField.jsx:160 -#, fuzzy, c-format +#, c-format msgid "%1$s %2$s partition (%3$s)" -msgstr "%s (包含 %d 个分区)" +msgstr "%1$s %2$s 分区 (%3$s)" #: src/components/storage/PartitionsField.jsx:172 -#, fuzzy msgid "Do not configure partitions for booting" -msgstr "引导分区" +msgstr "不要配置用于启动的分区" #: src/components/storage/PartitionsField.jsx:175 -#, fuzzy msgid "Boot partitions at installation disk" -msgstr "安装磁盘上的分区" +msgstr "位于安装磁盘上的启动分区" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) #: src/components/storage/PartitionsField.jsx:178 -#, fuzzy, c-format +#, c-format msgid "Boot partitions at %s" -msgstr "位于 %s 的分区" +msgstr "位于 %s 的启动分区" #. TRANSLATORS: header for a list of items referring to size limits for file systems #: src/components/storage/PartitionsField.jsx:200 @@ -1559,14 +1524,12 @@ msgid "Partition at installation disk" msgstr "安装磁盘上的分区" #: src/components/storage/PartitionsField.jsx:321 -#, fuzzy msgid "Reset location" -msgstr "注册" +msgstr "重设位置" #: src/components/storage/PartitionsField.jsx:322 -#, fuzzy msgid "Change location" -msgstr "更改时区" +msgstr "更改位置" #: src/components/storage/PartitionsField.jsx:323 #: src/components/storage/SpaceActionsTable.jsx:78 @@ -1598,27 +1561,25 @@ msgstr "添加文件系统" #: src/components/storage/PartitionsField.jsx:596 msgid "Other" -msgstr "" +msgstr "其他" #: src/components/storage/PartitionsField.jsx:731 msgid "Reset to defaults" msgstr "重设为默认" #: src/components/storage/PartitionsField.jsx:801 -#, fuzzy msgid "Partitions and file systems" -msgstr "文件系统的准确大小。" +msgstr "分区与文件系统" #: src/components/storage/PartitionsField.jsx:802 msgid "" "Structure of the new system, including any additional partition needed for " "booting" -msgstr "" +msgstr "新文件系统的结构,包含启动所需的任何附加分区" #: src/components/storage/PartitionsField.jsx:808 -#, fuzzy msgid "Show partitions and file-systems actions" -msgstr "文件系统的准确大小。" +msgstr "显示分区与文件系统操作" #. TRANSLATORS: show/hide toggle action, this is a clickable link #: src/components/storage/ProposalActionsDialog.jsx:62 @@ -1635,14 +1596,12 @@ msgid_plural "Show %d subvolume actions" msgstr[0] "显示 %d 个子卷操作" #: src/components/storage/ProposalActionsSummary.jsx:57 -#, fuzzy msgid "Destructive actions are not allowed" -msgstr "已计划执行 %d 个具有破坏性的操作" +msgstr "不允许执行具有破坏性的操作" #: src/components/storage/ProposalActionsSummary.jsx:59 -#, fuzzy msgid "Destructive actions are allowed" -msgstr "已计划执行 %d 个具有破坏性的操作" +msgstr "允许执行具有破坏性的操作" #: src/components/storage/ProposalActionsSummary.jsx:66 #, c-format @@ -1652,67 +1611,60 @@ msgstr[0] "已计划执行 %d 个具有破坏性的操作" #: src/components/storage/ProposalActionsSummary.jsx:79 #: src/components/storage/ProposalActionsSummary.jsx:126 -#, fuzzy msgid "affecting" msgstr "影响到" #: src/components/storage/ProposalActionsSummary.jsx:107 -#, fuzzy msgid "Shrinking partitions is not allowed" -msgstr "缩小现有分区" +msgstr "不允许缩小分区" #: src/components/storage/ProposalActionsSummary.jsx:111 -#, fuzzy msgid "Shrinking partitions is allowed" -msgstr "缩小现有分区" +msgstr "允许缩小分区" #: src/components/storage/ProposalActionsSummary.jsx:113 -#, fuzzy msgid "Shrinking some partitions is allowed but not needed" -msgstr "缩小安装设备上的分区" +msgstr "允许缩小分区,但并非必需" #: src/components/storage/ProposalActionsSummary.jsx:116 -#, fuzzy, c-format +#, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" -msgstr[0] "%s 分区表" +msgstr[0] "%d 个分区将被缩小" #: src/components/storage/ProposalActionsSummary.jsx:151 -#, fuzzy msgid "Cannot accommodate the required file systems for installation" -msgstr "无法在远程安装中更改" +msgstr "无法容纳安装所需的文件系统" #: src/components/storage/ProposalActionsSummary.jsx:160 -#, fuzzy, c-format +#, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" -msgstr[0] "检查所有已计划的操作" +msgstr[0] "检查 %d 个已计划的操作" #: src/components/storage/ProposalActionsSummary.jsx:179 -#, fuzzy msgid "Waiting for actions information..." -msgstr "正在等待引导配置信息" +msgstr "正在等待操作信息……" #: src/components/storage/ProposalPage.jsx:314 msgid "Planned Actions" msgstr "计划执行的操作" #: src/components/storage/ProposalResultSection.jsx:42 -#, fuzzy msgid "Waiting for information about storage configuration" -msgstr "正在等待引导配置信息" +msgstr "正在等待存储配置信息" #: src/components/storage/ProposalResultSection.jsx:70 msgid "Final layout" -msgstr "" +msgstr "最终布局" #: src/components/storage/ProposalResultSection.jsx:71 msgid "The systems will be configured as displayed below." -msgstr "" +msgstr "系统将按下列选项进行配置。" #: src/components/storage/ProposalResultSection.jsx:78 msgid "Storage proposal not possible" -msgstr "" +msgstr "存储建议不可行" #: src/components/storage/ProposalResultTable.jsx:74 msgid "New" @@ -1743,18 +1695,14 @@ msgstr "" "更新。" #: src/components/storage/SnapshotsField.jsx:36 -#, fuzzy msgid "Use Btrfs snapshots for the root file system" -msgstr "文件系统的准确大小。" +msgstr "为根文件系统使用 Btrfs 快照" #: src/components/storage/SnapshotsField.jsx:37 -#, fuzzy msgid "" "Allows to boot to a previous version of the system after configuration " "changes or software upgrades." -msgstr "" -"使用 Btrfs 作为根文件系统,可以允许配置被改变或软件更新后回退启动到先前版本的" -"系统。" +msgstr "允许配置被改变或软件更新后,回退启动到先前版本的系统。" #. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) #: src/components/storage/SpaceActionsTable.jsx:75 @@ -1772,7 +1720,7 @@ msgstr "不要修改" #: src/components/storage/SpaceActionsTable.jsx:111 msgid "The content may be deleted" -msgstr "" +msgstr "内容可能会被删除" #: src/components/storage/SpaceActionsTable.jsx:144 msgid "Shrinkable" @@ -1788,17 +1736,17 @@ msgstr "查找空间的操作" #: src/components/storage/SpacePolicySelection.jsx:170 msgid "Space policy" -msgstr "" +msgstr "存储策略" #: src/components/storage/VolumeDialog.jsx:78 -#, fuzzy, c-format +#, c-format msgid "Add %s file system" -msgstr "添加文件系统" +msgstr "添加 %s 文件系统" #: src/components/storage/VolumeDialog.jsx:79 -#, fuzzy, c-format +#, c-format msgid "Edit %s file system" -msgstr "编辑文件系统" +msgstr "编辑 %s 文件系统" #: src/components/storage/VolumeDialog.jsx:81 msgid "Edit file system" @@ -1806,38 +1754,34 @@ msgstr "编辑文件系统" #. TRANSLATORS: Warning when editing a file system. #: src/components/storage/VolumeDialog.jsx:96 -#, fuzzy msgid "The type and size of the file system cannot be edited." -msgstr "在 %s 上存在文件系统" +msgstr "文件系统的类型与尺寸无法编辑。" #. TRANSLATORS: Description of a warning. The first %s is replaced by a device name (e.g., #. /dev/vda) and the second %s is replaced by a mount path (e.g., /home). #: src/components/storage/VolumeDialog.jsx:99 #, c-format msgid "The current file system on %s is selected to be mounted at %s." -msgstr "" +msgstr "当前位于 %s 上的文件系统已经被选为挂载到 %s。" #. TRANSLATORS: Warning when editing a file system. #: src/components/storage/VolumeDialog.jsx:105 -#, fuzzy msgid "The size of the file system cannot be edited" -msgstr "在 %s 上存在文件系统" +msgstr "文件系统的尺寸无法编辑" #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). #: src/components/storage/VolumeDialog.jsx:107 -#, fuzzy, c-format +#, c-format msgid "The file system is allocated at the device %s." -msgstr "默认情况下,文件系统将被分配为所选设备上的新分区。" +msgstr "文件系统被分配在设备 %s 上。" #: src/components/storage/VolumeDialog.jsx:152 -#, fuzzy msgid "A mount point is required" -msgstr "需要指定最小尺寸" +msgstr "需要挂载点" #: src/components/storage/VolumeDialog.jsx:179 -#, fuzzy msgid "The mount point is invalid" -msgstr "挂载点列表" +msgstr "挂载点无效" #: src/components/storage/VolumeDialog.jsx:207 msgid "A size value is required" @@ -1852,24 +1796,22 @@ msgid "Maximum must be greater than minimum" msgstr "最大值必须高于最小值" #: src/components/storage/VolumeDialog.jsx:309 -#, fuzzy, c-format +#, c-format msgid "There is already a file system for %s." -msgstr "在 %s 上存在文件系统" +msgstr "在 %s 上已有文件系统。" #: src/components/storage/VolumeDialog.jsx:311 -#, fuzzy msgid "Do you want to edit it?" -msgstr "您想要取消注册 %s 吗?" +msgstr "您是否想要编辑它?" #: src/components/storage/VolumeDialog.jsx:356 -#, fuzzy, c-format +#, c-format msgid "There is a predefined file system for %s." -msgstr "在 %s 上存在文件系统" +msgstr "已存在为 %s 预定义的文件系统。" #: src/components/storage/VolumeDialog.jsx:358 -#, fuzzy msgid "Do you want to add it?" -msgstr "您想要取消注册 %s 吗?" +msgstr "您是否想要添加它?" #. TRANSLATORS: info about possible file system types. #: src/components/storage/VolumeFields.jsx:217 @@ -1996,75 +1938,70 @@ msgstr "按范围计算" msgid "" "The file systems are allocated at the installation device by default. " "Indicate a custom location to create the file system at a specific device." -msgstr "" +msgstr "默认情况下,文件系统将被分配到安装设备。选择一个自定义位置以在特定设备上创建" +"文件系统。" #. TRANSLATORS: Title of the dialog for changing the location of a file system. %s is replaced #. by a mount path (e.g., /home). #: src/components/storage/VolumeLocationDialog.jsx:135 -#, fuzzy, c-format +#, c-format msgid "Location for %s file system" -msgstr "文件系统的准确大小。" +msgstr "%s 文件系统的位置" #: src/components/storage/VolumeLocationDialog.jsx:145 msgid "Select in which device to allocate the file system" -msgstr "" +msgstr "选择用于分配文件系统的设备" #: src/components/storage/VolumeLocationDialog.jsx:148 -#, fuzzy msgid "Select a location" -msgstr "选择一个磁盘" +msgstr "选择位置" #: src/components/storage/VolumeLocationDialog.jsx:160 -#, fuzzy msgid "Select how to allocate the file system" -msgstr "文件系统的准确大小。" +msgstr "选择如何分配文件系统" #: src/components/storage/VolumeLocationDialog.jsx:165 msgid "Create a new partition" -msgstr "" +msgstr "创建新分区" #: src/components/storage/VolumeLocationDialog.jsx:166 -#, fuzzy msgid "" "The file system will be allocated as a new partition at the selected disk." -msgstr "默认情况下,文件系统将被分配为所选设备上的新分区。" +msgstr "文件系统将被分配为所选 磁盘上的新分区。" #: src/components/storage/VolumeLocationDialog.jsx:175 -#, fuzzy msgid "Create a dedicated LVM volume group" -msgstr "创建 LVM 卷组" +msgstr "创建专用 LVM 卷组" #: src/components/storage/VolumeLocationDialog.jsx:176 msgid "" "A new volume group will be allocated in the selected disk and the file " "system will be created as a logical volume." -msgstr "" +msgstr "新的 LVM 卷组将被分配到所选的磁盘上,文件系统将创建为逻辑卷。" #: src/components/storage/VolumeLocationDialog.jsx:185 -#, fuzzy msgid "Format the device" -msgstr "正在格式化 DASD 设备" +msgstr "格式化设备" #. TRANSLATORS: %s is replaced by a file system type (e.g., Ext4). #: src/components/storage/VolumeLocationDialog.jsx:188 #, c-format msgid "The selected device will be formatted as %s file system." -msgstr "" +msgstr "选定的设备将被格式化为 %s 文件系统。" #: src/components/storage/VolumeLocationDialog.jsx:198 -#, fuzzy msgid "Mount the file system" -msgstr "编辑文件系统" +msgstr "挂载文件系统" #: src/components/storage/VolumeLocationDialog.jsx:199 msgid "" "The current file system on the selected device will be mounted without " "formatting the device." -msgstr "" +msgstr "选定磁盘上的当前文件系统将被挂载,但不会被格式化。" #: src/components/storage/VolumeLocationSelectorTable.jsx:102 msgid "Usage" -msgstr "" +msgstr "用量" #: src/components/storage/ZFCPDiskForm.jsx:109 msgid "The zFCP disk was not activated." @@ -2327,7 +2264,7 @@ msgstr "目标" #: src/components/storage/routes.js:36 msgid "Proposal" -msgstr "" +msgstr "建议" #: src/components/storage/utils.js:64 msgid "KiB" @@ -2360,9 +2297,8 @@ msgstr "所有分区将被移除,磁盘上的数据将丢失。" #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space deleting current content". Keep it short #: src/components/storage/utils.js:82 -#, fuzzy msgid "deleting current content" -msgstr "删除当前内容" +msgstr "通过删除当前内容" #: src/components/storage/utils.js:87 msgid "Shrink existing partitions" @@ -2375,9 +2311,8 @@ msgstr "数据将被保留,但当前分区的大小将会按需调整。" #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space shrinking partitions". Keep it short. #: src/components/storage/utils.js:92 -#, fuzzy msgid "shrinking partitions" -msgstr "缩小现有分区" +msgstr "通过缩小分区" #: src/components/storage/utils.js:97 msgid "Use available space" @@ -2405,9 +2340,8 @@ msgstr "选择对每个分区执行的操作。" #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space with custom actions". Keep it short. #: src/components/storage/utils.js:112 -#, fuzzy msgid "with custom actions" -msgstr "执行一组自定义操作" +msgstr "通过自定义操作" #: src/components/users/FirstUser.jsx:35 msgid "No user defined yet." @@ -2451,23 +2385,20 @@ msgid "Use suggested username" msgstr "使用建议的用户名" #: src/components/users/FirstUserForm.jsx:140 -#, fuzzy msgid "All fields are required" -msgstr "必须输入尺寸值" +msgstr "需要填写全部字段" #: src/components/users/FirstUserForm.jsx:147 msgid "Please, try again." -msgstr "" +msgstr "请重试。" #: src/components/users/FirstUserForm.jsx:197 -#, fuzzy msgid "Create user" -msgstr "创建用户账户" +msgstr "创建用户" #: src/components/users/FirstUserForm.jsx:197 -#, fuzzy msgid "Edit user" -msgstr "编辑 %s" +msgstr "编辑用户" #: src/components/users/FirstUserForm.jsx:214 #: src/components/users/FirstUserForm.jsx:216 @@ -2574,7 +2505,7 @@ msgstr "清除" #: src/components/users/UsersPage.jsx:45 msgid "First user" -msgstr "" +msgstr "首个用户" #: src/components/users/UsersPage.jsx:52 msgid "Root authentication" From 97886ff7ba3302dde0578eb6cca423fbe1ca1497 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 7 Jul 2024 02:51:19 +0000 Subject: [PATCH 121/430] Update service PO files Agama-weblate commit: 2531ecb584319cdf2c0eedd5c1e7b163be2f9670 --- service/po/cs.po | 12 ++++++------ service/po/de.po | 22 ++++++++++------------ service/po/es.po | 11 ++++++----- service/po/ja.po | 12 ++++++------ service/po/zh_Hans.po | 26 ++++++++++++-------------- 5 files changed, 40 insertions(+), 43 deletions(-) diff --git a/service/po/cs.po b/service/po/cs.po index dcde59be30..8e53be0d64 100644 --- a/service/po/cs.po +++ b/service/po/cs.po @@ -8,25 +8,25 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-24 17:46+0000\n" +"PO-Revision-Date: 2024-07-05 16:46+0000\n" "Last-Translator: Aleš Kastner \n" -"Language-Team: Czech \n" +"Language-Team: Czech \n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 5.6\n" +"X-Generator: Weblate 5.6.2\n" #. Runs the startup phase #: service/lib/agama/manager.rb:88 msgid "Load software translations" -msgstr "" +msgstr "Načíst překlady softwaru" #: service/lib/agama/manager.rb:89 msgid "Load storage translations" -msgstr "" +msgstr "Načíst překlady paměti" #. Runs the config phase #: service/lib/agama/manager.rb:104 diff --git a/service/po/de.po b/service/po/de.po index 071ff3bb42..fcadf619eb 100644 --- a/service/po/de.po +++ b/service/po/de.po @@ -8,21 +8,21 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-22 21:46+0000\n" +"PO-Revision-Date: 2024-07-06 01:46+0000\n" "Last-Translator: Ettore Atalan \n" -"Language-Team: German \n" +"Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6\n" +"X-Generator: Weblate 5.6.2\n" #. Runs the startup phase #: service/lib/agama/manager.rb:88 msgid "Load software translations" -msgstr "" +msgstr "Softwareübersetzungen laden" #: service/lib/agama/manager.rb:89 msgid "Load storage translations" @@ -31,27 +31,25 @@ msgstr "" #. Runs the config phase #: service/lib/agama/manager.rb:104 msgid "Analyze disks" -msgstr "" +msgstr "Festplatten analysieren" #: service/lib/agama/manager.rb:104 -#, fuzzy msgid "Configure software" -msgstr "Software wird untersucht" +msgstr "Software konfigurieren" #. Runs the install phase #. rubocop:disable Metrics/AbcSize #: service/lib/agama/manager.rb:124 msgid "Prepare disks" -msgstr "" +msgstr "Festplatten vorbereiten" #: service/lib/agama/manager.rb:125 -#, fuzzy msgid "Install software" -msgstr "Software wird installiert" +msgstr "Software installieren" #: service/lib/agama/manager.rb:126 msgid "Configure the system" -msgstr "" +msgstr "System konfigurieren" #. Callback to handle unsigned files #. diff --git a/service/po/es.po b/service/po/es.po index 55ee5f4909..14d4b5fb7b 100644 --- a/service/po/es.po +++ b/service/po/es.po @@ -8,16 +8,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-05-12 21:43+0000\n" -"Last-Translator: Alejandro Jiménez \n" -"Language-Team: Spanish \n" +"PO-Revision-Date: 2024-07-02 11:46+0000\n" +"Last-Translator: Victor hck \n" +"Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.9.1\n" +"X-Generator: Weblate 5.6.2\n" #. Runs the startup phase #: service/lib/agama/manager.rb:88 @@ -172,6 +172,7 @@ msgstr "" #: service/lib/agama/storage/proposal.rb:192 msgid "Cannot accommodate the required file systems for installation" msgstr "" +"No se pueden acomodar los sistemas de archivos necesarios para la instalación" #. Issue to communicate a generic Y2Storage error. #. diff --git a/service/po/ja.po b/service/po/ja.po index d5c6f547fe..18dbb56889 100644 --- a/service/po/ja.po +++ b/service/po/ja.po @@ -8,25 +8,25 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-24 02:23+0000\n" +"PO-Revision-Date: 2024-07-04 00:46+0000\n" "Last-Translator: Yasuhiko Kamata \n" -"Language-Team: Japanese \n" +"Language-Team: Japanese \n" "Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 5.6\n" +"X-Generator: Weblate 5.6.2\n" #. Runs the startup phase #: service/lib/agama/manager.rb:88 msgid "Load software translations" -msgstr "" +msgstr "ソフトウエアの翻訳の読み込み" #: service/lib/agama/manager.rb:89 msgid "Load storage translations" -msgstr "" +msgstr "ストレージの翻訳の読み込み" #. Runs the config phase #: service/lib/agama/manager.rb:104 diff --git a/service/po/zh_Hans.po b/service/po/zh_Hans.po index e0e919389a..7acef46847 100644 --- a/service/po/zh_Hans.po +++ b/service/po/zh_Hans.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-04-10 09:43+0000\n" +"PO-Revision-Date: 2024-07-03 14:46+0000\n" "Last-Translator: Monstorix \n" "Language-Team: Chinese (Simplified) \n" @@ -17,41 +17,39 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.9.1\n" +"X-Generator: Weblate 5.6.2\n" #. Runs the startup phase #: service/lib/agama/manager.rb:88 msgid "Load software translations" -msgstr "" +msgstr "载入软件翻译" #: service/lib/agama/manager.rb:89 msgid "Load storage translations" -msgstr "" +msgstr "载入存储翻译" #. Runs the config phase #: service/lib/agama/manager.rb:104 msgid "Analyze disks" -msgstr "" +msgstr "分析磁盘" #: service/lib/agama/manager.rb:104 -#, fuzzy msgid "Configure software" -msgstr "正在探测软件" +msgstr "配置软件" #. Runs the install phase #. rubocop:disable Metrics/AbcSize #: service/lib/agama/manager.rb:124 msgid "Prepare disks" -msgstr "" +msgstr "准备磁盘" #: service/lib/agama/manager.rb:125 -#, fuzzy msgid "Install software" -msgstr "正在安装软件" +msgstr "安装软件" #: service/lib/agama/manager.rb:126 msgid "Configure the system" -msgstr "" +msgstr "配置系统" #. Callback to handle unsigned files #. @@ -172,14 +170,14 @@ msgstr "正在写入引导加载程序 sysconfig" #. @return [Issue] #: service/lib/agama/storage/proposal.rb:192 msgid "Cannot accommodate the required file systems for installation" -msgstr "无法调整进行安装所需的文件系统" +msgstr "无法容纳安装所需的文件系统" #. Issue to communicate a generic Y2Storage error. #. #. @return [Issue] #: service/lib/agama/storage/proposal.rb:203 msgid "A problem ocurred while calculating the storage setup" -msgstr "" +msgstr "计算存储设置时发生问题" #. Returns an issue if there is no target device. #. @@ -202,7 +200,7 @@ msgstr[0] "未在系统中找到下列已选择的设备: %{devices}" #: service/lib/agama/users.rb:152 msgid "" "Defining a user, setting the root password or a SSH public key is required" -msgstr "" +msgstr "必须定义一个用户、设置 root 密码或者配置 SSH 公钥" #~ msgid "Probing Storage" #~ msgstr "正在探测存储" From e9d8d4a92ca5d8f55e5b68bb871527405f898664 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 7 Jul 2024 02:53:06 +0000 Subject: [PATCH 122/430] Update translations in the product files Agama-weblate commit: 2531ecb584319cdf2c0eedd5c1e7b163be2f9670 --- products.d/leap_160.yaml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/products.d/leap_160.yaml b/products.d/leap_160.yaml index 3d47d143b8..293b7cd1ba 100644 --- a/products.d/leap_160.yaml +++ b/products.d/leap_160.yaml @@ -5,11 +5,25 @@ name: Leap 16.0 Alpha # at the at translations/description key below to avoid using obsolete # translations!! # ------------------------------------------------------------------------------ -description: 'Leap 16.0 is the latest version of a community distribution based on the latest SUSE Linux Enterprise Server.' +description: 'Leap 16.0 is the latest version of a community distribution based + on the latest SUSE Linux Enterprise Server.' # Do not manually change any translations! See README.md for more details. translations: description: - + ca: El Leap 16.0 és la darrera versió d'una distribució comunitària basada en + l'últim SUSE Linux Enterprise Server. + cs: Leap 16.0 je nejnovější verze komunitní distribuce založené na nejnovějším + SUSE Linux Enterprise Serveru. + ja: Leap 16.0 は最新のSUSE Linux Enterprise Server をベースにしたコミュニティディストリビューションの最新版です。 + nb_NO: Leap 16.0 er den nyeste versjonen av den fellesskapte distribusjon basert + på den nyeste SUSE Linux Enterprise Server. + pt_BR: Leap 16.0 é a versão mais recente de uma distribuição comunitária baseada + no mais recente SUSE Linux Enterprise Server. + ru: Leap 16.0 - это последняя версия дистрибутива от сообщества, основанного на + последней версии SUSE Linux Enterprise Server. + sv: Leap 16.0 är den senaste versionen av en distribution skapad av gemenskapen + som baseras på den senaste SUSE Linux Enterprise Server. + zh_Hans: Leap 16.0 是基于 SUSE Linux Enterprise Server 构建的社区发行版的最新版本。 software: installation_repositories: - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/standard @@ -20,7 +34,7 @@ software: archs: x86_64 - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/product/repo/Leap-Packages-16.0-aarch64 archs: aarch64 - + mandatory_patterns: - enhanced_base # only pattern that is shared among all roles on Leap optional_patterns: null # no optional pattern shared From c9ee944f6423438af71ebc018e1f8b314fa4f3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 8 Jul 2024 10:33:59 +0100 Subject: [PATCH 123/430] fix(web): drop repeated invalidation --- web/src/queries/l10n.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js index 59af07a1f4..75d193251b 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.js @@ -88,8 +88,6 @@ const keymapsQuery = () => ({ * It does not require to call `useMutation`. */ const useConfigMutation = () => { - const queryClient = useQueryClient(); - const query = { mutationFn: (newConfig) => fetch("/api/l10n/config", { @@ -98,10 +96,7 @@ const useConfigMutation = () => { headers: { "Content-Type": "application/json", }, - }), - onSuccess: () => - queryClient.invalidateQueries({ queryKey: ["l10n", "config"] }) - , + }) }; return useMutation(query); }; From b4e169aba6c2ea9884b1699abb1e9e47ff37831a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 8 Jul 2024 11:59:12 +0100 Subject: [PATCH 124/430] doc(web): update the changes file --- web/package/agama-web-ui.changes | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index ee19817591..b8caf21bba 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Mon Jul 8 10:56:14 UTC 2024 - Imobach Gonzalez Sosa + +- Introduce TanStack Query to handle for data fetching and state + management (gh#openSUSE/agama#1439). +- Replace the l10n context with a solution based on TanStack + Query. + ------------------------------------------------------------------- Wed Jul 3 07:17:55 UTC 2024 - Imobach Gonzalez Sosa From cbbb727a012bb80fce723ad9e75b0b0f175692a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 8 Jul 2024 13:18:04 +0100 Subject: [PATCH 125/430] doc(fix): fix changes entry --- web/package/agama-web-ui.changes | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index b8caf21bba..9dda2dcb85 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,8 +1,8 @@ ------------------------------------------------------------------- Mon Jul 8 10:56:14 UTC 2024 - Imobach Gonzalez Sosa -- Introduce TanStack Query to handle for data fetching and state - management (gh#openSUSE/agama#1439). +- Introduce TanStack Query for data fetching and state management + (gh#openSUSE/agama#1439). - Replace the l10n context with a solution based on TanStack Query. From 8838564fdf6475419313b1728ab9400b1e96c718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 8 Jul 2024 13:23:08 +0100 Subject: [PATCH 126/430] fix(service): fix manager tests --- service/test/agama/software/manager_test.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb index 0b24c2b8a5..8643608c20 100644 --- a/service/test/agama/software/manager_test.rb +++ b/service/test/agama/software/manager_test.rb @@ -253,7 +253,8 @@ expect(products).to all(be_a(Agama::Software::Product)) expect(products).to contain_exactly( an_object_having_attributes(id: "Tumbleweed"), - an_object_having_attributes(id: "MicroOS") + an_object_having_attributes(id: "MicroOS"), + an_object_having_attributes(id: "Leap_16.0") ) end end @@ -335,7 +336,7 @@ expect(proposal).to receive(:set_resolvables) .with("agama", :pattern, [], { optional: true }) expect(proposal).to receive(:set_resolvables) - .with("agama", :package, ["NetworkManager", "openSUSE-repos"]) + .with("agama", :package, ["NetworkManager", "openSUSE-repos-Tumbleweed"]) expect(proposal).to receive(:set_resolvables) .with("agama", :package, [], { optional: true }) subject.propose From 52cc3a60c0b25001fbc8be3ff4404051f074ce15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Jul 2024 14:05:10 +0100 Subject: [PATCH 127/430] storage: report more accurate info about shrinking --- .../dbus/storage/interfaces/device/block.rb | 17 +- service/lib/agama/storage/device_shrinking.rb | 187 ++++++++++++++++++ .../interfaces/device/block_examples.rb | 14 +- .../agama/storage/device_shrinking_test.rb | 171 ++++++++++++++++ 4 files changed, 375 insertions(+), 14 deletions(-) create mode 100644 service/lib/agama/storage/device_shrinking.rb create mode 100644 service/test/agama/storage/device_shrinking_test.rb diff --git a/service/lib/agama/dbus/storage/interfaces/device/block.rb b/service/lib/agama/dbus/storage/interfaces/device/block.rb index def101b5de..d9f3f5ad8f 100644 --- a/service/lib/agama/dbus/storage/interfaces/device/block.rb +++ b/service/lib/agama/dbus/storage/interfaces/device/block.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require "dbus" +require "agama/storage/device_shrinking" module Agama module DBus @@ -86,11 +87,17 @@ def block_size storage_device.size.to_i end - # Size of the space that could be theoretically reclaimed by shrinking the device. + # Shrinking information. # - # @return [Integer] - def block_recoverable_size - storage_device.recoverable_size.to_i + # @return [Hash] + def block_shrinking + shrinking = Agama::Storage::DeviceShrinking.new(storage_device) + + if shrinking.supported? + { "Supported" => shrinking.min_size.to_i } + else + { "Unsupported" => shrinking.unsupported_reasons } + end end # Name of the currently installed systems @@ -112,7 +119,7 @@ def self.included(base) dbus_reader :block_udev_ids, "as", dbus_name: "UdevIds" dbus_reader :block_udev_paths, "as", dbus_name: "UdevPaths" dbus_reader :block_size, "t", dbus_name: "Size" - dbus_reader :block_recoverable_size, "t", dbus_name: "RecoverableSize" + dbus_reader :block_shrinking, "a{sv}", dbus_name: "Shrinking" dbus_reader :block_systems, "as", dbus_name: "Systems" end end diff --git a/service/lib/agama/storage/device_shrinking.rb b/service/lib/agama/storage/device_shrinking.rb new file mode 100644 index 0000000000..9d4bae94fd --- /dev/null +++ b/service/lib/agama/storage/device_shrinking.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast/i18n" +require "y2storage" + +module Agama + module Storage + # Generates information about shrinking a block device. + class DeviceShrinking + include Yast::I18n + + # @param device [Y2Storage::BlkDevice] + def initialize(device) + textdomain "agama" + + @device = device + end + + # Whether the device can be shrunk. + # + # Conditions to allow shrinking: + # * Either the device does not exist in system yet or it has some content. + # * The device allows shriking to a minimum valid size (bigger than 0 and less than the + # device size). + # * There is no reason preventing to shrink the device. + # + # @return [Boolean] + def supported? + (!device.exists_in_probed? || content?) && min_size? && !reasons? + end + + # Minimun size the device can be shrunk to. + # + # @return [DiskSize, nil] nil if the device cannot be shrunk. + def min_size + return nil unless supported? + + device.resize_info.min_size + end + + # Reasons because the device cannot be shrunk. + # + # @return [Array, nil] nil if the device can be shrunk. + def unsupported_reasons + return nil if supported? + + [ + content_reason_text, + min_size_reason_text, + shrinking_reasons_text, + resize_reasons_text + ].flatten.compact + end + + private + + # @return [Y2Storage::BlkDevice] + attr_reader :device + + # Reasons preventing shrinking. + SHRINKING_REASONS = [ + :RB_SHRINK_NOT_SUPPORTED_BY_FILESYSTEM, + :RB_SHRINK_NOT_SUPPORTED_BY_MULTIDEVICE_FILESYSTE, + :RB_MIN_SIZE_FOR_FILESYSTEM, + :RB_MIN_SIZE_FOR_PARTITION, + :RB_SHRINK_NOT_SUPPORTED_FOR_LVM_LV_TYPE, + :RB_MIN_SIZE_FOR_LVM_LV, + :RB_FILESYSTEM_FULL + ].freeze + private_constant :SHRINKING_REASONS + + # Reasons preventing both shrinking and growing. + RESIZE_REASONS = [ + :RB_RESIZE_NOT_SUPPORTED_BY_DEVICE, + :RB_MIN_MAX_ERROR, + :RB_FILESYSTEM_INCONSISTENT, + :RB_EXTENDED_PARTITION, + :RB_ON_IMPLICIT_PARTITION_TABLE, + :RB_RESIZE_NOT_SUPPORTED_FOR_LVM_LV_TYPE, + :RB_RESIZE_NOT_SUPPORTED_DUE_TO_SNAPSHOTS, + :RB_PASSWORD_REQUIRED + ].freeze + private_constant :RESIZE_REASONS + + # Whether the device has some content. + # + # If the device only contains an encryption layer, then the device is considered as empty. + # + # @return [Boolean] + def content? + device = self.device.encryption || self.device + device.descendants.any? + end + + # Whether the device can be shrunk to a minimum valid size. + # + # @return [Boolean] + def min_size? + min_size = device.resize_info.min_size + + min_size > 0 && min_size != device.size + end + + # Whether there is some reason preventing to shrink the device. + # + # @return [Boolean] + def reasons? + shrinking_reasons.any? || resize_reasons.any? + end + + # Reasons preventing to shrink. + # + # @return [Array] + def shrinking_reasons + device.resize_info.reasons.select { |r| SHRINKING_REASONS.include?(r) } + end + + # Reasons preventing both to shrink and to grow. + # + # @return [Array] + def resize_reasons + device.resize_info.reasons.select { |r| RESIZE_REASONS.include?(r) } + end + + # Text of the reason preventing to shrink because there is no content. + # + # @return [String, nil] nil if there is content or there is any other reasons. + def content_reason_text + return nil if content? || reasons? + + _("Neither a file system nor a storage system was detected on the device. In case the " \ + "device does contain a file system or a storage system that is not supported, resizing " \ + "will most likely cause data loss.") + end + + # Text of the reason preventing to shrink because there is no valid minimum size. + # + # @return [String, nil] nil if there is a minimum size or there is any other reasons. + def min_size_reason_text + return nil if min_size? || reasons? + + _("Shrinking is not supported by this device") + end + + # Text of the reasons proventing to shrink. + # + # @return [Array] + def shrinking_reasons_text + shrinking_reasons.map { |r| reason_text(r) } + end + + # Text of the reasons proventing both to shrink and to grow. + # + # @return [Array] + def resize_reasons_text + resize_reasons.map { |r| reason_text(r) } + end + + # Text of the given reason. + # + # @param reason [Symbol] + # @return [String] + def reason_text(reason) + device.resize_info.reason_text(reason) + end + end + end +end diff --git a/service/test/agama/dbus/storage/interfaces/device/block_examples.rb b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb index ba3ec052a9..9f4c7130da 100644 --- a/service/test/agama/dbus/storage/interfaces/device/block_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb @@ -89,15 +89,11 @@ end end - describe "#block_recoverable_size" do - before do - allow(device).to receive(:recoverable_size).and_return(size) - end - - let(:size) { Y2Storage::DiskSize.new(1024) } - - it "returns the recoverable size in bytes" do - expect(subject.block_recoverable_size).to eq(1024) + describe "#block_shrinking" do + it "returns the shrinking info" do + expect(subject.block_shrinking).to eq( + { "Unsupported"=>["Resizing is not supported by this device."] } + ) end end diff --git a/service/test/agama/storage/device_shrinking_test.rb b/service/test/agama/storage/device_shrinking_test.rb new file mode 100644 index 0000000000..7e52acd74a --- /dev/null +++ b/service/test/agama/storage/device_shrinking_test.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require_relative "storage_helpers" +require "agama/storage/device_shrinking" +require "y2storage" + +describe Agama::Storage::DeviceShrinking do + include Agama::RSpec::StorageHelpers + + subject { described_class.new(device) } + + let(:device) { target.find_by_name(device_name) } + + let(:resize_info) do + instance_double(Y2Storage::ResizeInfo, min_size: min_size, reasons: reasons) + end + + let(:min_size) { Y2Storage::DiskSize.zero } + + let(:reasons) { [] } + + let(:system) { Y2Storage::StorageManager.instance.probed } + + let(:target) { Y2Storage::StorageManager.instance.staging } + + before do + mock_storage(devicegraph: "partitioned_md.yml") + + sdb = target.find_by_name("/dev/sdb") + gpt = sdb.create_partition_table(Y2Storage::PartitionTables::Type::GPT) + gpt.create_partition( + "/dev/sdb1", + Y2Storage::Region.create(2048, 1048576, 512), + Y2Storage::PartitionType::PRIMARY + ) + + allow(device).to receive(:resize_info).and_return(resize_info) + end + + describe "#supported?" do + shared_examples "supported checks" do + context "and the min size for resizing is 0" do + let(:min_size) { Y2Storage::DiskSize.zero } + + it "returns false" do + expect(subject.supported?).to eq(false) + end + end + + context "and the min size for resizing is equal to the device size" do + let(:min_size) { device.size } + + it "returns false" do + expect(subject.supported?).to eq(false) + end + end + + context "and the min size for resizing is valid" do + let(:min_size) { Y2Storage::DiskSize.MiB(100) } + + context "and there is some reasons preventing to shrink" do + let(:reasons) { [:RB_MIN_SIZE_FOR_FILESYSTEM] } + + it "returns false" do + expect(subject.supported?).to eq(false) + end + end + + context "and there is some reasons preventing to resize" do + let(:reasons) { [:RB_RESIZE_NOT_SUPPORTED_BY_DEVICE] } + + it "returns false" do + expect(subject.supported?).to eq(false) + end + end + + context "and there is not reasons" do + let(:reasons) { [] } + + it "returns true" do + expect(subject.supported?).to eq(true) + end + end + end + end + + context "if the device does not exist in the system yet" do + let(:device_name) { "/dev/sdb1" } + + include_examples "supported checks" + end + + context "if the device already exists in the system" do + context "and it has no content" do + let(:device_name) { "/dev/md0p1" } + + it "returns false" do + expect(subject.supported?).to eq(false) + end + end + + context "and it has content" do + let(:device_name) { "/dev/sda1" } + + include_examples "supported checks" + end + end + end + + describe "#min_size" do + let(:min_size) { Y2Storage::DiskSize.MiB(100) } + + context "if shrinking is not supported" do + let(:device_name) { "/dev/md0p1" } + + it "returns nil" do + expect(subject.min_size).to be_nil + end + end + + context "if shrinking is supported" do + let(:device_name) { "/dev/sda1" } + + it "returns the min size the device can be shrunk to" do + expect(subject.min_size).to eq(min_size) + end + end + end + + describe "#unsupported_reasons" do + let(:min_size) { Y2Storage::DiskSize.MiB(100) } + + context "if shrinking is supported" do + let(:device_name) { "/dev/sda1" } + + it "returns nl" do + expect(subject.unsupported_reasons).to be_nil + end + end + + context "if shrinking is not supported" do + let(:device_name) { "/dev/md0p1" } + + it "returns the list of reasons" do + expect(subject.unsupported_reasons).to contain_exactly( + /a file system nor a storage system was detected/ + ) + end + end + end +end From 2d62f27b67a10ccdda44fe39c70fb8f4eb40c39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Jul 2024 14:06:57 +0100 Subject: [PATCH 128/430] rust: adapt code to changes in storage D-Bus API --- rust/agama-lib/src/storage/client.rs | 2 +- rust/agama-lib/src/storage/model.rs | 33 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 6f5a8ef158..9998cc61aa 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -218,8 +218,8 @@ impl<'a> StorageClient<'a> { Ok(Some(BlockDevice { active: get_property(properties, "Active")?, encrypted: get_property(properties, "Encrypted")?, - recoverable_size: get_property(properties, "RecoverableSize")?, size: get_property(properties, "Size")?, + shrinking: get_property(properties, "Shrinking")?, start: get_property(properties, "Start")?, systems: get_property(properties, "Systems")?, udev_ids: get_property(properties, "UdevIds")?, diff --git a/rust/agama-lib/src/storage/model.rs b/rust/agama-lib/src/storage/model.rs index 1c450e61d3..38b4e57955 100644 --- a/rust/agama-lib/src/storage/model.rs +++ b/rust/agama-lib/src/storage/model.rs @@ -504,14 +504,45 @@ pub struct DeviceInfo { pub struct BlockDevice { pub active: bool, pub encrypted: bool, - pub recoverable_size: DeviceSize, pub size: DeviceSize, + pub shrinking: ShrinkingInfo, pub start: u64, pub systems: Vec, pub udev_ids: Vec, pub udev_paths: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum ShrinkingInfo { + Supported(DeviceSize), + Unsupported(Vec), +} + +impl TryFrom> for ShrinkingInfo { + type Error = zbus::zvariant::Error; + + fn try_from(value: zbus::zvariant::Value) -> Result { + let hash: HashMap = value.clone().try_into()?; + let mut info: Option = None; + + if let Some(size) = get_optional_property(&hash, "Supported")? { + info = Some(Self::Supported(size)); + } + if let Some(reasons) = get_optional_property(&hash, "Unsupported")? { + info = Some(Self::Unsupported(reasons)); + } + + if let Some(info_value) = info { + Ok(info_value) + } else { + Err(Self::Error::Message( + format!("Wrong value for Shrinking: {}", value).to_string(), + )) + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Component { From ddbcdaac2bad10527c50d6d6adee8b9f0097c471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Jul 2024 14:09:24 +0100 Subject: [PATCH 129/430] web: adapt storage client to changes in HTTP API --- web/src/client/storage.js | 6 +- web/src/client/storage.test.js | 60 +++++----- .../core/ExpandableSelector.test.jsx | 8 +- .../storage/BootConfigField.test.jsx | 2 +- .../components/storage/BootSelection.test.jsx | 6 +- .../storage/InstallationDeviceField.test.jsx | 4 +- .../storage/ProposalActionsSummary.test.jsx | 2 +- .../storage/ProposalSettingsSection.test.jsx | 4 +- .../storage/VolumeLocationDialog.test.jsx | 4 +- .../components/storage/device-utils.test.jsx | 4 +- .../storage/test-data/full-result-example.js | 104 +++++++++--------- web/src/components/storage/utils.test.js | 6 +- 12 files changed, 107 insertions(+), 103 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 83303d41ff..d7d59fbfc4 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -72,7 +72,7 @@ class DBusClient { * @property {boolean} [encrypted] - Whether the device is encrypted (only for block devices) * @property {boolean} [isEFI] - Whether the device is an EFI partition (only for partition) * @property {number} [size] - * @property {number} [recoverableSize] + * @property {ShrinkingInfo} [shrinking] * @property {string[]} [systems] - Name of the installed systems * @property {string[]} [udevIds] * @property {string[]} [udevPaths] @@ -102,6 +102,10 @@ class DBusClient { * @property {string} [mountPath] * @property {string} [label] * + * @typedef {object} ShrinkingInfo + * @property {number} [supported] - Min size the device can be shrunk to. + * @property {string[]} [unsupported] - Reasons why the device cannot be shrunk. + * * @typedef {object} ProposalResult * @property {ProposalSettings} settings * @property {Action[]} actions diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 2c07469955..704b485633 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -59,7 +59,7 @@ const sda = { size: 1024, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], @@ -76,7 +76,7 @@ const sda1 = { size: 512, start: 123, encrypted: false, - recoverableSize: 128, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [], @@ -94,7 +94,7 @@ const sda2 = { size: 256, start: 1789, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [], @@ -120,7 +120,7 @@ const sdb = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: ["pci-0000:00-19"] @@ -145,7 +145,7 @@ const sdc = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] @@ -170,7 +170,7 @@ const sdd = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] @@ -195,7 +195,7 @@ const sde = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] @@ -214,7 +214,7 @@ const md0 = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, devices: [], systems : ["openSUSE Leap 15.2"], udevIds: [], @@ -241,7 +241,7 @@ const raid = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, devices: [], systems : [], udevIds: [], @@ -267,7 +267,7 @@ const multipath = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] @@ -292,7 +292,7 @@ const dasd = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] @@ -317,7 +317,7 @@ const sdf = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] @@ -334,7 +334,7 @@ const sdf1 = { size: 512, start: 1024, encrypted: true, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [], @@ -362,7 +362,7 @@ const lvmLv1 = { size: 512, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { supported: 128 }, systems : [], udevIds: [], udevPaths: [] @@ -485,7 +485,7 @@ const sdbStaging = { size: 2048, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: ["pci-0000:00-19"] @@ -667,7 +667,7 @@ const contexts = { encrypted: false, size: 1024, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"] @@ -703,7 +703,7 @@ const contexts = { encrypted: false, size: 512, start: 123, - recoverableSize: 128, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -726,7 +726,7 @@ const contexts = { encrypted: false, size: 256, start: 1789, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -748,7 +748,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: ["pci-0000:00-19"] @@ -783,7 +783,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -818,7 +818,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -853,7 +853,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -888,7 +888,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: ["openSUSE Leap 15.2"], udevIds: [], udevPaths: [] @@ -916,7 +916,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -949,7 +949,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -982,7 +982,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -1012,7 +1012,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -1048,7 +1048,7 @@ const contexts = { encrypted: true, size: 512, start: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: [] @@ -1083,7 +1083,7 @@ const contexts = { encrypted: false, size: 512, start: 0, - recoverableSize: 0, + shrinking: { supported: 128 }, systems: [], udevIds: [], udevPaths: [] @@ -1118,7 +1118,7 @@ const contexts = { encrypted: false, size: 2048, start: 0, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: ["pci-0000:00-19"] diff --git a/web/src/components/core/ExpandableSelector.test.jsx b/web/src/components/core/ExpandableSelector.test.jsx index bcfaf8887a..eee55dd6be 100644 --- a/web/src/components/core/ExpandableSelector.test.jsx +++ b/web/src/components/core/ExpandableSelector.test.jsx @@ -39,7 +39,7 @@ const sda = { active: true, name: "/dev/sda", size: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], @@ -52,7 +52,7 @@ const sda1 = { active: true, name: "/dev/sda1", size: 512, - recoverableSize: 128, + shrinking: { supported: 128 }, systems : [], udevIds: [], udevPaths: [] @@ -65,7 +65,7 @@ const sda2 = { active: true, name: "/dev/sda2", size: 512, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] @@ -92,7 +92,7 @@ const sdb = { active: true, name: "/dev/sdb", size: 2048, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: ["pci-0000:00-19"] diff --git a/web/src/components/storage/BootConfigField.test.jsx b/web/src/components/storage/BootConfigField.test.jsx index 94f1537f32..e478b95651 100644 --- a/web/src/components/storage/BootConfigField.test.jsx +++ b/web/src/components/storage/BootConfigField.test.jsx @@ -48,7 +48,7 @@ const sda = { active: true, name: "/dev/sda", size: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], diff --git a/web/src/components/storage/BootSelection.test.jsx b/web/src/components/storage/BootSelection.test.jsx index 19d1688f61..df352f933b 100644 --- a/web/src/components/storage/BootSelection.test.jsx +++ b/web/src/components/storage/BootSelection.test.jsx @@ -47,7 +47,7 @@ const sda = { name: "/dev/sda", description: "", size: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], @@ -70,7 +70,7 @@ const sdb = { name: "/dev/sdb", description: "", size: 2048, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: ["pci-0000:00-19"] @@ -93,7 +93,7 @@ const sdc = { name: "/dev/sdc", description: "", size: 2048, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], udevPaths: ["pci-0000:00-19"] diff --git a/web/src/components/storage/InstallationDeviceField.test.jsx b/web/src/components/storage/InstallationDeviceField.test.jsx index a2a90df3cd..a402188b3b 100644 --- a/web/src/components/storage/InstallationDeviceField.test.jsx +++ b/web/src/components/storage/InstallationDeviceField.test.jsx @@ -57,7 +57,7 @@ const sda = { active: true, name: "/dev/sda", size: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], @@ -80,7 +80,7 @@ const sdb = { active: true, name: "/dev/sdb", size: 2048, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: ["pci-0000:00-19"] diff --git a/web/src/components/storage/ProposalActionsSummary.test.jsx b/web/src/components/storage/ProposalActionsSummary.test.jsx index a4fa6931ff..eda355bc2f 100644 --- a/web/src/components/storage/ProposalActionsSummary.test.jsx +++ b/web/src/components/storage/ProposalActionsSummary.test.jsx @@ -44,7 +44,7 @@ const sda = { active: true, name: "/dev/sda", size: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index 99e539a90c..caa0a17729 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -57,7 +57,7 @@ const sda = { active: true, name: "/dev/sda", size: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], @@ -80,7 +80,7 @@ const sdb = { active: true, name: "/dev/sdb", size: 2048, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: ["pci-0000:00-19"] diff --git a/web/src/components/storage/VolumeLocationDialog.test.jsx b/web/src/components/storage/VolumeLocationDialog.test.jsx index 0f261fdb83..fafe9ca965 100644 --- a/web/src/components/storage/VolumeLocationDialog.test.jsx +++ b/web/src/components/storage/VolumeLocationDialog.test.jsx @@ -49,7 +49,7 @@ const sda = { active: true, name: "/dev/sda", size: 1024, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], @@ -107,7 +107,7 @@ const sdb = { active: true, name: "/dev/sdb", size: 2048, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: ["pci-0000:00-19"] diff --git a/web/src/components/storage/device-utils.test.jsx b/web/src/components/storage/device-utils.test.jsx index 1fdc0f040e..218208233f 100644 --- a/web/src/components/storage/device-utils.test.jsx +++ b/web/src/components/storage/device-utils.test.jsx @@ -68,7 +68,7 @@ const vda1 = { size: 512, start: 123, encrypted: false, - recoverableSize: 128, + shrinking: { supported: 128 }, systems : [], udevIds: [], udevPaths: [], @@ -87,7 +87,7 @@ const lvmLv1 = { size: 512, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] diff --git a/web/src/components/storage/test-data/full-result-example.js b/web/src/components/storage/test-data/full-result-example.js index 9013994320..79aaa3ed32 100644 --- a/web/src/components/storage/test-data/full-result-example.js +++ b/web/src/components/storage/test-data/full-result-example.js @@ -86,7 +86,7 @@ export const settings = { "encrypted": false, "start": 0, "size": 32212254720, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -105,7 +105,7 @@ export const settings = { "encrypted": false, "start": 2048, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -129,7 +129,7 @@ export const settings = { "encrypted": false, "start": 10487808, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [ "openSUSE Leap 15.2", "Fedora 10.30" @@ -156,7 +156,7 @@ export const settings = { "encrypted": false, "start": 20973568, "size": 1073741824, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -178,7 +178,7 @@ export const settings = { "encrypted": false, "start": 23070720, "size": 2147483648, - "recoverableSize": 2147483136, + "shrinking": { "supported": 2147483136 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -222,7 +222,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 53687091200, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -241,7 +241,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 8388608, - "recoverableSize": 8388096, + "shrinking": { "supported": 8388096 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -259,7 +259,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 53677637120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -299,7 +299,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -331,7 +331,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 32212254720, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -350,7 +350,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -374,7 +374,7 @@ export const devices = { "encrypted": false, "start": 10487808, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [ "openSUSE Leap 15.2", "Fedora 10.30" @@ -401,7 +401,7 @@ export const devices = { "encrypted": false, "start": 20973568, "size": 1073741824, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -423,7 +423,7 @@ export const devices = { "encrypted": false, "start": 23070720, "size": 2147483648, - "recoverableSize": 2147483136, + "shrinking": { "supported": 2147483136 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -460,7 +460,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -484,7 +484,7 @@ export const devices = { "encrypted": false, "start": 10487808, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [ "openSUSE Leap 15.2", "Fedora 10.30" @@ -506,7 +506,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 10737287168, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [ "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea" @@ -525,7 +525,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 2147483648, - "recoverableSize": 2040147968, + "shrinking": { "supported": 2040147968 }, "systems": [], "udevIds": [ "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1" @@ -565,7 +565,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 53677637120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -591,7 +591,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 51527024640, - "recoverableSize": 30647779328, + "shrinking": { "supported": 30647779328 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -611,7 +611,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 2147483648, - "recoverableSize": 2143289344, + "shrinking": { "supported": 2143289344 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -633,7 +633,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 51527024640, - "recoverableSize": 30647779328, + "shrinking": { "supported": 30647779328 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -653,7 +653,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 2147483648, - "recoverableSize": 2143289344, + "shrinking": { "supported": 2143289344 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -673,7 +673,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 8388608, - "recoverableSize": 8388096, + "shrinking": { "supported": 8388096 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -691,7 +691,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 53677637120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -715,7 +715,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -739,7 +739,7 @@ export const devices = { "encrypted": false, "start": 10487808, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [ "openSUSE Leap 15.2", "Fedora 10.30" @@ -766,7 +766,7 @@ export const devices = { "encrypted": false, "start": 20973568, "size": 1073741824, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -788,7 +788,7 @@ export const devices = { "encrypted": false, "start": 23070720, "size": 2147483648, - "recoverableSize": 2147483136, + "shrinking": { "supported": 2147483136 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -806,7 +806,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 2147483648, - "recoverableSize": 2040147968, + "shrinking": { "supported": 2040147968 }, "systems": [], "udevIds": [ "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1" @@ -841,7 +841,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 53687091200, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -860,7 +860,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 8388608, - "recoverableSize": 8388096, + "shrinking": { "supported": 8388096 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -878,7 +878,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 53677637120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -918,7 +918,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 5368709120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -950,7 +950,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 32212254720, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -969,7 +969,7 @@ export const devices = { "encrypted": false, "start": 10487808, "size": 5368709120, - "recoverableSize": 5368708608, + "shrinking": { "supported": 5368708608 }, "systems": [ "openSUSE Leap 15.2", "Fedora 10.30" @@ -990,7 +990,7 @@ export const devices = { "encrypted": false, "start": 23070720, "size": 1608515584, - "recoverableSize": 1608515072, + "shrinking": { "supported": 1608515072 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1008,7 +1008,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 8388608, - "recoverableSize": 8388096, + "shrinking": { "supported": 8388096 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1026,7 +1026,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 1610612736, - "recoverableSize": 1610571776, + "shrinking": { "supported": 1610571776 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1049,7 +1049,7 @@ export const devices = { "encrypted": false, "start": 26212352, "size": 18791513600, - "recoverableSize": 18523078144, + "shrinking": { "supported": 18523078144 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1094,7 +1094,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 53677637120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1120,7 +1120,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 51527024640, - "recoverableSize": 30647779328, + "shrinking": { "supported": 30647779328 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -1140,7 +1140,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 2147483648, - "recoverableSize": 2143289344, + "shrinking": { "supported": 2143289344 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -1162,7 +1162,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 51527024640, - "recoverableSize": 30647779328, + "shrinking": { "supported": 30647779328 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -1182,7 +1182,7 @@ export const devices = { "encrypted": false, "start": 0, "size": 2147483648, - "recoverableSize": 2143289344, + "shrinking": { "supported": 2143289344 }, "systems": [], "udevIds": [], "udevPaths": [], @@ -1202,7 +1202,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 8388608, - "recoverableSize": 8388096, + "shrinking": { "supported": 8388096 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1220,7 +1220,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 53677637120, - "recoverableSize": 0, + "shrinking": { "unsupported": ["Resizing is not supported"] }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1244,7 +1244,7 @@ export const devices = { "encrypted": false, "start": 10487808, "size": 5368709120, - "recoverableSize": 5368708608, + "shrinking": { "supported": 5368708608 }, "systems": [ "openSUSE Leap 15.2", "Fedora 10.30" @@ -1265,7 +1265,7 @@ export const devices = { "encrypted": false, "start": 23070720, "size": 1608515584, - "recoverableSize": 1608515072, + "shrinking": { "supported": 1608515072 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1283,7 +1283,7 @@ export const devices = { "encrypted": false, "start": 2048, "size": 8388608, - "recoverableSize": 8388096, + "shrinking": { "supported": 8388096 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1301,7 +1301,7 @@ export const devices = { "encrypted": false, "start": 18432, "size": 1610612736, - "recoverableSize": 1610571776, + "shrinking": { "supported": 1610571776 }, "systems": [], "udevIds": [], "udevPaths": [ @@ -1324,7 +1324,7 @@ export const devices = { "encrypted": false, "start": 26212352, "size": 18791513600, - "recoverableSize": 18523078144, + "shrinking": { "supported": 18523078144 }, "systems": [], "udevIds": [], "udevPaths": [ diff --git a/web/src/components/storage/utils.test.js b/web/src/components/storage/utils.test.js index 3798ea2c4c..72359504c3 100644 --- a/web/src/components/storage/utils.test.js +++ b/web/src/components/storage/utils.test.js @@ -103,7 +103,7 @@ const sda1 = { size: 512, start: 123, encrypted: false, - recoverableSize: 128, + shrinking: { supported: 128 }, systems : [], udevIds: [], udevPaths: [], @@ -121,7 +121,7 @@ const sda2 = { size: 256, start: 1789, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [], @@ -159,7 +159,7 @@ const lvmLv1 = { size: 512, start: 0, encrypted: false, - recoverableSize: 0, + shrinking: { unsupported: ["Resizing is not supported"] }, systems : [], udevIds: [], udevPaths: [] From 5eee771f96e66d0fc164ba21a706d72b4e68e9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 5 Jul 2024 14:10:24 +0100 Subject: [PATCH 130/430] web: improve UI for finding space - Render actions with a toggle group. - Add button to show info about the device. --- .../components/storage/SpaceActionsTable.jsx | 116 +++++++--- .../storage/SpaceActionsTable.test.jsx | 206 ++++++++++++++++++ .../storage/SpacePolicySelection.jsx | 1 - 3 files changed, 291 insertions(+), 32 deletions(-) create mode 100644 web/src/components/storage/SpaceActionsTable.test.jsx diff --git a/web/src/components/storage/SpaceActionsTable.jsx b/web/src/components/storage/SpaceActionsTable.jsx index fb3741a535..c756fc2a67 100644 --- a/web/src/components/storage/SpaceActionsTable.jsx +++ b/web/src/components/storage/SpaceActionsTable.jsx @@ -22,7 +22,13 @@ // @ts-check import React from "react"; -import { FormSelect, FormSelectOption } from "@patternfly/react-core"; +import { + Button, + Flex, FlexItem, + List, ListItem, + Popover, + ToggleGroup, ToggleGroupItem +} from "@patternfly/react-core"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; @@ -31,6 +37,7 @@ import { DeviceName, DeviceDetails, DeviceSize, toStorageDevice } from "~/components/storage/device-utils"; import { TreeTable } from "~/components/core"; +import { Icon } from "~/components/layout"; /** * @typedef {import("~/client/storage").PartitionSlot} PartitionSlot @@ -40,45 +47,98 @@ import { TreeTable } from "~/components/core"; */ /** + * Info about the device. * @component * * @param {object} props - * @param {PartitionSlot|StorageDevice} props.item + * @param {StorageDevice} props.device */ -const DeviceSizeDetails = ({ item }) => { - const device = toStorageDevice(item); - if (!device || device.isDrive || device.recoverableSize === 0) return null; +const DeviceInfoContent = ({ device }) => { + const minSize = device.shrinking?.supported; - return deviceSize(device.recoverableSize); + if (minSize) { + const recoverable = device.size - minSize; + return ( + sprintf(_("Up to %s can be recovered by shrinking the device."), deviceSize(recoverable)) + ); + } + + const reasons = device.shrinking.unsupported; + + return ( + <> + {_("The device cannot be shrunk:")} + + {reasons.map((reason, idx) => {reason})} + + + ); }; /** - * Form to configure the space action for a device (a partition). + * Button to show a popup with info about the device. + * @component + * + * @param {object} props + * @param {StorageDevice} props.device + */ +const DeviceInfo = ({ device }) => { + return ( + } + > + })); +jest.mock("~/queries/l10n", () => ({ + useL10n: () => mockLoadedData +})); + beforeEach(() => { mockLoadedData = { - locale: { id: "en_US.UTF-8", name: "English", territory: "United States" }, - keymap: { id: "us", name: "English" }, - timezone: { id: "Europe/Berlin", parts: ["Europe", "Berlin"] } + locales, + keymaps, + timezones, + selectedLocale: locales[0], + selectedKeymap: keymaps[0], + selectedTimezone: timezones[0], }; }); @@ -49,7 +70,7 @@ it("renders a section for configuring the language", () => { describe("if there is no selected language", () => { beforeEach(() => { - mockLoadedData.locale = undefined; + mockLoadedData.selectedLocale = undefined; }); it("renders a button for selecting a language", () => { @@ -69,7 +90,7 @@ it("renders a section for configuring the keyboard", () => { describe("if there is no selected keyboard", () => { beforeEach(() => { - mockLoadedData.keymap = undefined; + mockLoadedData.selectedKeymap = undefined; }); it("renders a button for selecting a keyboard", () => { @@ -89,7 +110,7 @@ it("renders a section for configuring the time zone", () => { describe("if there is no selected time zone", () => { beforeEach(() => { - mockLoadedData.timezone = undefined; + mockLoadedData.selectedTimezone = undefined; }); it("renders a button for selecting a time zone", () => { diff --git a/web/src/components/l10n/LocaleSelection.jsx b/web/src/components/l10n/LocaleSelection.jsx index 9f143b76dd..543c18336a 100644 --- a/web/src/components/l10n/LocaleSelection.jsx +++ b/web/src/components/l10n/LocaleSelection.jsx @@ -21,10 +21,10 @@ import React, { useState } from "react"; import { Flex, Form, FormGroup, Radio } from "@patternfly/react-core"; -import { useLoaderData, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { ListSearch, Page } from "~/components/core"; import { _ } from "~/i18n"; -import { useConfigMutation } from "~/queries/l10n"; +import { useConfigMutation, useL10n } from "~/queries/l10n"; import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; // TODO: Add documentation and typechecking @@ -32,7 +32,7 @@ import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; export default function LocaleSelection() { const navigate = useNavigate(); const setConfig = useConfigMutation(); - const { locales, locale: currentLocale } = useLoaderData(); + const { locales, selectedLocale: currentLocale } = useL10n(); const [selected, setSelected] = useState(currentLocale.id); const [filteredLocales, setFilteredLocales] = useState(locales); diff --git a/web/src/components/l10n/LocaleSelection.test.jsx b/web/src/components/l10n/LocaleSelection.test.jsx index 1fc001578f..a104ddcd30 100644 --- a/web/src/components/l10n/LocaleSelection.test.jsx +++ b/web/src/components/l10n/LocaleSelection.test.jsx @@ -36,13 +36,13 @@ const mockConfigMutation = { jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), + useL10n: () => ({ locales, selectedLocale: locales[0] }), useConfigMutation: () => mockConfigMutation })); jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), - useNavigate: () => mockNavigateFn, - useLoaderData: () => ({ locales, locale: locales[0] }) + useNavigate: () => mockNavigateFn })); it("allows changing the keyboard", async () => { diff --git a/web/src/components/l10n/TimezoneSelection.jsx b/web/src/components/l10n/TimezoneSelection.jsx index 4650bfa52b..a67c65fc5a 100644 --- a/web/src/components/l10n/TimezoneSelection.jsx +++ b/web/src/components/l10n/TimezoneSelection.jsx @@ -21,11 +21,11 @@ import React, { useState } from "react"; import { Divider, Flex, Form, FormGroup, Radio, Text } from "@patternfly/react-core"; -import { useLoaderData, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { ListSearch, Page } from "~/components/core"; import { _ } from "~/i18n"; import { timezoneTime } from "~/utils"; -import { useConfigMutation } from "~/queries/l10n"; +import { useConfigMutation, useL10n } from "~/queries/l10n"; import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; let date; @@ -56,7 +56,7 @@ export default function TimezoneSelection() { date = new Date(); const navigate = useNavigate(); const setConfig = useConfigMutation(); - const { timezones, timezone: currentTimezone } = useLoaderData(); + const { timezones, selectedTimezone: currentTimezone } = useL10n(); const displayTimezones = timezones.map(timezoneWithDetails); const [selected, setSelected] = useState(currentTimezone.id); const [filteredTimezones, setFilteredTimezones] = useState(sortedTimezones(displayTimezones)); diff --git a/web/src/components/l10n/TimezoneSelection.test.jsx b/web/src/components/l10n/TimezoneSelection.test.jsx index efecd089fb..61e882fc8f 100644 --- a/web/src/components/l10n/TimezoneSelection.test.jsx +++ b/web/src/components/l10n/TimezoneSelection.test.jsx @@ -36,13 +36,13 @@ const mockConfigMutation = { jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), - useConfigMutation: () => mockConfigMutation + useConfigMutation: () => mockConfigMutation, + useL10n: () => ({ timezones, selectedTimezone: timezones[0] }) })); jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), - useNavigate: () => mockNavigateFn, - useLoaderData: () => ({ timezones, timezone: timezones[0] }) + useNavigate: () => mockNavigateFn })); it("allows changing the keyboard", async () => { diff --git a/web/src/components/overview/L10nSection.jsx b/web/src/components/overview/L10nSection.jsx index 1b2c8112dc..b909e79e07 100644 --- a/web/src/components/overview/L10nSection.jsx +++ b/web/src/components/overview/L10nSection.jsx @@ -23,16 +23,10 @@ import React from "react"; import { TextContent, Text, TextVariants } from "@patternfly/react-core"; import { Em } from "~/components/core"; import { _ } from "~/i18n"; -import { localesQuery, configQuery } from "~/queries/l10n"; -import { useQuery } from "@tanstack/react-query"; +import { useL10n } from "~/queries/l10n"; export default function L10nSection() { - const { isPending: isLocalesPending, data: locales } = useQuery(localesQuery()); - const { isPending: isConfigPending, data: config } = useQuery(configQuery()); - - if (isLocalesPending || isConfigPending) return; - - const locale = locales.find((l) => l.id === config?.locales[0]); + const { selectedLocale: locale } = useL10n(); // TRANSLATORS: %s will be replaced by a language name and territory, example: // "English (United States)". diff --git a/web/src/routes/l10n.js b/web/src/routes/l10n.js index b6d26c10ff..3e0df54833 100644 --- a/web/src/routes/l10n.js +++ b/web/src/routes/l10n.js @@ -36,20 +36,6 @@ const LOCALE_SELECTION_PATH = "locale/select"; const KEYMAP_SELECTION_PATH = "keymap/select"; const TIMEZONE_SELECTION_PATH = "timezone/select"; -const l10nLoader = async () => { - const config = await queryClient.fetchQuery(configQuery()); - const locales = await queryClient.fetchQuery(localesQuery()); - const keymaps = await queryClient.fetchQuery(keymapsQuery()); - const timezones = await queryClient.fetchQuery(timezonesQuery()); - - const { locales: [localeId], keymap: keymapId, timezone: timezoneId } = config; - const locale = locales.find((l) => l.id === localeId); - const keymap = keymaps.find((k) => k.id === keymapId); - const timezone = timezones.find((t) => t.id === timezoneId); - - return { locales, locale, keymaps, keymap, timezones, timezone }; -}; - const routes = { path: L10N_PATH, element: , @@ -60,22 +46,18 @@ const routes = { children: [ { index: true, - loader: l10nLoader, element: }, { path: LOCALE_SELECTION_PATH, - loader: l10nLoader, element: , }, { path: KEYMAP_SELECTION_PATH, - loader: l10nLoader, element: , }, { path: TIMEZONE_SELECTION_PATH, - loader: l10nLoader, element: , } ] From bac70f60f4716dd304391a6e12744663ed8663dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 10 Jul 2024 13:21:18 +0100 Subject: [PATCH 151/430] refactor(web): drop useDataInvalidator --- web/src/App.jsx | 3 -- web/src/context/app.jsx | 2 +- web/src/queries/hooks.js | 65 ----------------------------------- web/src/queries/hooks.test.js | 49 -------------------------- web/src/queries/l10n.js | 9 +++-- 5 files changed, 5 insertions(+), 123 deletions(-) delete mode 100644 web/src/queries/hooks.js delete mode 100644 web/src/queries/hooks.test.js diff --git a/web/src/App.jsx b/web/src/App.jsx index bef32b5860..d65379b109 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -29,11 +29,8 @@ import { useInstallerClientStatus } from "~/context/installer"; import { useProduct } from "./context/product"; import { CONFIG, INSTALL, STARTUP } from "~/client/phase"; import { BUSY } from "~/client/status"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useL10nConfigChanges } from "~/queries/l10n"; -const queryClient = new QueryClient(); - /** * Main application component. * diff --git a/web/src/context/app.jsx b/web/src/context/app.jsx index d22ccce51e..3b9a83f0bc 100644 --- a/web/src/context/app.jsx +++ b/web/src/context/app.jsx @@ -52,4 +52,4 @@ function AppProviders({ children }) { ); } -export { AppProviders, queryClient }; +export { AppProviders }; diff --git a/web/src/queries/hooks.js b/web/src/queries/hooks.js deleted file mode 100644 index 1c64660b2f..0000000000 --- a/web/src/queries/hooks.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useRevalidator } from "react-router-dom"; -import { useQueryClient } from "@tanstack/react-query"; - -/** - * Allows invalidating cached data - * - * This hook is useful for marking data as outdated and retrieve it again. To do so, it performs two important steps - * - ask @tanstack/react-query to invalidate query matching given key - * - ask react-router-dom for a revalidation of loaded data - * - * TODO: rethink the revalidation; we may decide to keep the outdated data - * instead, but warning the user about it (as Github does when reviewing a PR, - * for example) - * - * TODO: allow to specify more than one queryKey - * - * To know more, please visit the documentation of these dependencies - * - * - https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation - * - https://reactrouter.com/en/main/hooks/use-revalidator#userevalidator - * - * @example - * - * const dataInvalidator = useDataInvalidator(); - * - * useEffect(() => { - * dataInvalidator({ queryKey: ["user", "auth"] }) - * }, [dataInvalidator]); - */ -const useDataInvalidator = () => { - const queryClient = useQueryClient(); - const revalidator = useRevalidator(); - - const dataInvalidator = ({ queryKey }) => { - if (queryKey) queryClient.invalidateQueries({ queryKey }); - revalidator.revalidate(); - }; - - return dataInvalidator; -}; - -export { - useDataInvalidator -}; diff --git a/web/src/queries/hooks.test.js b/web/src/queries/hooks.test.js deleted file mode 100644 index 6db7d4b3ee..0000000000 --- a/web/src/queries/hooks.test.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { renderHook } from "@testing-library/react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useDataInvalidator } from "~/queries/hooks.js"; - -const mockRevalidateFn = jest.fn(); -jest.mock("react-router-dom", () => ({ - ...jest.requireActual("react-router-dom"), - useRevalidator: () => ({ revalidate: mockRevalidateFn }) -})); - -const queryClient = new QueryClient(); -jest.spyOn(queryClient, "invalidateQueries"); -const wrapper = ({ children }) => ( - - {children} - -); - -describe("useDataInvalidator", () => { - it("forces a data/cache refresh", () => { - const { result } = renderHook(() => useDataInvalidator(), { wrapper }); - const { current: dataInvalidator } = result; - dataInvalidator({ queryKey: "fakeQuery" }); - expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ queryKey: "fakeQuery" }); - expect(mockRevalidateFn).toHaveBeenCalled(); - }); -}); diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js index 0a9c21a183..dae64117bb 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.js @@ -20,9 +20,8 @@ */ import React from "react"; -import { useQueryClient, useMutation, useSuspenseQuery, useSuspenseQueries } from "@tanstack/react-query"; +import { useQueryClient, useMutation, useSuspenseQueries } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; -import { useDataInvalidator } from "~/queries/hooks"; import { timezoneUTCOffset } from "~/utils"; /** @@ -108,7 +107,7 @@ const useConfigMutation = () => { * revalidate its data (executing the loaders again). */ const useL10nConfigChanges = () => { - const dataInvalidator = useDataInvalidator(); + const queryClient = useQueryClient(); const client = useInstallerClient(); React.useEffect(() => { @@ -116,10 +115,10 @@ const useL10nConfigChanges = () => { return client.ws().onEvent(event => { if (event.type === "L10nConfigChanged") { - dataInvalidator({ queryKey: ["l10n", "config"] }); + queryClient.invalidateQueries({ queryKey }); } }); - }, [client, dataInvalidator]); + }, [client, queryClient]); }; /// Returns the l10n data. From fbbd49b002d739d0f72c2a464da917966eba5af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 10 Jul 2024 13:52:49 +0100 Subject: [PATCH 152/430] refactor(web): simpify queries keys --- web/src/queries/l10n.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.js index dae64117bb..66d81f851c 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.js @@ -29,7 +29,7 @@ import { timezoneUTCOffset } from "~/utils"; */ const configQuery = () => { return { - queryKey: ["l10n", "config"], + queryKey: ["l10n/config"], queryFn: () => fetch("/api/l10n/config").then((res) => res.json()), }; }; @@ -38,7 +38,7 @@ const configQuery = () => { * Returns a query for retrieving the list of known locales */ const localesQuery = () => ({ - queryKey: ["l10n", "locales"], + queryKey: ["l10n/locales"], queryFn: async () => { const response = await fetch("/api/l10n/locales"); const locales = await response.json(); @@ -53,7 +53,7 @@ const localesQuery = () => ({ * Returns a query for retrieving the list of known timezones */ const timezonesQuery = () => ({ - queryKey: ["l10n", "timezones"], + queryKey: ["l10n/timezones"], queryFn: async () => { const response = await fetch("/api/l10n/timezones"); const timezones = await response.json(); @@ -69,7 +69,7 @@ const timezonesQuery = () => ({ * Returns a query for retrieving the list of known keymaps */ const keymapsQuery = () => ({ - queryKey: ["l10n", "keymaps"], + queryKey: ["l10n/keymaps"], queryFn: async () => { const response = await fetch("/api/l10n/keymaps"); const json = await response.json(); @@ -115,7 +115,7 @@ const useL10nConfigChanges = () => { return client.ws().onEvent(event => { if (event.type === "L10nConfigChanged") { - queryClient.invalidateQueries({ queryKey }); + queryClient.invalidateQueries({ queryKey: ["l10n/config"] }); } }); }, [client, queryClient]); From 1c4ea4ba7cfc61d50bea6ec7688a0fe97dc65576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 10 Jul 2024 15:07:40 +0100 Subject: [PATCH 153/430] fix(web): make ESLint happy --- web/src/components/l10n/L10nPage.test.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.jsx index 00ddb0dc13..6ad324dc6e 100644 --- a/web/src/components/l10n/L10nPage.test.jsx +++ b/web/src/components/l10n/L10nPage.test.jsx @@ -26,19 +26,19 @@ import L10nPage from "~/components/l10n/L10nPage"; let mockLoadedData; const locales = [ - { id: "en_US.UTF-8", name: "English", territory: "United States" }, + { id: "en_US.UTF-8", name: "English", territory: "United States" }, { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" } ]; const keymaps = [ - { id: "us", name: "English" }, + { id: "us", name: "English" }, { id: "es", name: "Spanish" } ]; const timezones = [ { id: "Europe/Berlin", parts: ["Europe", "Berlin"] }, { id: "Europe/Madrid", parts: ["Europe", "Madrid"] } -] +]; jest.mock('react-router-dom', () => ({ ...jest.requireActual("react-router-dom"), From 8dc27bea1f93aaa6553f6f2bb68312cadafb6936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 10 Jul 2024 15:32:56 +0100 Subject: [PATCH 154/430] test(web): fix L10nSection test --- web/src/components/overview/L10nSection.test.jsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.jsx index dad4cde147..a3f29f6945 100644 --- a/web/src/components/overview/L10nSection.test.jsx +++ b/web/src/components/overview/L10nSection.test.jsx @@ -30,14 +30,7 @@ const locales = [ ]; jest.mock("~/queries/l10n", () => ({ - localesQuery: () => ({ - queryKey: ["l10n", "locales"], - queryFn: jest.fn().mockResolvedValue(locales) - }), - configQuery: () => ({ - queryKey: ["l10n", "config"], - queryFn: jest.fn().mockResolvedValue({ locales: ["en_US.UTF-8"] }) - }) + useL10n: () => ({ locales, selectedLocale: locales[0] }), })); it("displays the selected locale", async () => { From 7af3d316d6bf4f2f7c81d0773ebcf0d515f6d954 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 10 Jul 2024 17:01:39 +0200 Subject: [PATCH 155/430] implement deleting question --- rust/agama-server/src/questions.rs | 1 + rust/agama-server/src/questions/web.rs | 35 +++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/rust/agama-server/src/questions.rs b/rust/agama-server/src/questions.rs index 64b510623d..11c6f68e7c 100644 --- a/rust/agama-server/src/questions.rs +++ b/rust/agama-server/src/questions.rs @@ -221,6 +221,7 @@ impl Questions { } /// Removes question at given object path + /// TODO: use id as parameter ( need at first check other users of method ) async fn delete(&mut self, question: ObjectPath<'_>) -> zbus::fdo::Result<()> { // TODO: error checking let id: u32 = question.rsplit('/').next().unwrap().parse().unwrap(); diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 2866ddfca6..079f4b28e6 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -13,7 +13,7 @@ use agama_lib::{ use anyhow::Context; use axum::{ extract::{Path, State}, - routing::{get, put}, + routing::{delete, get, put}, Json, Router, }; use regex::Regex; @@ -145,6 +145,18 @@ impl<'a> QuestionsClient<'a> { Ok(result) } + pub async fn delete(&self, id: u32) -> Result<(), ServiceError> { + let question_path = ObjectPath::from( + ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) + .context("Failed to create dbus path")?, + ); + + self.questions_proxy + .delete(&question_path) + .await + .map_err(|e| e.into()) + } + pub async fn answer(&self, id: u32, answer: Answer) -> Result<(), ServiceError> { let question_path = OwnedObjectPath::from( ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) @@ -243,6 +255,7 @@ pub async fn questions_service(dbus: zbus::Connection) -> Result, Json(answer): Json, ) -> Result<(), Error> { - state.questions.answer(question_id, answer).await?; - Ok(()) + let res = state.questions.answer(question_id, answer).await; + Ok(res?) +} + +/// Deletes question. +/// +/// * `state`: service state. +/// * `questions_id`: id of question +#[utoipa::path(delete, path = "/questions/:id", responses( + (status = 200, description = "question deleted"), + (status = 400, description = "The D-Bus service could not perform the action") +))] +async fn delete_question( + State(state): State>, + Path(question_id): Path, +) -> Result<(), Error> { + let res = state.questions.delete(question_id).await; + Ok(res?) } /// Create new question. From 1e2a6d7c22dfc5ca05b6e7920f0ca88d4a611fef Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 10 Jul 2024 22:11:28 +0200 Subject: [PATCH 156/430] implement get_answer --- rust/agama-server/src/questions/web.rs | 65 ++++++++++++++++++++++++-- rust/agama-server/src/web/docs.rs | 5 +- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 079f4b28e6..d80fab5804 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -13,7 +13,9 @@ use agama_lib::{ use anyhow::Context; use axum::{ extract::{Path, State}, - routing::{delete, get, put}, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{delete, get}, Json, Router, }; use regex::Regex; @@ -157,6 +159,42 @@ impl<'a> QuestionsClient<'a> { .map_err(|e| e.into()) } + pub async fn get_answer(&self, id: u32) -> Result, ServiceError> { + let question_path = OwnedObjectPath::from( + ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) + .context("Failed to create dbus path")?, + ); + let mut result = Answer { + generic: GenericAnswer { + answer: String::new(), + }, + with_password: None, + }; + let dbus_password_res = QuestionWithPasswordProxy::builder(&self.connection) + .path(&question_path)? + .cache_properties(zbus::CacheProperties::No) + .build() + .await; + if let Ok(dbus_password) = dbus_password_res { + result.with_password = Some(PasswordAnswer { + password: dbus_password.password().await?, + }); + } + + let dbus_generic = GenericQuestionProxy::builder(&self.connection) + .path(&question_path)? + .cache_properties(zbus::CacheProperties::No) + .build() + .await?; + let answer = dbus_generic.answer().await?; + if answer.is_empty() { + Ok(None) + } else { + result.generic.answer = answer; + Ok(Some(result)) + } + } + pub async fn answer(&self, id: u32, answer: Answer) -> Result<(), ServiceError> { let question_path = OwnedObjectPath::from( ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) @@ -256,7 +294,7 @@ pub async fn questions_service(dbus: zbus::Connection) -> Result>, + Path(question_id): Path, +) -> Result { + let res = state.questions.get_answer(question_id).await?; + if let Some(answer) = res { + Ok(Json(answer).into_response()) + } else { + Ok(StatusCode::NOT_FOUND.into_response()) + } +} + /// Provide answer to question. /// /// * `state`: service state. @@ -309,7 +368,7 @@ async fn list_questions( (status = 200, description = "answer question"), (status = 400, description = "The D-Bus service could not perform the action") ))] -async fn answer( +async fn answer_question( State(state): State>, Path(question_id): Path, Json(answer): Json, diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index b0748c314b..dcaa1b0680 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -20,7 +20,10 @@ use utoipa::OpenApi; crate::network::web::devices, crate::network::web::disconnect, crate::network::web::update_connection, - crate::questions::web::answer, + crate::questions::web::answer_question, + crate::questions::web::get_answer, + crate::questions::web::delete_question, + crate::questions::web::create_question, crate::questions::web::list_questions, crate::software::web::get_config, crate::software::web::patterns, From e115f30300800c3621715bf1380a32165fab24dd Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Wed, 10 Jul 2024 22:14:07 +0200 Subject: [PATCH 157/430] changes --- rust/package/agama.changes | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index e139a29486..f825ec58f1 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,8 +1,15 @@ +------------------------------------------------------------------- +Wed Jul 10 20:11:39 UTC 2024 - Josef Reidinger + +- Add to HTTP API method DELETE for question to remove question +- Add to HTTP API method GET for answer to get answer for question + (gh#openSUSE/agama#1453) + ------------------------------------------------------------------- Wed Jul 10 10:01:18 UTC 2024 - Josef Reidinger - Add to HTTP API method POST for question to ask new question - (gh#openSUSE/agam#1451) + (gh#openSUSE/agama#1451) ------------------------------------------------------------------- Fri Jul 5 13:17:17 UTC 2024 - José Iván López González From ab0945d25267f394bb14293734a94c65f426d5cb Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 11 Jul 2024 10:45:32 +0200 Subject: [PATCH 158/430] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa --- rust/package/agama.changes | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index f825ec58f1..7b185bc5e5 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,8 +1,8 @@ ------------------------------------------------------------------- Wed Jul 10 20:11:39 UTC 2024 - Josef Reidinger -- Add to HTTP API method DELETE for question to remove question -- Add to HTTP API method GET for answer to get answer for question +- Add to HTTP API a method to remove questions +- Add to HTTP API method to get the answer to a question (gh#openSUSE/agama#1453) ------------------------------------------------------------------- From de8e3bc8edd1ca1d89ea5134c861124e8428579c Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 11 Jul 2024 10:53:07 +0200 Subject: [PATCH 159/430] use default trait --- rust/agama-server/src/questions/web.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index d80fab5804..19b4463091 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -164,12 +164,7 @@ impl<'a> QuestionsClient<'a> { ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) .context("Failed to create dbus path")?, ); - let mut result = Answer { - generic: GenericAnswer { - answer: String::new(), - }, - with_password: None, - }; + let mut result = Answer::default(); let dbus_password_res = QuestionWithPasswordProxy::builder(&self.connection) .path(&question_path)? .cache_properties(zbus::CacheProperties::No) @@ -266,7 +261,7 @@ pub struct GenericQuestion { #[serde(rename_all = "camelCase")] pub struct QuestionWithPassword {} -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Default, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Answer { generic: GenericAnswer, @@ -274,7 +269,7 @@ pub struct Answer { } /// Answer needed for GenericQuestion -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Default, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct GenericAnswer { answer: String, From 98f3843ee7344a7090f2b0aa5ebde1cecc5f289b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 5 Jul 2024 15:57:23 +0100 Subject: [PATCH 160/430] refactor(web): add software queries and a loader --- web/src/App.jsx | 5 +- web/src/App.test.jsx | 11 +- web/src/MainLayout.jsx | 2 +- web/src/SimpleLayout.jsx | 7 +- .../components/overview/OverviewPage.test.jsx | 7 +- .../product/ProductRegistrationPage.jsx | 2 +- .../product/ProductSelectionPage.jsx | 11 +- .../product/ProductSelectionPage.test.jsx | 7 +- .../product/ProductSelectionProgress.jsx | 6 +- .../components/storage/ProposalPage.test.jsx | 7 +- .../storage/ProposalTransactionalInfo.jsx | 2 +- .../ProposalTransactionalInfo.test.jsx | 7 +- web/src/queries/software.js | 102 ++++++++++++++++++ web/src/router.js | 4 +- .../product/routes.js => routes/products.js} | 14 +-- 15 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 web/src/queries/software.js rename web/src/{components/product/routes.js => routes/products.js} (78%) diff --git a/web/src/App.jsx b/web/src/App.jsx index d65379b109..962996a1b1 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -26,7 +26,7 @@ import { Questions } from "~/components/questions"; import { ServerError, Installation } from "~/components/core"; import { useInstallerL10n } from "./context/installerL10n"; import { useInstallerClientStatus } from "~/context/installer"; -import { useProduct } from "./context/product"; +import { useProduct, useProductChanges } from "./queries/software"; import { CONFIG, INSTALL, STARTUP } from "~/client/phase"; import { BUSY } from "~/client/status"; import { useL10nConfigChanges } from "~/queries/l10n"; @@ -44,6 +44,7 @@ function App() { const { selectedProduct, products } = useProduct(); const { language } = useInstallerL10n(); useL10nConfigChanges(); + useProductChanges(); const Content = () => { if (error) return ; @@ -58,7 +59,7 @@ function App() { return ; } - if (selectedProduct === null && location.pathname !== "/products") { + if ((selectedProduct === undefined) & (location.pathname !== "/products")) { return ; } diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index d3fe46be4b..2fd0ac3986 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -28,6 +28,7 @@ import { createClient } from "~/client"; import { STARTUP, CONFIG, INSTALL } from "~/client/phase"; import { IDLE, BUSY } from "~/client/status"; import { useL10nConfigChanges } from "./queries/l10n"; +import { useProductChanges } from "./queries/software"; jest.mock("~/client"); @@ -35,14 +36,15 @@ jest.mock("~/client"); let mockProducts; let mockSelectedProduct; -jest.mock("~/context/product", () => ({ - ...jest.requireActual("~/context/product"), +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), useProduct: () => { return { products: mockProducts, selectedProduct: mockSelectedProduct }; - } + }, + useProductChanges: () => jest.fn() })); jest.mock("~/queries/l10n", () => ({ @@ -78,7 +80,7 @@ describe("App", () => { l10n: { getUIKeymap: jest.fn().mockResolvedValue("en"), getUILocale: jest.fn().mockResolvedValue("en_us"), - setUILocale: jest.fn().mockResolvedValue("en_us"), + setUILocale: jest.fn().mockResolvedValue("en_us") } }; }); @@ -125,6 +127,7 @@ describe("App", () => { describe("if the service is busy", () => { beforeEach(() => { mockClientStatus.status = BUSY; + mockSelectedProduct = { id: "Tumbleweed" }; }); it("redirects to product selection progress", async () => { diff --git a/web/src/MainLayout.jsx b/web/src/MainLayout.jsx index b830c96a84..6ed3d31994 100644 --- a/web/src/MainLayout.jsx +++ b/web/src/MainLayout.jsx @@ -32,7 +32,7 @@ import { Icon, Loading } from "~/components/layout"; import { About, InstallerOptions, LogsButton } from "~/components/core"; import { _ } from "~/i18n"; import { rootRoutes } from "~/router"; -import { useProduct } from "~/context/product"; +import { useProduct } from "./queries/software"; const Header = () => { const { selectedProduct } = useProduct(); diff --git a/web/src/SimpleLayout.jsx b/web/src/SimpleLayout.jsx index edad34d16f..99ea0b3320 100644 --- a/web/src/SimpleLayout.jsx +++ b/web/src/SimpleLayout.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React from "react"; +import React, { Suspense } from "react"; import { Outlet } from "react-router-dom"; import { Masthead, MastheadContent, @@ -28,6 +28,7 @@ import { } from "@patternfly/react-core"; import { InstallerOptions } from "./components/core"; import { _ } from "~/i18n"; +import { Loading } from "./components/layout"; /** * Simple layout for displaying content that comes before product configuration @@ -49,7 +50,9 @@ export default function SimpleLayout({ showOutlet = true, showInstallerOptions = - {showOutlet ? : children} + }> + {showOutlet ? : children} + ); } diff --git a/web/src/components/overview/OverviewPage.test.jsx b/web/src/components/overview/OverviewPage.test.jsx index 02592374f7..cb0a26f2b5 100644 --- a/web/src/components/overview/OverviewPage.test.jsx +++ b/web/src/components/overview/OverviewPage.test.jsx @@ -29,9 +29,10 @@ const startInstallationFn = jest.fn(); let mockSelectedProduct = { id: "Tumbleweed" }; jest.mock("~/client"); -jest.mock("~/context/product", () => ({ - ...jest.requireActual("~/context/product"), - useProduct: () => ({ selectedProduct: mockSelectedProduct }) +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useProduct: () => ({ selectedProduct: mockSelectedProduct }), + useProductChanges: () => jest.fn() })); jest.mock("~/components/overview/L10nSection", () => () =>
    Localization Section
    ); diff --git a/web/src/components/product/ProductRegistrationPage.jsx b/web/src/components/product/ProductRegistrationPage.jsx index 21e79d9a21..9b74b7beb2 100644 --- a/web/src/components/product/ProductRegistrationPage.jsx +++ b/web/src/components/product/ProductRegistrationPage.jsx @@ -23,7 +23,7 @@ import React, { useState } from "react"; import { Alert, Form, FormGroup } from "@patternfly/react-core"; import { useNavigate } from "react-router-dom"; import { EmailInput, Page, PasswordInput } from "~/components/core"; -import { useProduct } from "~/context/product"; +import { useProduct } from "~/queries/software"; import { useInstallerClient } from "~/context/installer"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; diff --git a/web/src/components/product/ProductSelectionPage.jsx b/web/src/components/product/ProductSelectionPage.jsx index 454a9e39d7..1ba59c83cd 100644 --- a/web/src/components/product/ProductSelectionPage.jsx +++ b/web/src/components/product/ProductSelectionPage.jsx @@ -32,7 +32,7 @@ import styles from '@patternfly/react-styles/css/utilities/Text/text'; import { _ } from "~/i18n"; import { Page } from "~/components/core"; import { Loading, Center } from "~/components/layout"; -import { useProduct } from "~/context/product"; +import { useConfigMutation, useProduct } from "~/queries/software"; const Label = ({ children }) => ( @@ -41,7 +41,8 @@ const Label = ({ children }) => ( ); function ProductSelectionPage() { - const { products, selectedProduct, selectProduct } = useProduct(); + const { products, selectedProduct } = useProduct(); + const setConfig = useConfigMutation(); const [nextProduct, setNextProduct] = useState(selectedProduct); const [isLoading, setIsLoading] = useState(false); @@ -49,15 +50,11 @@ function ProductSelectionPage() { e.preventDefault(); if (nextProduct) { - await selectProduct(nextProduct.id); + setConfig.mutate({ product: nextProduct.id }); setIsLoading(true); } }; - if (!products) return ( - - ); - const Item = ({ children }) => { return ( diff --git a/web/src/components/product/ProductSelectionPage.test.jsx b/web/src/components/product/ProductSelectionPage.test.jsx index b7b21cb75c..4a9db4db8a 100644 --- a/web/src/components/product/ProductSelectionPage.test.jsx +++ b/web/src/components/product/ProductSelectionPage.test.jsx @@ -39,14 +39,15 @@ const products = [ ]; jest.mock("~/client"); -jest.mock("~/context/product", () => ({ - ...jest.requireActual("~/context/product"), +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), useProduct: () => { return { products, selectedProduct: products[0] }; - } + }, + useProductChanges: () => jest.fn() })); const managerMock = { diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.jsx index bfda9a3cc9..944ebe3243 100644 --- a/web/src/components/product/ProductSelectionProgress.jsx +++ b/web/src/components/product/ProductSelectionProgress.jsx @@ -22,7 +22,7 @@ import React, { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; import { _ } from "~/i18n"; -import { useProduct } from "~/context/product"; +import { useProduct } from "~/queries/software"; import { ProgressReport } from "~/components/core"; import { IDLE } from "~/client/status"; import { useInstallerClient } from "~/context/installer"; @@ -42,10 +42,6 @@ function ProductSelectionProgress() { return manager.onStatusChange(setStatus); }, [manager, setStatus]); - if (!selectedProduct) { - return; - } - if (status === IDLE) return ; return ( diff --git a/web/src/components/storage/ProposalPage.test.jsx b/web/src/components/storage/ProposalPage.test.jsx index 801fcf5c3b..8e69b02bb2 100644 --- a/web/src/components/storage/ProposalPage.test.jsx +++ b/web/src/components/storage/ProposalPage.test.jsx @@ -48,11 +48,12 @@ jest.mock("@patternfly/react-core", () => { }); jest.mock("./DevicesTechMenu", () => () =>
    Devices Tech Menu
    ); -jest.mock("~/context/product", () => ({ - ...jest.requireActual("~/context/product"), +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), useProduct: () => ({ selectedProduct: { name: "Test" } - }) + }), + useProductChanges: () => jest.fn() })); const createClientMock = /** @type {jest.Mock} */(createClient); diff --git a/web/src/components/storage/ProposalTransactionalInfo.jsx b/web/src/components/storage/ProposalTransactionalInfo.jsx index 4a4c6606b3..8e65762dc0 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.jsx +++ b/web/src/components/storage/ProposalTransactionalInfo.jsx @@ -23,7 +23,7 @@ import React from "react"; import { Alert } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { useProduct } from "~/context/product"; +import { useProduct } from "~/queries/software"; import { isTransactionalSystem } from "~/components/storage/utils"; /** diff --git a/web/src/components/storage/ProposalTransactionalInfo.test.jsx b/web/src/components/storage/ProposalTransactionalInfo.test.jsx index e9556107fa..561793d4ba 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.test.jsx +++ b/web/src/components/storage/ProposalTransactionalInfo.test.jsx @@ -24,11 +24,12 @@ import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalTransactionalInfo } from "~/components/storage"; -jest.mock("~/context/product", () => ({ - ...jest.requireActual("~/context/product"), +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), useProduct: () => ({ selectedProduct : { name: "Test" } - }) + }), + useProductChanges: () => jest.fn() })); let props; diff --git a/web/src/queries/software.js b/web/src/queries/software.js new file mode 100644 index 0000000000..cb2ef253dc --- /dev/null +++ b/web/src/queries/software.js @@ -0,0 +1,102 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { + QueryClient, + useMutation, + useQueryClient, + useSuspenseQueries +} from "@tanstack/react-query"; +import { useInstallerClient } from "~/context/installer"; + +const configQuery = () => ({ + queryKey: ["software/config"], + queryFn: () => fetch("/api/software/config").then(res => res.json()) +}); + +const productsQuery = () => ({ + queryKey: ["software/products"], + queryFn: () => fetch("/api/software/products").then(res => res.json()), + staleTime: Infinity +}); + +/** + * Hook that builds a mutation to update the software configuration + * + * It does not require to call `useMutation`. + */ +const useConfigMutation = () => { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + + const query = { + mutationFn: newConfig => + fetch("/api/software/config", { + // FIXME: use "PATCH" instead + method: "PUT", + body: JSON.stringify(newConfig), + headers: { + "Content-Type": "application/json" + } + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["software/config"] }); + client.manager.startProbing(); + } + }; + return useMutation(query); +}; + +/** + * Hook that returns a useEffect to listen for software events + * + * When the configuration changes, it invalidates the config query and forces the router to + * revalidate its data (executing the loaders again). + */ +const useProductChanges = () => { + const client = useInstallerClient(); + + React.useEffect(() => { + if (!client) return; + const queryClient = new QueryClient(); + + return client.ws().onEvent(event => { + if (event.type === "ProductChanged") { + queryClient.invalidateQueries({ queryKey: ["software/config"] }); + } + }); + }, [client]); +}; + +const useProduct = () => { + const [{ data: config }, { data: products }] = useSuspenseQueries({ + queries: [configQuery(), productsQuery()] + }); + + const selectedProduct = products.find(p => p.id === config.product); + return { + products, + selectedProduct + }; +}; + +export { configQuery, productsQuery, useConfigMutation, useProduct, useProductChanges }; diff --git a/web/src/router.js b/web/src/router.js index 5b4596ddd5..a4b6732b5f 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -31,7 +31,7 @@ import { _ } from "~/i18n"; import overviewRoutes from "~/components/overview/routes"; import l10nRoutes from "~/routes/l10n"; import networkRoutes from "~/components/network/routes"; -import { productsRoute } from "~/components/product/routes"; +import productsRoutes from "~/routes/products"; import storageRoutes from "~/components/storage/routes"; import softwareRoutes from "~/components/software/routes"; import usersRoutes from "~/components/users/routes"; @@ -62,7 +62,7 @@ const protectedRoutes = [ }, { element: , - children: [productsRoute] + children: [productsRoutes] } ] } diff --git a/web/src/components/product/routes.js b/web/src/routes/products.js similarity index 78% rename from web/src/components/product/routes.js rename to web/src/routes/products.js index 3762d113f7..9ddb6eef94 100644 --- a/web/src/components/product/routes.js +++ b/web/src/routes/products.js @@ -21,11 +21,13 @@ import React from "react"; import { Page } from "~/components/core"; -import ProductSelectionPage from "./ProductSelectionPage"; -import ProductSelectionProgress from "./ProductSelectionProgress"; +import ProductSelectionPage from "~/components/product/ProductSelectionPage"; +import ProductSelectionProgress from "~/components/product/ProductSelectionProgress"; -const productsRoute = { - path: "/products", +const PRODUCTS_PATH = "/products"; + +const productsRoutes = { + path: PRODUCTS_PATH, element: , children: [ { @@ -39,6 +41,4 @@ const productsRoute = { ] }; -export { - productsRoute -}; +export default productsRoutes; From ddd29113aac0f49b6e5efb1ef2731d5a5385bbe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 11 Jul 2024 09:52:06 +0100 Subject: [PATCH 161/430] refactor(web): drop the ProductProvider --- web/src/context/app.jsx | 9 ++-- web/src/context/product.jsx | 103 ------------------------------------ 2 files changed, 3 insertions(+), 109 deletions(-) delete mode 100644 web/src/context/product.jsx diff --git a/web/src/context/app.jsx b/web/src/context/app.jsx index 3b9a83f0bc..704e03339d 100644 --- a/web/src/context/app.jsx +++ b/web/src/context/app.jsx @@ -24,7 +24,6 @@ import React from "react"; import { InstallerClientProvider } from "./installer"; import { InstallerL10nProvider } from "./installerL10n"; -import { ProductProvider } from "./product"; import { IssuesProvider } from "./issues"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -41,11 +40,9 @@ function AppProviders({ children }) { - - - {children} - - + + {children} + diff --git a/web/src/context/product.jsx b/web/src/context/product.jsx deleted file mode 100644 index 3079193e63..0000000000 --- a/web/src/context/product.jsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React, { useContext, useEffect, useState } from "react"; -import { useCancellablePromise } from "~/utils"; -import { useInstallerClient } from "./installer"; - -/** - * @typedef {import ("~/client/software").Product} Product - * @typedef {import ("~/client/software").Registration} Registration - */ - -const ProductContext = React.createContext([]); - -function ProductProvider({ children }) { - const client = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - const [products, setProducts] = useState(undefined); - const [selectedId, setSelectedId] = useState(undefined); - const [registration, setRegistration] = useState(undefined); - - useEffect(() => { - const load = async () => { - const productClient = client.product; - const available = await cancellablePromise(productClient.getAll()); - const selected = await cancellablePromise(productClient.getSelected()); - const registration = await cancellablePromise(productClient.getRegistration()); - setProducts(available); - setSelectedId(selected); - setRegistration(registration); - }; - - if (client) { - load().catch(console.error); - } - }, [client, setProducts, setSelectedId, setRegistration, cancellablePromise]); - - useEffect(() => { - if (!client) return; - - return client.product.onChange(setSelectedId); - }, [client, setSelectedId]); - - useEffect(() => { - if (!client) return; - - return client.product.onRegistrationChange(setRegistration); - }, [client, setRegistration]); - - const selectProduct = async (id) => { - await client.product.select(id); - client.manager.startProbing(); - setSelectedId(id); - }; - - const value = { products, selectedId, registration, selectProduct }; - return {children}; -} - -/** - * Product context. - * @function - * - * @typedef {object} ProductContext - * @property {Product[]} products - * @property {Product|null} selectedProduct - * @property {string} selectedId - * @property {Registration} registration - * - * @returns {ProductContext} - */ -function useProduct() { - const context = useContext(ProductContext); - - if (!context) { - throw new Error("useProduct must be used within a ProductProvider"); - } - - const { products = [], selectedId } = context; - const selectedProduct = products.find(p => p.id === selectedId) || null; - - return { ...context, selectedProduct }; -} - -export { ProductProvider, useProduct }; From cf5b82ac72f0f87b0432db774d4c59ebf5d89aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:34:47 +0100 Subject: [PATCH 162/430] web: UI code pruning and clean up `round #1` (#1441) ## Problem [Switching from the former UI to the current approach](https://github.com/openSUSE/agama/pull/1202) left behind a lot of things to review, dead components and a CSS clean up among them. ## Solution Drop dead components and make as much clean-up rounds as needed. This can be considered `Round #1`, made in the middle of more urgent work. ## Testing Tested manually ## Screenshots Visual changes after this PR should keep at minimum. In any case, it's preferred that reviewers give the UI a shot taking the time they need. --- web/src/assets/styles/app.scss | 137 +------ web/src/assets/styles/blocks.scss | 364 ------------------ web/src/assets/styles/composition.scss | 8 +- web/src/assets/styles/index.scss | 1 - web/src/assets/styles/layout.scss | 84 ---- .../assets/styles/patternfly-overrides.scss | 19 - web/src/assets/styles/utilities.scss | 117 ------ web/src/assets/styles/variables.scss | 32 +- web/src/components/core/EmptyState.jsx | 16 +- web/src/components/core/FileViewer.jsx | 85 ---- web/src/components/core/FileViewer.test.jsx | 120 ------ .../components/core/InstallationFinished.jsx | 16 +- web/src/components/core/Page.jsx | 53 --- web/src/components/core/PageMenu.jsx | 190 --------- web/src/components/core/PageMenu.test.jsx | 105 ----- web/src/components/core/Popup.jsx | 8 +- web/src/components/core/Selector.jsx | 137 ------- web/src/components/core/Selector.test.jsx | 153 -------- web/src/components/core/ServerError.jsx | 43 ++- web/src/components/core/index.js | 3 - web/src/components/layout/Icon.jsx | 2 + .../storage/ProposalActionsDialog.jsx | 11 +- 22 files changed, 58 insertions(+), 1646 deletions(-) delete mode 100644 web/src/assets/styles/layout.scss delete mode 100644 web/src/components/core/FileViewer.jsx delete mode 100644 web/src/components/core/FileViewer.test.jsx delete mode 100644 web/src/components/core/PageMenu.jsx delete mode 100644 web/src/components/core/PageMenu.test.jsx delete mode 100644 web/src/components/core/Selector.jsx delete mode 100644 web/src/components/core/Selector.test.jsx diff --git a/web/src/assets/styles/app.scss b/web/src/assets/styles/app.scss index 3ade7e3ad8..dab621c696 100644 --- a/web/src/assets/styles/app.scss +++ b/web/src/assets/styles/app.scss @@ -1,45 +1,11 @@ -// Make proposal actions compact -.proposal-actions li + li { - margin-block-start: 0; -} - -.proposal-action--delete { - font-weight: bold; -} - -// Align the expandable-actions with the actions list -// See https://www.patternfly.org/components/list#css-variables -.expandable-actions { - // --pf-v5-c-list--PaddingLeft - --pf-v5-c-list--nested--MarginLeft - --pf-v5-c-list--m-icon-lg__item-icon-MinWidth - margin-inline-start: calc(var(--pf-v5-global--spacer--lg) - var(--pf-v5-global--spacer--sm) - var(--pf-v5-global--icon--FontSize--sm)); -} - -.expandable-actions > div { - margin-block-start: 0; -} - -// Using a "selected-product" CSS class because sadly we cannot use -// ".pf-v5-c-card:has(> input[type="radio"]:checked)" yet -// -// See: -// - https://drafts.csswg.org/selectors/#relational -// - https://caniuse.com/css-has -.pf-v5-c-card.selected-product { - --pf-v5-c-card--BoxShadow: var(--pf-v5-global--BoxShadow--md); - - .pf-v5-c-radio { - // https://drafts.csswg.org/css-ui/#widget-accent - // https://caniuse.com/mdn-css_properties_accent-color - accent-color: var(--color-primary-darkest); - } - - .pf-v5-c-radio__label { - color: var(--color-primary); - font-weight: bold; +// Better alignment for expandable section with a sibling list +ul.pf-v5-c-list + div.pf-v5-c-expandable-section { + > button { + margin-inline-start: calc(var(--pf-v5-global--spacer--lg) - var(--pf-v5-global--spacer--sm) - var(--pf-v5-global--icon--FontSize--sm)); } - .pf-v5-c-radio__description { - color: var(--color-primary-darkest); + > div { + margin-block-start: 0; } } @@ -56,97 +22,6 @@ button.remove-link:hover { color: var(--pf-v5-c-button--m-danger--BackgroundColor); } - -button.hidden-popover-button { - visibility: hidden; - display: inline; -} - -.wifi-network-menu button.pf-v5-c-dropdown__toggle { - padding-right: 0; -} - -.keep-words { - word-break: keep-all; -} - -button.kebab-toggler { - padding-right: 0; - - svg { - vertical-align: middle; - } -} - -.volumes-list { - .pf-v5-c-label { - margin-inline-end: 5px; - } -} - -.pattern-container { - display: grid; - grid-template-columns: 16px auto; - grid-template-rows: auto auto; - gap: 0.2em 1em; - grid-auto-flow: row; - grid-template-areas: - "checkbox label" - "empty summary"; - margin-bottom: 1em; - padding: 0.5em; - border-radius: 5px; -} - -.pattern-container:hover { - background-color: #eee; -} - -.pattern-label { - display: grid; - grid-template-columns: 32px auto; - grid-template-rows: auto; - gap: 0 1em; - grid-auto-flow: row; - grid-template-areas: "label-icon label-text"; - grid-area: label; -} - -.pattern-label-icon { - grid-area: label-icon; - align-self: center; -} - -.pattern-label-text { - grid-area: label-text; - font-size: 110%; - font-weight: bold; - justify-self: start; - align-self: center; -} - -.pattern-summary { - grid-area: summary; - color: #666; -} - -.pattern-checkbox { - grid-area: checkbox; - justify-self: center; - align-self: center; -} - -.pattern-group-name { - font-size: 120%; -} - -.locale-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0 1em; - width: 100%; -} - .first-username-dropdown { position: absolute; width: 100%; diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index e0ec9c93b6..f580648b72 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -2,142 +2,6 @@ // In the future we might add different section layouts by using data-variant attribute // or similar strategy -// Custom selection list -.selection-list > * { - border: 1px solid #eee; - transition: - font-size 0.15s ease-in-out, - font-weight 0.25s ease-in-out, - margin-block 0.15s ease-in-out, - box-shadow 0.35s ease-in-out; - - margin-block-start: -2px; -} - -.selection-list .header { - border-block-end: 1px solid #eee; - padding: var(--spacer-normal); -} - -.selection-list .content { - padding: var(--spacer-normal); -} - -.selection-list [data-state="focused"] { - margin-block: 20px; - box-shadow: 0 0 6px rgb(0 0 0 / 16%), 0 6px 12px rgb(0 0 0 / 32%); -} - -.selection-list [data-state="unstyled"] { - border: 0; -} - -[data-type="agama/sidebar"] { - /** Override the header background, see styles/layout.scss */ - --agama-header-bg: var(--color-primary-lighter); - - position: absolute; - padding: 0; - right: 0; - z-index: 1000; - inline-size: 70%; - min-inline-size: min-content; - box-shadow: -10px 10px 20px 0 var(--color-primary); - - header { - --focus-color: var(--color-primary-darkest); - } - - footer { - border-top: 1px solid var(--color-gray); - } - - a, button { - font-size: 16px; - font-weight: var(--fw-bold); - text-decoration: underline; - text-underline-offset: 2px; - padding-block: 0; - - &:hover { - color: var(--color-link-hover); - text-decoration: underline; - - svg { - color: var(--color-link); - } - } - - svg { - color: var(--color-link); - vertical-align: text-bottom; - margin-block-end: -2px; - } - } - - a { - margin-inline-start: var(--pf-v5-global--spacer--md); - - // Keep links and buttons labels aligned by adding the same margin than - // .pf-v5-c-button__icon.pf-m-start - svg { - margin-inline-end: var(--pf-v5-global--spacer--xs); - } - } - - // Remove not wanted PatternFly padding left on a loading link - button.pf-m-progress { - --pf-v5-c-button--m-progress--PaddingLeft: var(--pf-v5-global--spacer--md); - } - - button.pf-m-progress + div { - padding-inline-start: calc(var(--pf-v5-global--spacer--md)); - } - - &[data-state="hidden"] { - transition: all 0.04s ease-in-out; - inline-size: 0; - min-inline-size: 0; - box-shadow: none; - } - - &[data-state="visible"] { - transition: all 0.2s ease-in-out; - } -} - - -.disclosure > button { - margin-inline-start: var(--pf-v5-global--spacer--md); - display: inline-flex; - align-items: center; - // Keep links and buttons labels aligned by adding the same margin than - // .pf-v5-c-button__icon.pf-m-start - svg { - margin-inline-end: var(--pf-v5-global--spacer--xs); - transition: transform 0.2s ease-in-out; - } - - &[aria-expanded="true"] { - svg { - transform: rotate(90deg); - } - } - - &[aria-expanded="false"] + div { - display: none; - visibility: hidden; - } -} - -.disclosure > div { - margin-inline-start: calc( - var(--pf-v5-global--spacer--md) + 12px // half of the icon size; - ); - border-inline-start: 1px solid var(--color-primary-lighter); - padding-block: var(--spacer-small); -} - // raw file content with formatting similar to
     .filecontent {
       font-family: var(--ff-code);
    @@ -359,72 +223,6 @@ ul[data-items-type="agama/patterns"] {
       }
     }
     
    -[data-type="agama/tag"] {
    -  font-size: var(--fs-small);
    -
    -  &[data-variant="teal"] {
    -    color: var(--color-teal);
    -  }
    -
    -  &[data-variant="orange"] {
    -    color: var(--color-orange);
    -  }
    -
    -  &[data-variant="gray-highlight"] {
    -    padding: var(--spacer-smaller);
    -    color: var(--color-gray-darkest);
    -    background: var(--color-gray);
    -    border: 1px solid var(--color-gray-dark);
    -    border-radius: 5px;
    -    margin-inline-start: var(--spacer-smaller);
    -  }
    -}
    -
    -[data-type="agama/controlled-panels"] {
    -  [data-type="agama/option"] {
    -    label, input {
    -      cursor: pointer;
    -    }
    -
    -    label {
    -      display: flex;
    -      gap: var(--spacer-smaller);
    -    }
    -  }
    -
    -  [data-variant="buttons"] {
    -    input { position: absolute; opacity: 0 }
    -
    -    label {
    -      border: 1px solid var(--color-primary);
    -      padding: var(--spacer-small);
    -      gap: var(--spacer-small);
    -      border-radius: var(--spacer-smaller);
    -      position: relative;
    -
    -      &:has(input:checked) {
    -        background: var(--color-primary);
    -        color: white;
    -      }
    -
    -      &:has(input:focus-visible) {
    -        // outline: 1px dotted;
    -        // outline-offset: 0.25rem;
    -        box-shadow: 0 0 0 3px var(--focus-color);
    -      }
    -    }
    -
    -    [data-type="agama/option"]:not(:last-child) {
    -      border-inline-end: 2px solid var(--color-gray-darker);
    -      padding-inline-end: var(--spacer-small);
    -    }
    -  }
    -
    -  > div[aria-expanded="false"] {
    -    display: none;
    -  }
    -}
    -
     table[data-type="agama/tree-table"] {
       th:first-child {
         padding-inline-end: var(--spacer-normal);
    @@ -556,70 +354,6 @@ table.proposal-result {
       }
     }
     
    -[data-type="agama/options-picker"] {
    -  display: grid;
    -  grid-template-columns: repeat(4, 1fr);
    -  gap: var(--spacer-smaller);
    -
    -  [role="option"] {
    -    cursor: pointer;
    -    border: 1px solid var(--color-gray);
    -    padding: var(--spacer-small);
    -    border-block-end-width: 4px;
    -
    -    &[aria-selected="true"] {
    -      background: var(--color-gray-light);
    -      border-color: var(--color-primary);
    -    }
    -
    -    >:first-child {
    -      margin-block-end: var(--spacer-small);
    -    }
    -
    -    >:last-child {
    -      font-size: var(--fs-small);
    -    }
    -  }
    -}
    -
    -[data-type="agama/reminder"] {
    -  --accent-color: var(--color-primary-lighter);
    -  --inline-margin: calc(var(--header-icon-size) + var(--spacer-small));
    -
    -  display: flex;
    -  gap: var(--spacer-small);
    -  margin-inline: var(--inline-margin);
    -  margin-block-end: var(--spacer-normal);
    -  padding: var(--spacer-smaller) var(--spacer-small);
    -  border-inline-start: 3px solid var(--accent-color);
    -
    -  svg {
    -    fill: var(--accent-color);
    -  }
    -
    -  h4 {
    -    color: var(--accent-color);
    -  }
    -
    -  h4 ~ * {
    -    margin-block-start: var(--spacer-small);
    -  }
    -}
    -
    -section [data-type="agama/reminder"] {
    -  margin-inline: 0;
    -}
    -
    -[data-type="agama/reminder"][data-variant="subtle"] {
    -  --accent-color: var(--color-primary);
    -  padding-block: 0;
    -  border-inline-start-width: 1px;
    -
    -  h4 {
    -    font-size: var(--fs-normal);
    -  }
    -}
    -
     [role="dialog"] {
       section:not([class^="pf-c"]) {
         > svg:first-child {
    @@ -633,20 +367,6 @@ section [data-type="agama/reminder"] {
       }
     }
     
    -.tpm-hint {
    -  container-type: inline-size;
    -  container-name: tpm-info;
    -  text-align: start;
    -
    -  .pf-v5-c-alert__title {
    -    margin-block-end: var(--spacer-small);
    -  }
    -
    -  .pf-v5-c-alert__description {
    -    max-inline-size: 100%;
    -  }
    -}
    -
     [data-type="agama/expandable-selector"] {
       // The expandable selector is built on top of PF/Table#expandable
       // Let's tweak some styles
    @@ -669,87 +389,3 @@ section [data-type="agama/reminder"] {
         }
       }
     }
    -
    -[data-type="agama/field"] {
    -  > div:first-child {
    -    font-size: var(--fs-large);
    -
    -    button {
    -      padding-inline: 0;
    -    }
    -
    -    button:hover {
    -      color: var(--color-link-hover);
    -      fill: var(--color-link-hover);
    -    }
    -
    -    button b, button:hover b {
    -      text-decoration: underline;
    -      text-underline-offset: var(--spacer-smaller);
    -    }
    -
    -    div.pf-v5-c-skeleton {
    -      display: inline-block;
    -      vertical-align: middle;
    -      height: 1.5ex;
    -    }
    -  }
    -
    -  > div:nth-child(n+2) {
    -    margin-inline-start: calc(var(--icon-size-s) + 1ch);
    -  }
    -
    -  > div:nth-child(2) {
    -    color: gray;
    -    font-size: var(--fs-medium);
    -  }
    -
    -  > div:nth-child(n+3) {
    -    margin-block-start: var(--spacer-small);
    -  }
    -
    -  &.highlighted > div:last-child {
    -    --spacing: calc(var(--icon-size-s) / 2);
    -    margin-inline: var(--spacing);
    -    padding-inline: var(--spacing);
    -    border-inline-start: 2px solid;
    -  }
    -
    -  &.highlighted.on > div:last-child {
    -    border-color: var(--color-link-hover);
    -  }
    -
    -  &.highlighted.off > div:last-child {
    -    border-color: var(--color-gray-darker);
    -  }
    -
    -  &.on {
    -    button:not(.password-toggler) {
    -      fill: var(--color-link-hover);
    -    }
    -  }
    -
    -  hr {
    -    margin-block: var(--spacer-normal);
    -    border: 0;
    -    border-bottom: thin dashed var(--color-gray);
    -  }
    -}
    -
    -[data-type="agama/field"] button.pf-v5-c-menu-toggle.pf-m-plain {
    -  padding: 0;
    -}
    -
    -[data-type="agama/field"] .pf-v5-c-menu__list {
    -  padding: calc(var(--spacer-smaller) / 2) 0;
    -  margin: 0;
    -}
    -
    -#boot-form {
    -  legend {
    -    label {
    -      font-size: var(--fs-large);
    -      font-weight: bold;
    -    }
    -  }
    -}
    diff --git a/web/src/assets/styles/composition.scss b/web/src/assets/styles/composition.scss
    index 09be3715ef..eebf2a6c1d 100644
    --- a/web/src/assets/styles/composition.scss
    +++ b/web/src/assets/styles/composition.scss
    @@ -32,13 +32,7 @@
       }
     }
     
    -[data-state="reversed"] {
    -  flex-direction: row-reverse;
    -}
    -
     body > div[inert],
    -body > div[aria-hidden="true"],
    -div[data-type="agama/page"] > [inert],
    -div[data-type="agama/page"] > [aria-hidden="true"] {
    +body > div[aria-hidden="true"] {
       filter: grayscale(1) blur(2px);
     }
    diff --git a/web/src/assets/styles/index.scss b/web/src/assets/styles/index.scss
    index 9c4e142efa..e97db6b414 100644
    --- a/web/src/assets/styles/index.scss
    +++ b/web/src/assets/styles/index.scss
    @@ -6,7 +6,6 @@
     // TODO: merge app and global
     @use "~/assets/styles/global.scss";
     @use "~/assets/styles/app.scss";
    -@use "~/assets/styles/layout.scss";
     @use "~/assets/styles/utilities.scss";
     @use "~/assets/styles/composition.scss";
     @use "~/assets/styles/blocks.scss";
    diff --git a/web/src/assets/styles/layout.scss b/web/src/assets/styles/layout.scss
    deleted file mode 100644
    index d8a7034ad5..0000000000
    --- a/web/src/assets/styles/layout.scss
    +++ /dev/null
    @@ -1,84 +0,0 @@
    -@use "~/assets/styles/utilities.scss";
    -
    -[data-layout="agama/base"] {
    -  --agama-header-bg: var(--color-primary);
    -
    -  @extend .shadow;
    -  display: grid;
    -  block-size: 100dvh;
    -  background: white;
    -  overflow: hidden;
    -  grid-template-columns: 1fr;
    -  grid-template-rows: 60px 1fr 70px;
    -  grid-template-areas:
    -    "header"
    -    "body"
    -    "footer"
    -  ;
    -
    -  > * {
    -    padding: var(--spacer-small);
    -  }
    -
    -  > header {
    -    @extend .bottom-shadow;
    -    grid-area: header;
    -    display: flex;
    -    align-items: center;
    -    justify-content: space-between;
    -    background: var(--agama-header-bg);
    -    color: white;
    -    fill: white;
    -
    -    h1 {
    -      display: grid;
    -      align-items: center;
    -      gap: var(--spacer-small);
    -      grid-template-columns: var(--header-icon-size) 1fr;
    -      grid-template-areas:
    -        "icon text"
    -      ;
    -
    -      svg {
    -        grid-area: icon;
    -        block-size: var(--header-icon-size);
    -        inline-size: var(--header-icon-size);
    -      }
    -
    -      span {
    -        grid-area: text;
    -      }
    -    }
    -  }
    -
    -  main {
    -    grid-area: body;
    -    overflow: auto;
    -    padding-block: var(--spacer-normal);
    -    container-type: inline-size;
    -    container-name: agama-page-content;
    -  }
    -
    -  footer {
    -    grid-area: footer;
    -    @extend .top-shadow;
    -    display: flex;
    -    flex-direction: row-reverse;
    -    justify-content: space-between;
    -    align-items: center;
    -
    -    img {
    -      inline-size: 30%;
    -      max-inline-size: 150px;
    -    }
    -  }
    -}
    -
    -[data-type="agama/header-actions"] {
    -  display: flex;
    -  gap: var(--spacer-small);
    -}
    -
    -[data-variant="flip-X"] {
    -  transform: scaleX(-1);
    -}
    diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss
    index da930f63e1..4cf2b70cd6 100644
    --- a/web/src/assets/styles/patternfly-overrides.scss
    +++ b/web/src/assets/styles/patternfly-overrides.scss
    @@ -67,25 +67,6 @@
       --pf-v5-c-button--m-secondary--Color: var(--color-link-hover);
     }
     
    -// Force a separation between PF/EmptyStateBody paragraph without needing
    -// either: add the .pf-v5-c-content class nor wrapping PF/Text into
    -// PF/TextContent
    -.pf-v5-c-empty-state__body p:not(:last-child) {
    -  margin-block-end: var(--pf-v5-global--spacer--md);
    -}
    -
    -// Do not add block padding to empty state inside a table/column
    -table td > .pf-v5-c-empty-state {
    -  --pf-v5-c-empty-state--PaddingTop: 0;
    -  --pf-v5-c-empty-state--PaddingBottom: 0;
    -}
    -
    -// Fix single-line sub-progress miss-alignment
    -.pf-v5-c-progress.pf-m-singleline .pf-v5-c-progress__bar {
    -  grid-row: 1/3;
    -  grid-column: 1/3;
    -}
    -
     .pf-v5-c-modal-box__body {
       padding-block: var(--pf-v5-c-modal-box__body--PaddingTop);
     }
    diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss
    index 9e8030b5e9..980c327205 100644
    --- a/web/src/assets/styles/utilities.scss
    +++ b/web/src/assets/styles/utilities.scss
    @@ -1,7 +1,3 @@
    -.justify-between {
    -  justify-content: space-between;
    -}
    -
     // Sadly, Firefox does not support :has pseudo-selector yet.
     // See @components/layout/Center documentation.
     //
    @@ -19,25 +15,6 @@
       inline-size: 100%;
     }
     
    -.horizontally-centered {
    -  inline-size: 100%;
    -  margin-inline: 0 auto;
    -  text-align: center;
    -}
    -
    -.title {
    -  font-size: var(--fs-large);
    -  font-weight: var(--fw-bold);
    -}
    -
    -.bold {
    -  font-weight: bold;
    -}
    -
    -.fs-small {
    -  font-size: var(--fs-small);
    -}
    -
     // Utility classes for sizing icons
     .icon-xxxs {
       block-size: var(--icon-size-xxxs);
    @@ -74,10 +51,6 @@
       inline-size: var(--icon-size-xxxl);
     }
     
    -.color-warn {
    -  color: var(--color-warn);
    -}
    -
     .color-success {
       color: var(--color-success);
       fill: var(--color-success);
    @@ -87,85 +60,10 @@
       inline-size: 100%;
     }
     
    -.full-size {
    -  width: 100%;
    -  height: 100%;
    -}
    -
    -.transform-on-hover {
    -  transition: all 0.2s ease-in-out;
    -
    -  &:hover {
    -    transform: scale(1.4);
    -    color: var(--color-primary-darkest);
    -  }
    -}
    -
    -.gradient-border-bottom {
    -  border-bottom: 1px solid #efefef;
    -  border-image: linear-gradient(
    -    var(--gradient-border-angle),
    -    var(--gradient-border-start-color),
    -    var(--gradient-border-end-color)
    -  ) 1;
    -}
    -
    -.visually-hidden {
    -  border: 0;
    -  clip: rect(0 0 0 0);
    -  height: auto;
    -  margin: 0;
    -  overflow: hidden;
    -  padding: 0;
    -  position: absolute;
    -  width: 1px;
    -  white-space: nowrap;
    -}
    -
    -.shadow {
    -  box-shadow: 0 0 10px 0 var(--color-gray-darker);
    -}
    -
    -.top-shadow {
    -  box-shadow: 0 5px 10px 0 var(--color-gray-darker);
    -}
    -
    -.bottom-shadow {
    -  box-shadow: 0 -3px 10px 0 var(--color-gray-darker);
    -}
    -
    -.plain-control {
    -  position: relative;
    -  background: none;
    -  border: none;
    -}
    -
    -.plain-button {
    -  border: none;
    -  background: none;
    -  color: inherit;
    -  font: inherit;
    -  padding: 0;
    -  text-align: start;
    -}
    -
    -.inline-flex-button{
    -  @extend .plain-button;
    -  display: inline-flex;
    -  align-items: center;
    -  gap: 0.7ch;
    -  text-decoration: underline;
    -}
    -
    -.p-0 {
    -  padding: 0;
    -}
    -
     .block-size-auto {
       block-size: auto;
     }
     
    -
     .inline-size-auto {
       inline-size: auto;
     }
    @@ -218,17 +116,6 @@
       }
     }
     
    -.large {
    -  /** block-size fallbacks **/
    -  height: 95dvh;
    -  width: 95dvw;
    -  max-width: calc(var(--ui-max-inline-size) + var(--spacer-large));
    -  /** END block-size fallbacks **/
    -  block-size: 95dvh;
    -  inline-size: 95dvw;
    -  max-inline-size: calc(var(--ui-max-inline-size) + var(--spacer-large))
    -}
    -
     .scrollbox {
       background:
         linear-gradient(#fff 33%, rgb(255 255 255 / 0%)),
    @@ -241,10 +128,6 @@
       background-size: 100% 48px, 100% 48px, 100% 16px, 100% 16px;
     }
     
    -.height-75 {
    -  height: 75dvh;
    -}
    -
     // FIXME: drop as soon as Tip component gets rethought / refactored
     .label-tip .pf-v5-c-label__text {
       display: flex;
    diff --git a/web/src/assets/styles/variables.scss b/web/src/assets/styles/variables.scss
    index 7c79ff96c2..7e35e4e433 100644
    --- a/web/src/assets/styles/variables.scss
    +++ b/web/src/assets/styles/variables.scss
    @@ -40,49 +40,19 @@
       --icon-size-xxl: 5rem;
       --icon-size-xxxl: 10rem;
     
    -  --wrapper-padding: var(--spacer-small);
    -  --wrapper-background: white;
    -
       --color-primary: #0c322c;
    -  --color-primary-lighter: #30ba78;
       --color-gray-light: #fcfcfc;
       --color-gray: #f2f2f2;
       --color-gray-dark: #efefef; // Fog
    -  --color-gray-darker: #999;
    -  --color-gray-darkest: #333;
       --color-gray-dimmed: #888;
    -  --color-gray-dimmest: #666;
    -  --color-teal: #279c9c;
    -  --color-blue: #0d4ea6;
    -  --color-orange: #e86427;
    +  --color-success: #30ba78;
     
       --color-link: #0c322c;
       --color-link-hover: #30ba78;
    -
       --color-button-primary: var(--color-link);
       --color-button-primary-hover: var(--color-link-hover);
    -
       --color-button-plain-link: var(--color-link);
       --color-button-plain-link-hover: var(--color-link-hover);
     
    -  --color-background-primary: var(--color-primary);
    -  --color-background-secondary: var(--color-gray-dark);
    -
    -  --color-text-primary: var(--color-primary);
    -  --color-text-secondary: var(--color-gray-dark);
    -
    -  --color-success: #30ba78;
    -  --color-warn: #d4351c; // #FE7C3F; // Persimmon
    -
       --focus-color: #00b2e2; //cerulean 500
    -
    -  --gradient-border-angle: 45deg;
    -  --gradient-border-start-color: var(--color-gray);
    -  --gradient-border-end-color: transparent;
    -
    -  --header-icon-size: var(--icon-size-m);
    -  --section-icon-size: var(--icon-size);
    -
    -  --header-block-size: auto;
    -  --footer-block-size: auto;
     }
    diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx
    index 46f7cd9770..32c256aa46 100644
    --- a/web/src/components/core/EmptyState.jsx
    +++ b/web/src/components/core/EmptyState.jsx
    @@ -22,7 +22,7 @@
     // @ts-check
     
     import React from "react";
    -import { EmptyState, EmptyStateHeader, EmptyStateBody } from "@patternfly/react-core";
    +import { EmptyState, EmptyStateHeader, EmptyStateBody, Flex } from "@patternfly/react-core";
     import { Icon } from "~/components/layout";
     
     /**
    @@ -42,10 +42,10 @@ import { Icon } from "~/components/layout";
      * @param {string} props.title
      * @param {IconName} props.icon
      * @param {string} props.color
    - * @param {Pick} [props.headingLevel="h4"]
    + * @param {EmptyStateHeaderProps["headingLevel"]} [props.headingLevel="h4"]
      * @param {boolean} [props.noPadding=false]
      * @param {React.ReactNode} props.children
    - * @param {EmptyStateProps} [props.props]
    + * @param {EmptyStateProps} [props.rest]
      * @todo write documentation
      */
     export default function EmptyStateWrapper({
    @@ -55,12 +55,12 @@ export default function EmptyStateWrapper({
       headingLevel = "h4",
       noPadding = false,
       children,
    -  ...props
    +  ...rest
     }) {
    -  if (noPadding) props.className = [props.className, 'no-padding'].join(" ").trim();
    +  if (noPadding) rest.className = [rest.className, 'no-padding'].join(" ").trim();
     
       return (
    -    
    +    
           }
           />
           
    -        {children}
    +        
    +          {children}
    +        
           
         
       );
    diff --git a/web/src/components/core/FileViewer.jsx b/web/src/components/core/FileViewer.jsx
    deleted file mode 100644
    index 8fa58990aa..0000000000
    --- a/web/src/components/core/FileViewer.jsx
    +++ /dev/null
    @@ -1,85 +0,0 @@
    -/*
    - * Copyright (c) [2023] SUSE LLC
    - *
    - * All Rights Reserved.
    - *
    - * This program is free software; you can redistribute it and/or modify it
    - * under the terms of version 2 of the GNU General Public License as published
    - * by the Free Software Foundation.
    - *
    - * This program is distributed in the hope that it will be useful, but WITHOUT
    - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    - * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
    - * more details.
    - *
    - * You should have received a copy of the GNU General Public License along
    - * with this program; if not, contact SUSE LLC.
    - *
    - * To contact SUSE LLC about this file by physical or electronic mail, you may
    - * find current contact information at www.suse.com.
    - */
    -
    -import React, { useState, useEffect } from "react";
    -import { Popup } from "~/components/core";
    -import { Alert } from "@patternfly/react-core";
    -import { Loading } from "~/components/layout";
    -import { _ } from "~/i18n";
    -
    -import cockpit from "../../lib/cockpit";
    -
    -export default function FileViewer({ file, title, onCloseCallback }) {
    -  // the popup is visible
    -  const [isOpen, setIsOpen] = useState(true);
    -  // error message for failed load
    -  const [error, setError] = useState(null);
    -  // the file content
    -  const [content, setContent] = useState("");
    -  // current state
    -  const [state, setState] = useState("loading");
    -
    -  useEffect(() => {
    -    // NOTE: reading non-existing files in cockpit does not fail, the result is null
    -    // see https://cockpit-project.org/guide/latest/cockpit-file
    -    cockpit.file(file).read()
    -      .then((data) => {
    -        setState("ready");
    -        setContent(data);
    -      })
    -      .catch((data) => {
    -        setState("ready");
    -        setError(data.message);
    -      });
    -  }, [file]);
    -
    -  const close = () => {
    -    setIsOpen(false);
    -    if (onCloseCallback) onCloseCallback();
    -  };
    -
    -  return (
    -    
    -      {state === "loading" && }
    -      {(content === null || error) &&
    -        
    -          {error}
    -        }
    -      
    - {content} -
    - - - {_("Close")} - -
    - ); -} diff --git a/web/src/components/core/FileViewer.test.jsx b/web/src/components/core/FileViewer.test.jsx deleted file mode 100644 index 6c9b3972d2..0000000000 --- a/web/src/components/core/FileViewer.test.jsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; - -import { screen, waitFor, within } from "@testing-library/react"; -import { mockGettext, plainRender } from "~/test-utils"; -import { FileViewer } from "~/components/core"; -import cockpit from "../../lib/cockpit"; - -jest.mock("../../lib/cockpit"); - -const readFn = jest.fn(() => new Promise(jest.fn())); - -const fileFn = jest.fn(); -fileFn.mockImplementation(() => { - return { - read: readFn - }; -}); - -cockpit.file.mockImplementation(fileFn); - -// testing data -const file_name = "/testfile"; -const content = "Read file content"; -const title = "YaST Logs"; - -mockGettext(); - -describe("FileViewer", () => { - beforeEach(() => { - readFn.mockResolvedValue(content); - }); - - it("displays the specified file and the title", async () => { - plainRender(); - const dialog = await screen.findByRole("dialog"); - - // the file was read from cockpit - expect(fileFn).toHaveBeenCalledWith(file_name); - expect(readFn).toHaveBeenCalled(); - - within(dialog).getByText(title); - within(dialog).getByText(content); - }); - - it("displays the file name when the title is missing", async () => { - plainRender(); - const dialog = await screen.findByRole("dialog"); - - within(dialog).getByText(file_name); - }); - - it("closes the popup after clicking the close button", async () => { - const { user } = plainRender(); - const dialog = await screen.findByRole("dialog"); - const closeButton = within(dialog).getByRole("button", { name: /Close/i }); - - await user.click(closeButton); - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); - - it("triggers the onCloseCallback after clicking the close button", async () => { - const callback = jest.fn(); - const { user } = plainRender(); - const dialog = await screen.findByRole("dialog"); - const closeButton = within(dialog).getByRole("button", { name: /Close/i }); - - await user.click(closeButton); - - expect(callback).toHaveBeenCalled(); - }); - - describe("when the file does not exist", () => { - beforeEach(() => { - readFn.mockResolvedValue(null); - }); - - it("displays an error", async () => { - plainRender(); - const dialog = await screen.findByRole("dialog"); - - await within(dialog).findByText(/cannot read the file/i); - }); - }); - - describe("when the file cannot be read", () => { - beforeEach(() => { - readFn.mockRejectedValue(new Error("read error")); - }); - - it("displays the error message", async () => { - plainRender(); - const dialog = await screen.findByRole("dialog"); - - await within(dialog).findByText(/read error/i); - }); - }); -}); diff --git a/web/src/components/core/InstallationFinished.jsx b/web/src/components/core/InstallationFinished.jsx index c60b2943b3..a56ece90d5 100644 --- a/web/src/components/core/InstallationFinished.jsx +++ b/web/src/components/core/InstallationFinished.jsx @@ -99,13 +99,15 @@ function InstallationFinished() { icon={} /> - {_("The installation on your machine is complete.")} - - {usingIguana - ? _("At this point you can power off the machine.") - : _("At this point you can reboot the machine to log in to the new system.")} - - {usingTpm && } + + {_("The installation on your machine is complete.")} + + {usingIguana + ? _("At this point you can power off the machine.") + : _("At this point you can reboot the machine to log in to the new system.")} + + {usingTpm && } +
    diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.jsx index 6e2ed90862..a6c70af5a0 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.jsx @@ -30,7 +30,6 @@ import { PageGroup, PageSection, Stack } from "@patternfly/react-core"; -import { PageMenu } from "~/components/core"; import { _ } from "~/i18n"; import tabsStyles from '@patternfly/react-styles/css/components/Tabs/tabs'; import flexStyles from '@patternfly/react-styles/css/utilities/Flex/flex'; @@ -52,16 +51,6 @@ import flexStyles from '@patternfly/react-styles/css/utilities/Flex/flex'; */ const Actions = ({ children }) => <>{children}; -/** - * Component for rendering options related to the page. I.e., a menu. - * - * @note it is defined in its own file and then included here under Page.Menu - * "alias". - * - * @see core/PageMenu to know more. - */ -const Menu = PageMenu; - /** * A convenient component representing a Page action * @@ -180,47 +169,6 @@ const CardSection = ({ title, children, ...props }) => { * * * - * @example Using custom actions - * - * - * - * - * alert("Are you sure?")}> - * Reset to defaults - * - * Accept - * - * - * - * @example Using custom actions and a page menu - * - * - * - * - * - * Expert mode - * Help - * - * - * - * - * alert("Are you sure?")}> - * Reset to defaults - * - * Accept - * - * - * - * @example Using a page menu from external component - * ... - * import { UserPageMenu } from "somewhere"; - * ... - * - * - * - * - * - * * @param {object} props * @param {string} [props.icon] - The icon for the page. * @param {string} [props.title="Agama"] - The title for the page. By default it @@ -245,7 +193,6 @@ Page.CardSection = CardSection; Page.Actions = Actions; Page.NextActions = NextActions; Page.Action = Action; -Page.Menu = Menu; Page.MainContent = MainContent; Page.CancelAction = CancelAction; Page.Header = Header; diff --git a/web/src/components/core/PageMenu.jsx b/web/src/components/core/PageMenu.jsx deleted file mode 100644 index 608c9a0384..0000000000 --- a/web/src/components/core/PageMenu.jsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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 version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -import React, { useState } from 'react'; -import { - Dropdown, DropdownGroup, DropdownItem, DropdownList, - MenuToggle -} from '@patternfly/react-core'; -import { _ } from "~/i18n"; -import { Icon } from "~/components/layout"; - -/** - * Internal component to build the {PageMenu} toggler - * @component - * - * @typedef {object} TogglerBaseProps - * @property {React.Ref} toggleRef - * @property {string} label - * - * @typedef {TogglerBaseProps & import('@patternfly/react-core').MenuToggleProps} TogglerProps - * - * @param {TogglerProps} props - */ -const Toggler = ({ toggleRef, label, onClick, "aria-label": ariaLabel = _(("Show page menu")) }) => { - return ( - - {label} - - - ); -}; - -/** - * A group of actions belonging to a {PageMenu} component - * @component - * - * Built on top of {@link https://www.patternfly.org/components/menus/dropdown#dropdowngroup PF/DropdownGroup} - * - * @see {PageMenu} examples. - * - * @param {import('@patternfly/react-core').DropdownGroupProps} props - */ -const Group = ({ children, ...props }) => { - return ( - - {children} - - ); -}; - -/** - * An option belonging to a {PageMenu} component - * @component - * - * Built on top of {@link https://www.patternfly.org/components/menus/dropdown#dropdownitem PF/DropdownItem} - * - * @see {PageMenu} examples. - * - * @param {import('@patternfly/react-core').DropdownItemProps} props - */ -const Option = ({ children, ...props }) => { - return ( - - {children} - - ); -}; - -/** - * A collection of {Option}s belonging to a {PageMenu} component - * @component - * - * Built on top of {@link https://www.patternfly.org/components/menus/dropdown#dropdownlist PF/DropdownList} - * - * @see {PageMenu} examples. - * - * @param {import('@patternfly/react-core').DropdownListProps} props - */ -const Options = ({ children, ...props }) => { - return ( - - {children} - - ); -}; - -/** - * Component for rendering actions related to a page. - * @component - * - * It consist in a {@link https://www.patternfly.org/components/menus/dropdown PF/Dropdown} - * rendered in the header close to the action for opening the Sidebar. - * - * @note when wrapping it in another component intended to hold all the needed - * logic for building the page menu, it's name must includes the "PageMenu" suffix. - * This is needed to allow core/Page properly work with it. See core/Page component - * for a better understanding. - * - * @see core/Page component. - * - * @example Usage example - * - * - * - * - * Reprobe - * - * - * - * - * - * DASD - * - * - * iSCSI - * - * - * - * - * - * @typedef {object} PageMenuProps - * @property {string} [togglerAriaLabel] - * @property {string} label - * @property {React.ReactNode} children - * - * @param {PageMenuProps} props - */ -const PageMenu = ({ togglerAriaLabel, label, children }) => { - const [isOpen, setIsOpen] = useState(false); - - const toggle = () => setIsOpen(!isOpen); - const close = () => setIsOpen(false); - - return ( - } - onSelect={close} - onOpenChange={close} - popperProps={{ minWidth: "150px", position: "right" }} - data-type="agama/page-menu" - > - - {children} - - - ); -}; - -PageMenu.Group = Group; -PageMenu.Options = Options; -PageMenu.Option = Option; - -export default PageMenu; diff --git a/web/src/components/core/PageMenu.test.jsx b/web/src/components/core/PageMenu.test.jsx deleted file mode 100644 index a20bbf27e5..0000000000 --- a/web/src/components/core/PageMenu.test.jsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen, waitForElementToBeRemoved } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import { PageMenu } from "~/components/core"; - -it("renders the component initially closed", async () => { - plainRender( - - A dummy action - - ); - - screen.getByRole("button", { name: "Testing menu" }); - expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); -}); - -it("shows and hides the component content on user request", async () => { - const { user } = plainRender( - - <>A dummy action - - ); - - const toggler = screen.getByRole("button", { name: "Menu toggler" }); - - expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); - - await user.click(toggler); - - screen.getByRole("menuitem", { name: "A dummy action" }); - - await user.click(toggler); - await waitForElementToBeRemoved(screen.queryByRole("menuitem", { name: "A dummy action" })); - // NOTE: the above is the same than: - // await waitFor(() => { - // expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); - // }); -}); - -it("hides the component content when user clicks on one of its actions", async () => { - const { user } = plainRender( - - - <>Section - <>Page - - <>Exit - - ); - - const toggler = screen.getByRole("button"); - await user.click(toggler); - const action = screen.getByRole("menuitem", { name: "Section" }); - await user.click(action); - - expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); -}); - -it('closes the component when user clicks outside', async () => { - const { user } = plainRender( - <> -
    Sibling
    - - <>Option 1 - <>Option 2 - - - ); - - const toggler = screen.getByRole("button"); - const sibling = screen.getByText("Sibling"); - - // Open the dropdown - await user.click(toggler); - - // Ensure the dropdown is open - screen.getByRole("menuitem", { name: "Option 2" }); - - // Click outside the dropdown - await user.click(sibling); - - // Ensure the dropdown is closed - await waitForElementToBeRemoved(() => screen.queryByRole("menuitem", { name: "Option 2" })); -}); diff --git a/web/src/components/core/Popup.jsx b/web/src/components/core/Popup.jsx index e16fc38413..5779d2fb80 100644 --- a/web/src/components/core/Popup.jsx +++ b/web/src/components/core/Popup.jsx @@ -23,15 +23,9 @@ import React from "react"; import { Button, Modal } from "@patternfly/react-core"; +import { Loading } from "~/components/layout"; import { _ } from "~/i18n"; import { partition } from "~/utils"; -import { Loading } from "~/components/layout"; - -/** - * @typedef {import("@patternfly/react-core").ModalProps} ModalProps - * @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps - * @typedef {Omit} ButtonWithoutVariantProps - */ /** * @typedef {import("@patternfly/react-core").ModalProps} ModalProps diff --git a/web/src/components/core/Selector.jsx b/web/src/components/core/Selector.jsx deleted file mode 100644 index 5e5763e6e1..0000000000 --- a/web/src/components/core/Selector.jsx +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check -import React from "react"; -import { _ } from "~/i18n"; -import { noop } from "~/utils"; - -/** - * @callback onSelectionChangeCallback - * @param {Array} selection - ids of selected options - */ - -/** - * Agama component for building a selector - * - * @example Usage example - * const options = [ - * { id: "es_ES", country: "Spain", label: "Spanish" }, - * { id: "cs_CZ", country: "Czechia", label: "Czech" }, - * { id: "de_DE", country: "Germany", label: "German" }, - * { id: "en_GB", country: "United Kingdom", label: "English" } - * ]; - * - * const selectedIds = ["es_ES", "en_GB"]; - * - * const renderFn = ({ label, country }) =>
    {label} - {country}
    ; - * - * return ( - * changePreferredLocales(selection)} - * /> - * ); - * - * @param {object} props - component props - * @param {string} [props.id] - Id attribute for selector. - * @param {boolean} [props.isMultiple=false] - Whether the selector should allow multiple selection. - * @param {Array} props.options=[] - Item objects to build options. - * @param {function} props.renderOption=noop - Function used for rendering options. - * @param {string} [props.optionIdKey="id"] - Key used for retrieve options id. - * @param {Array<*>} [props.selectedIds=[]] - Identifiers for selected options. - * @param {function} props.autoSelectionCheck=noop - Function used to check if option should be marked as auto selected. - * @param {onSelectionChangeCallback} [props.onSelectionChange=noop] - Callback to be called when the selection changes. - * @param {function|undefined} [props.onOptionClick] - Callback to be called when the selection changes. - * @param {object} [props.props] - Other props sent to the internal selector
      component - */ -const Selector = ({ - id = crypto.randomUUID(), - isMultiple = false, - options = [], - renderOption = noop, - optionIdKey = "id", - selectedIds = [], - autoSelectionCheck = noop, - onSelectionChange = noop, - onOptionClick, - ...props -}) => { - const onOptionClicked = (optionId) => { - if (typeof onOptionClick === "function") return onOptionClick(optionId); - - const alreadySelected = selectedIds.includes(optionId); - - if (!isMultiple) { - !alreadySelected && onSelectionChange([optionId]); - return; - } - - if (alreadySelected) { - onSelectionChange(selectedIds.filter((id) => id !== optionId)); - } else { - onSelectionChange([...selectedIds, optionId]); - } - }; - - return ( -
        - {options.map((option) => { - const optionId = option[optionIdKey]; - const optionHtmlId = `${id}-option-${optionId}`; - const isSelected = selectedIds.includes(optionId); - const isAutoSelected = isSelected && autoSelectionCheck(option); - const onClick = () => onOptionClicked(optionId); - - return ( -
      • -
        -
        - - {isAutoSelected &&
        {_("auto selected")}
        } -
        - {renderOption(option)} -
        -
      • - ); - })} -
      - ); -}; - -export default Selector; diff --git a/web/src/components/core/Selector.test.jsx b/web/src/components/core/Selector.test.jsx deleted file mode 100644 index 96fa778cf9..0000000000 --- a/web/src/components/core/Selector.test.jsx +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import { Selector } from "~/components/core"; - -const onChangeFn = jest.fn(); - -const TestingSelector = ({ isMultiple = false, selectedIds = ["es_ES"], ...props }) => { - const [selected, setSelected] = React.useState(selectedIds); - - onChangeFn.mockImplementation((selection) => setSelected(selection)); - - return ( -
      {option.label} - {option.country}
      } - selectedIds={selected} - onSelectionChange={onChangeFn} - aria-label="Testing selector" - { ...props } - /> - ); -}; - -const MultipleTestingSelector = (props) => ; - -describe("Selector", () => { - it("renders a selector and its options", () => { - plainRender(); - const selector = screen.getByRole("grid", { name: "Testing selector" }); - within(selector).getByRole("row", { name: "Spanish - Spain" }); - within(selector).getByRole("row", { name: "English - United Kingdom" }); - }); - - it("uses `id` as key for the option id if `optionIdKey` prop is not given", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - await user.click(option); - expect(onChangeFn).toHaveBeenCalledWith(["en_GB"]); - }); - - it("uses given `optionIdKey` as key for the option id", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - await user.click(option); - expect(onChangeFn).toHaveBeenCalledWith([2]); - }); - - it("sets data-auto-selected attribute to selected options for which `autoSelectionCheck` returns true", () => { - plainRender( - // Forcing a not selected option (en_GB) as auto selected for checking that it is not. - ["es_ES", "en_GB"].includes(o.id)} /> - ); - const spanishRow = screen.getByRole("row", { name: "Spanish - Spain auto selected" }); - const englishRow = screen.getByRole("row", { name: "English - United Kingdom" }); - const spanishOption = within(spanishRow).getByRole("radio"); - const englishOption = within(englishRow).getByRole("radio"); - expect(spanishRow).toHaveAttribute("data-auto-selected"); - expect(spanishOption).toHaveAttribute("data-auto-selected"); - expect(englishRow).not.toHaveAttribute("data-auto-selected"); - expect(englishOption).not.toHaveAttribute("data-auto-selected"); - }); - - describe("when set as single selector", () => { - it("renders a radio input for each option", () => { - plainRender(); - const selector = screen.getByRole("grid", { name: "Testing selector" }); - const options = within(selector).getAllByRole("row"); - options.forEach((option) => within(option).getByRole("radio")); - }); - - describe("and user clicks on a selected option", () => { - it("keeps it as selected and does not trigger the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(option).toHaveAttribute("aria-selected"); - await user.click(option); - expect(option).toHaveAttribute("aria-selected"); - expect(onChangeFn).not.toHaveBeenCalled(); - }); - }); - - describe("and user clicks a not selected option", () => { - it("sets it as selected and triggers the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const initialSelection = screen.getByRole("row", { name: "Spanish - Spain" }); - const nextSelection = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(initialSelection).toHaveAttribute("aria-selected"); - expect(nextSelection).not.toHaveAttribute("aria-selected"); - await user.click(nextSelection); - expect(initialSelection).not.toHaveAttribute("aria-selected"); - expect(nextSelection).toHaveAttribute("aria-selected"); - expect(onChangeFn).toHaveBeenCalledWith(["en_GB"]); - }); - }); - }); - - describe("when set as multiple selector", () => { - it("renders a checkbox input for each option", () => { - plainRender(); - const selector = screen.getByRole("grid", { name: "Testing selector" }); - const options = within(selector).getAllByRole("row"); - options.forEach((option) => within(option).getByRole("checkbox")); - }); - - describe("and user clicks on a selected option", () => { - it("sets it as not selected and triggers the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(option).toHaveAttribute("aria-selected"); - await user.click(option); - expect(option).not.toHaveAttribute("aria-selected"); - expect(onChangeFn).toHaveBeenCalledWith(expect.not.arrayContaining(["en_GB"])); - }); - }); - - describe("and user clicks on a not selected option", () => { - it("sets it as selected and triggers the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(option).not.toHaveAttribute("aria-selected"); - await user.click(option); - expect(option).toHaveAttribute("aria-selected"); - expect(onChangeFn).toHaveBeenCalledWith(expect.arrayContaining(["en_GB"])); - }); - }); - }); -}); diff --git a/web/src/components/core/ServerError.jsx b/web/src/components/core/ServerError.jsx index 27aafd7318..3452081129 100644 --- a/web/src/components/core/ServerError.jsx +++ b/web/src/components/core/ServerError.jsx @@ -23,35 +23,38 @@ import React from "react"; import { EmptyState, EmptyStateIcon, EmptyStateBody, EmptyStateHeader } from "@patternfly/react-core"; import { Center, Icon } from "~/components/layout"; import { Page } from "~/components/core"; +import SimpleLayout from "~/SimpleLayout"; import { _ } from "~/i18n"; import { locationReload } from "~/utils"; +// TODO: refactor, and if possible use a route for it. Related with needed +// changes in src/App.jsx + const ErrorIcon = () => ; function ServerError() { return ( - // TRANSLATORS: page title - -
      - - } - /> - - {_("Please, check whether it is running.")} - - -
      - - - locationReload()}> - {/* TRANSLATORS: button label */} + + +
      + + } + /> + + {_("Please, check whether it is running.")} + + +
      +
      + + {_("Reload")} -
      -
      + + ); } diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index 76354cc861..54ad24b00d 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -20,7 +20,6 @@ */ export { default as About } from "./About"; -export { default as PageMenu } from "./PageMenu"; export { default as Description } from "./Description"; export { default as Section } from "./Section"; export { default as FormLabel } from "./FormLabel"; @@ -38,7 +37,6 @@ export { default as SectionSkeleton } from "./SectionSkeleton"; export { default as ListSearch } from "./ListSearch"; export { default as LoginPage } from "./LoginPage"; export { default as LogsButton } from "./LogsButton"; -export { default as FileViewer } from "./FileViewer"; export { default as RowActions } from "./RowActions"; export { default as Page } from "./Page"; export { default as PasswordAndConfirmationInput } from "./PasswordAndConfirmationInput"; @@ -48,7 +46,6 @@ export { default as ProgressText } from "./ProgressText"; export { default as Tip } from "./Tip"; export { default as NumericTextInput } from "./NumericTextInput"; export { default as PasswordInput } from "./PasswordInput"; -export { default as Selector } from "./Selector"; export { default as ServerError } from "./ServerError"; export { default as ExpandableSelector } from "./ExpandableSelector"; export { default as TreeTable } from "./TreeTable"; diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index 3ecc03369b..086d6b60fe 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -171,6 +171,8 @@ const PREDEFINED_SIZES = [ * @param {IconName} props.name - Name of the desired icon. * @param {string} [props.className=""] - CSS classes. * @param {IconSize} [props.size] - Size used for both, width and height. + * @param {string} [props.color] - Color for the icon, currently based on PF + * text utils * It can be a CSS unit or one of PREDEFINED_SIZES. * @param {object} [props.otherProps] Other props sent to SVG icon. Please, note * that width and height will be overwritten by the size value if it was given. diff --git a/web/src/components/storage/ProposalActionsDialog.jsx b/web/src/components/storage/ProposalActionsDialog.jsx index 17bcd9484d..89e19d7a8e 100644 --- a/web/src/components/storage/ProposalActionsDialog.jsx +++ b/web/src/components/storage/ProposalActionsDialog.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -29,9 +29,11 @@ const ActionsList = ({ actions }) => { // Some actions (e.g., deleting a LV) are reported as several actions joined by a line break const actionItems = (action, id) => { return action.text.split("\n").map((text, index) => { + const Wrapper = action.delete ? "strong" : "span"; + return ( - - {text} + + {text} ); }); @@ -39,7 +41,7 @@ const ActionsList = ({ actions }) => { const items = actions.map(actionItems).flat(); - return {items}; + return {items}; }; /** @@ -72,7 +74,6 @@ export default function ProposalActionsDialog({ actions = [] }) { isExpanded={isExpanded} onToggle={() => setIsExpanded(!isExpanded)} toggleText={toggleText} - className="expandable-actions" > } From d29461af516819d5cf3e2e74158bd441190c8216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 11 Jul 2024 12:35:21 +0100 Subject: [PATCH 163/430] fix(web): update from code review --- web/src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/App.jsx b/web/src/App.jsx index 962996a1b1..81a7a8d03b 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -59,7 +59,7 @@ function App() { return ; } - if ((selectedProduct === undefined) & (location.pathname !== "/products")) { + if ((selectedProduct === undefined) && (location.pathname !== "/products")) { return ; } From d16b211c7f8b225e94ae95c855a3fdc17b01962d Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 11 Jul 2024 17:23:36 +0200 Subject: [PATCH 164/430] questions and ask CLI WIP --- rust/agama-cli/src/main.rs | 22 +++---- rust/agama-cli/src/questions.rs | 25 ++++---- rust/agama-lib/src/error.rs | 2 + rust/agama-lib/src/http_client.rs | 55 ++++++++++++++++++ rust/agama-lib/src/lib.rs | 1 + rust/agama-lib/src/questions.rs | 4 ++ rust/agama-lib/src/questions/http_client.rs | 19 +++++++ rust/agama-lib/src/questions/model.rs | 63 +++++++++++++++++++++ rust/agama-server/src/questions/web.rs | 63 +-------------------- rust/agama-server/src/web/docs.rs | 12 ++-- 10 files changed, 176 insertions(+), 90 deletions(-) create mode 100644 rust/agama-lib/src/http_client.rs create mode 100644 rust/agama-lib/src/questions/http_client.rs create mode 100644 rust/agama-lib/src/questions/model.rs diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index 1c0642eddc..b7891df3de 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -118,28 +118,28 @@ async fn build_manager<'a>() -> anyhow::Result> { Ok(ManagerClient::new(conn).await?) } -async fn run_command(cli: Cli) -> anyhow::Result<()> { - match cli.command { +async fn run_command(cli: Cli) -> Result<(), ServiceError> { + Ok(match cli.command { Commands::Config(subcommand) => { let manager = build_manager().await?; wait_for_services(&manager).await?; - run_config_cmd(subcommand).await + run_config_cmd(subcommand).await? } Commands::Probe => { let manager = build_manager().await?; wait_for_services(&manager).await?; - probe().await + probe().await? } - Commands::Profile(subcommand) => Ok(run_profile_cmd(subcommand).await?), + Commands::Profile(subcommand) => run_profile_cmd(subcommand).await?, Commands::Install => { let manager = build_manager().await?; - install(&manager, 3).await + install(&manager, 3).await? } - Commands::Questions(subcommand) => run_questions_cmd(subcommand).await, - Commands::Logs(subcommand) => run_logs_cmd(subcommand).await, - Commands::Auth(subcommand) => run_auth_cmd(subcommand).await, - Commands::Download { url } => crate::profile::download(&url, std::io::stdout()), - } + Commands::Questions(subcommand) => run_questions_cmd(subcommand).await?, + Commands::Logs(subcommand) => run_logs_cmd(subcommand).await?, + Commands::Auth(subcommand) => run_auth_cmd(subcommand).await?, + Commands::Download { url } => crate::profile::download(&url, std::io::stdout())?, + }) } /// Represents the result of execution. diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index f066452e22..9455b4180e 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -1,6 +1,5 @@ -use agama_lib::connection; +use agama_lib::{connection, error::ServiceError}; use agama_lib::proxies::Questions1Proxy; -use anyhow::Context; use clap::{Args, Subcommand, ValueEnum}; #[derive(Subcommand, Debug)] @@ -19,6 +18,8 @@ pub enum QuestionsCommands { /// Path to a file containing the answers in YAML format. path: String, }, + /// prints list of questions that is waiting for answer in YAML format + List, } #[derive(Args, Debug)] @@ -35,30 +36,32 @@ pub enum Modes { NonInteractive, } -async fn set_mode(proxy: Questions1Proxy<'_>, value: Modes) -> anyhow::Result<()> { - // TODO: how to print dbus error in that anyhow? +async fn set_mode(proxy: Questions1Proxy<'_>, value: Modes) -> Result<(), ServiceError> { proxy .set_interactive(value == Modes::Interactive) .await - .context("Failed to set mode for answering questions.") + .map_err(|e| e.into()) } -async fn set_answers(proxy: Questions1Proxy<'_>, path: String) -> anyhow::Result<()> { - // TODO: how to print dbus error in that anyhow? +async fn set_answers(proxy: Questions1Proxy<'_>, path: String) -> Result<(), ServiceError> { proxy .add_answer_file(path.as_str()) .await - .context("Failed to set answers from answers file") + .map_err(|e| e.into()) } -pub async fn run(subcommand: QuestionsCommands) -> anyhow::Result<()> { +async fn list_questions() -> Result<(), ServiceError> { + Ok(()) +} + +pub async fn run(subcommand: QuestionsCommands) -> Result<(), ServiceError> { let connection = connection().await?; let proxy = Questions1Proxy::new(&connection) - .await - .context("Failed to connect to Questions service")?; + .await?; match subcommand { QuestionsCommands::Mode(value) => set_mode(proxy, value.value).await, QuestionsCommands::Answers { path } => set_answers(proxy, path).await, + QuestionsCommands::List => list_questions().await, } } diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index 513381803a..d610d344e7 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -16,6 +16,8 @@ pub enum ServiceError { DBusProtocol(#[from] zbus::fdo::Error), #[error("Unexpected type on D-Bus '{0}'")] ZVariant(#[from] zvariant::Error), + #[error("Failed to communicate with HTTP backend '{0}'")] + HTTPError(#[from] reqwest::Error), // it's fine to say only "Error" because the original // specific error will be printed too #[error("Error: {0}")] diff --git a/rust/agama-lib/src/http_client.rs b/rust/agama-lib/src/http_client.rs new file mode 100644 index 0000000000..384f38c991 --- /dev/null +++ b/rust/agama-lib/src/http_client.rs @@ -0,0 +1,55 @@ +use anyhow::Context; +use reqwest::{header, Client, Response}; +use serde::de::DeserializeOwned; + +use crate::{auth::AuthToken, error::ServiceError}; + +pub struct HTTPClient { + client: Client, + pub base_url: String, +} + +const API_URL: &str = "http://localhost/api"; + +impl HTTPClient { + // if there is need for client without authorization, create new constructor for it + pub async fn new() -> Result { + let token = AuthToken::find().context("You are not logged in")?; + + let mut headers = header::HeaderMap::new(); + let value = header::HeaderValue::from_str(format!("Bearer {}", token).as_str()) + .map_err(|e| ServiceError::NetworkClientError(e.to_string()))?; + + headers.insert(header::AUTHORIZATION, value); + + let client = Client::builder() + .default_headers(headers) + .build()?; + + Ok(Self { + client, + base_url: API_URL.to_string(), // TODO: add support for remote server + }) + } + + // Simple wrapper around Response to get target type. + // For more advanced usage use directly get method + pub async fn get_type(&self, path: &str) -> Result { + let response = self.get(path).await?; + + response.json::().await.map_err(|e| e.into()) + } + + pub async fn get(&self, path: &str) -> Result { + self + .client + .get(self.target_path(path)) + .send() + .await + .map_err(|e| e.into()) + } + + fn target_path(&self, path: &str) -> String { + self.base_url.clone() + path + } +} \ No newline at end of file diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 6435517803..0137d3a1b8 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -26,6 +26,7 @@ pub mod auth; pub mod error; pub mod install_settings; +mod http_client; pub mod localization; pub mod manager; pub mod network; diff --git a/rust/agama-lib/src/questions.rs b/rust/agama-lib/src/questions.rs index b0b52d84de..ad5ce5a26a 100644 --- a/rust/agama-lib/src/questions.rs +++ b/rust/agama-lib/src/questions.rs @@ -1,8 +1,12 @@ //! Data model for Agama questions use std::collections::HashMap; +pub mod http_client; +pub mod model; /// Basic generic question that fits question without special needs +/// NOTE: structs below is for dbus usage and holds complete questions data +/// for user side data model see questions::model #[derive(Clone, Debug)] pub struct GenericQuestion { /// numeric id used to identify question on D-Bus diff --git a/rust/agama-lib/src/questions/http_client.rs b/rust/agama-lib/src/questions/http_client.rs new file mode 100644 index 0000000000..419aed396b --- /dev/null +++ b/rust/agama-lib/src/questions/http_client.rs @@ -0,0 +1,19 @@ +use crate::error::ServiceError; + +use super::model; + +struct HTTPClient { + client: crate::http_client::HTTPClient, +} + +impl HTTPClient { + pub async fn new() -> Result { + Ok(Self { + client: crate::http_client::HTTPClient::new().await? + }) + } + + pub async fn list_questions() -> Result, ServiceError> { + Ok(vec![]) + } +} \ No newline at end of file diff --git a/rust/agama-lib/src/questions/model.rs b/rust/agama-lib/src/questions/model.rs new file mode 100644 index 0000000000..07db655184 --- /dev/null +++ b/rust/agama-lib/src/questions/model.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Question { + pub generic: GenericQuestion, + pub with_password: Option, +} + +/// Facade of agama_lib::questions::GenericQuestion +/// For fields details see it. +/// Reason why it does not use directly GenericQuestion from lib +/// is that it contain both question and answer. It works for dbus +/// API which has both as attributes, but web API separate +/// question and its answer. So here it is split into GenericQuestion +/// and GenericAnswer +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct GenericQuestion { + pub id: u32, + pub class: String, + pub text: String, + pub options: Vec, + pub default_option: String, + pub data: HashMap, +} + +/// Facade of agama_lib::questions::WithPassword +/// For fields details see it. +/// Reason why it does not use directly WithPassword from lib +/// is that it is not composition as used here, but more like +/// child of generic question and contain reference to Base. +/// Here for web API we want to have in json that separation that would +/// allow to compose any possible future specialization of question. +/// Also note that question is empty as QuestionWithPassword does not +/// provide more details for question, but require additional answer. +/// Can be potentionally extended in future e.g. with list of allowed characters? +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct QuestionWithPassword {} + +#[derive(Default, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Answer { + pub generic: GenericAnswer, + pub with_password: Option, +} + +/// Answer needed for GenericQuestion +#[derive(Default, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct GenericAnswer { + pub answer: String, +} + +/// Answer needed for Password specific questions. +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PasswordAnswer { + pub password: String, +} \ No newline at end of file diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 19b4463091..edf36ad932 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -8,7 +8,7 @@ use crate::{error::Error, web::Event}; use agama_lib::{ error::ServiceError, - proxies::{GenericQuestionProxy, QuestionWithPasswordProxy, Questions1Proxy}, + proxies::{GenericQuestionProxy, QuestionWithPasswordProxy, Questions1Proxy}, questions::model::{Answer, GenericQuestion, PasswordAnswer, Question, QuestionWithPassword}, }; use anyhow::Context; use axum::{ @@ -19,7 +19,6 @@ use axum::{ Json, Router, }; use regex::Regex; -use serde::{Deserialize, Serialize}; use std::{collections::HashMap, pin::Pin}; use tokio_stream::{Stream, StreamExt}; use zbus::{ @@ -222,66 +221,6 @@ struct QuestionsState<'a> { questions: QuestionsClient<'a>, } -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Question { - generic: GenericQuestion, - with_password: Option, -} - -/// Facade of agama_lib::questions::GenericQuestion -/// For fields details see it. -/// Reason why it does not use directly GenericQuestion from lib -/// is that it contain both question and answer. It works for dbus -/// API which has both as attributes, but web API separate -/// question and its answer. So here it is split into GenericQuestion -/// and GenericAnswer -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct GenericQuestion { - id: u32, - class: String, - text: String, - options: Vec, - default_option: String, - data: HashMap, -} - -/// Facade of agama_lib::questions::WithPassword -/// For fields details see it. -/// Reason why it does not use directly WithPassword from lib -/// is that it is not composition as used here, but more like -/// child of generic question and contain reference to Base. -/// Here for web API we want to have in json that separation that would -/// allow to compose any possible future specialization of question. -/// Also note that question is empty as QuestionWithPassword does not -/// provide more details for question, but require additional answer. -/// Can be potentionally extended in future e.g. with list of allowed characters? -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct QuestionWithPassword {} - -#[derive(Default, Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Answer { - generic: GenericAnswer, - with_password: Option, -} - -/// Answer needed for GenericQuestion -#[derive(Default, Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct GenericAnswer { - answer: String, -} - -/// Answer needed for Password specific questions. -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct PasswordAnswer { - password: String, -} - /// Sets up and returns the axum service for the questions module. pub async fn questions_service(dbus: zbus::Connection) -> Result { let questions = QuestionsClient::new(dbus.clone()).await?; diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index dcaa1b0680..af9d28eacd 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -100,12 +100,12 @@ use utoipa::OpenApi; schemas(crate::manager::web::InstallerStatus), schemas(crate::network::model::Connection), schemas(crate::network::model::Device), - schemas(crate::questions::web::Answer), - schemas(crate::questions::web::GenericAnswer), - schemas(crate::questions::web::GenericQuestion), - schemas(crate::questions::web::PasswordAnswer), - schemas(crate::questions::web::Question), - schemas(crate::questions::web::QuestionWithPassword), + schemas(agama_lib::questions::model::Answer), + schemas(agama_lib::questions::model::GenericAnswer), + schemas(agama_lib::questions::model::GenericQuestion), + schemas(agama_lib::questions::model::PasswordAnswer), + schemas(agama_lib::questions::model::Question), + schemas(agama_lib::questions::model::QuestionWithPassword), schemas(crate::software::web::SoftwareConfig), schemas(crate::software::web::SoftwareProposal), schemas(crate::storage::web::ProductParams), From fb6b97ecfe42f9e70705dee634fef16688a09dde Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 12 Jul 2024 08:43:03 +0200 Subject: [PATCH 165/430] first working questions list command --- rust/agama-cli/src/questions.rs | 15 ++++++++++--- rust/agama-lib/src/error.rs | 2 ++ rust/agama-lib/src/http_client.rs | 24 +++++++++++++-------- rust/agama-lib/src/lib.rs | 2 +- rust/agama-lib/src/questions/http_client.rs | 11 +++++----- rust/agama-lib/src/questions/model.rs | 2 +- rust/agama-server/src/questions/web.rs | 3 ++- 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index 9455b4180e..b56099d2ce 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -1,5 +1,6 @@ -use agama_lib::{connection, error::ServiceError}; use agama_lib::proxies::Questions1Proxy; +use agama_lib::questions::http_client::HTTPClient; +use agama_lib::{connection, error::ServiceError}; use clap::{Args, Subcommand, ValueEnum}; #[derive(Subcommand, Debug)] @@ -51,13 +52,21 @@ async fn set_answers(proxy: Questions1Proxy<'_>, path: String) -> Result<(), Ser } async fn list_questions() -> Result<(), ServiceError> { + let client = HTTPClient::new().await?; + let questions = client.list_questions().await?; + // FIXME: that conversion to anyhow error is nasty, but we do not expect issue + // when questions are already read from json + // FIXME: if performance is bad, we can skip converting json from http to struct and then + // serialize it, but it won't be pretty string + let questions_json = + serde_json::to_string_pretty(&questions).map_err(Into::::into)?; + println!("{}", questions_json); Ok(()) } pub async fn run(subcommand: QuestionsCommands) -> Result<(), ServiceError> { let connection = connection().await?; - let proxy = Questions1Proxy::new(&connection) - .await?; + let proxy = Questions1Proxy::new(&connection).await?; match subcommand { QuestionsCommands::Mode(value) => set_mode(proxy, value.value).await, diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index d610d344e7..a2fd927195 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -35,6 +35,8 @@ pub enum ServiceError { UnsuccessfulAction(String), #[error("Unknown installation phase: '{0}")] UnknownInstallationPhase(u32), + #[error("Backend call failed with status '{0}' and text '{1}'")] + BackendError(u16, String), } #[derive(Error, Debug)] diff --git a/rust/agama-lib/src/http_client.rs b/rust/agama-lib/src/http_client.rs index 384f38c991..a538c52029 100644 --- a/rust/agama-lib/src/http_client.rs +++ b/rust/agama-lib/src/http_client.rs @@ -19,30 +19,36 @@ impl HTTPClient { let mut headers = header::HeaderMap::new(); let value = header::HeaderValue::from_str(format!("Bearer {}", token).as_str()) .map_err(|e| ServiceError::NetworkClientError(e.to_string()))?; - + headers.insert(header::AUTHORIZATION, value); - - let client = Client::builder() - .default_headers(headers) - .build()?; - + + let client = Client::builder().default_headers(headers).build()?; + Ok(Self { client, base_url: API_URL.to_string(), // TODO: add support for remote server }) } + const NO_TEXT: &'static str = "No text"; // Simple wrapper around Response to get target type. // For more advanced usage use directly get method pub async fn get_type(&self, path: &str) -> Result { let response = self.get(path).await?; + if !response.status().is_success() { + let code = response.status().as_u16(); + let text = response + .text() + .await + .unwrap_or_else(|_| Self::NO_TEXT.to_string()); + return Err(ServiceError::BackendError(code, text)); + } response.json::().await.map_err(|e| e.into()) } pub async fn get(&self, path: &str) -> Result { - self - .client + self.client .get(self.target_path(path)) .send() .await @@ -52,4 +58,4 @@ impl HTTPClient { fn target_path(&self, path: &str) -> String { self.base_url.clone() + path } -} \ No newline at end of file +} diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 0137d3a1b8..1fe1d7766b 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -25,8 +25,8 @@ pub mod auth; pub mod error; -pub mod install_settings; mod http_client; +pub mod install_settings; pub mod localization; pub mod manager; pub mod network; diff --git a/rust/agama-lib/src/questions/http_client.rs b/rust/agama-lib/src/questions/http_client.rs index 419aed396b..8cbe53fc41 100644 --- a/rust/agama-lib/src/questions/http_client.rs +++ b/rust/agama-lib/src/questions/http_client.rs @@ -2,18 +2,19 @@ use crate::error::ServiceError; use super::model; -struct HTTPClient { +pub struct HTTPClient { client: crate::http_client::HTTPClient, } impl HTTPClient { pub async fn new() -> Result { Ok(Self { - client: crate::http_client::HTTPClient::new().await? + client: crate::http_client::HTTPClient::new().await?, }) } - pub async fn list_questions() -> Result, ServiceError> { - Ok(vec![]) + pub async fn list_questions(&self) -> Result, ServiceError> { + let questions = self.client.get_type("/questions").await?; + Ok(questions) } -} \ No newline at end of file +} diff --git a/rust/agama-lib/src/questions/model.rs b/rust/agama-lib/src/questions/model.rs index 07db655184..6d1a87d72f 100644 --- a/rust/agama-lib/src/questions/model.rs +++ b/rust/agama-lib/src/questions/model.rs @@ -60,4 +60,4 @@ pub struct GenericAnswer { #[serde(rename_all = "camelCase")] pub struct PasswordAnswer { pub password: String, -} \ No newline at end of file +} diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index edf36ad932..3bae35d0e6 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -8,7 +8,8 @@ use crate::{error::Error, web::Event}; use agama_lib::{ error::ServiceError, - proxies::{GenericQuestionProxy, QuestionWithPasswordProxy, Questions1Proxy}, questions::model::{Answer, GenericQuestion, PasswordAnswer, Question, QuestionWithPassword}, + proxies::{GenericQuestionProxy, QuestionWithPasswordProxy, Questions1Proxy}, + questions::model::{Answer, GenericQuestion, PasswordAnswer, Question, QuestionWithPassword}, }; use anyhow::Context; use axum::{ From 2b7a16ef356fde61e92aac377121f0b051ccc404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:51:33 +0100 Subject: [PATCH 166/430] feat(web): start using TypeScript (#1456) ## Problem Agama web UI is using plain JavaScript but relying in TypeDoc for performing type checking. It is ok, but as the project grows looks like would be easier and straight forward to use TypeScript instead. ## Solution Start using TypeScript and migrate the current code little by little as the files are touched. ## Testing * Adapted Jest for working with TypeScript too. --- web/jest.config.js | 2 +- web/package-lock.json | 485 ++++++++++++++++++++++++++- web/package.json | 6 +- web/package/agama-web-ui.changes | 5 + web/src/client/http.js | 2 +- web/src/client/index.js | 3 +- web/src/queries/{l10n.js => l10n.ts} | 52 ++- web/src/types/l10n.ts | 44 +++ web/webpack.config.js | 30 +- 9 files changed, 583 insertions(+), 46 deletions(-) rename web/src/queries/{l10n.js => l10n.ts} (75%) create mode 100644 web/src/types/l10n.ts diff --git a/web/jest.config.js b/web/jest.config.js index 18ded77fa3..f416de4c8f 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -105,7 +105,7 @@ module.exports = { // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration - // preset: undefined, + preset: "ts-jest", // Run tests from one or more projects // projects: undefined, diff --git a/web/package-lock.json b/web/package-lock.json index 32878954fa..aebca1daf9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -36,7 +36,7 @@ "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", - "@types/jest": "^29.5.8", + "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "ajv": "^8.12.0", @@ -73,6 +73,7 @@ "po2json": "^1.0.0-alpha", "qunit": "^2.20.0", "react-refresh": "^0.14.0", + "react-refresh-typescript": "^2.0.9", "sass": "^1.69.5", "sass-loader": "^14.2.1", "string-replace-loader": "^3.0.0", @@ -81,7 +82,8 @@ "stylelint-config-standard-scss": "^13.1.0", "stylelint-webpack-plugin": "^5.0.0", "terser-webpack-plugin": "^5.3.9", - "ts-jest": "^29.0.3", + "ts-jest": "^29.2.2", + "ts-loader": "^9.5.1", "tsconfig-paths-webpack-plugin": "^4.0.0", "typedoc": "^0.25.4", "typedoc-plugin-external-module-map": "^2.0.1", @@ -2454,6 +2456,34 @@ "node": ">=18" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz", @@ -4497,6 +4527,42 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4691,6 +4757,7 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -5480,6 +5547,21 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -5604,6 +5686,15 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -5817,6 +5908,13 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -7170,6 +7268,15 @@ "node": ">=8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8017,6 +8124,18 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -8173,6 +8292,22 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.757", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.757.tgz", @@ -9828,6 +9963,29 @@ "node": ">= 12" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -11696,6 +11854,125 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jed": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", @@ -11707,6 +11984,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -15939,6 +16217,17 @@ "node": ">=0.10.0" } }, + "node_modules/react-refresh-typescript": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/react-refresh-typescript/-/react-refresh-typescript-2.0.9.tgz", + "integrity": "sha512-chAnOO4vpxm/3WkgOVmti+eN8yUtkJzeGkOigV6UA9eDFz12W34e/SsYe2H5+RwYJ3+sfSZkVbiXcG1chEBxlg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react-refresh": "0.10.x || 0.11.x || 0.12.x || 0.13.x || 0.14.x", + "typescript": "^4.8 || ^5.0" + } + }, "node_modules/react-router": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz", @@ -17998,12 +18287,14 @@ } }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.2.tgz", + "integrity": "sha512-sSW7OooaKT34AAngP6k1VS669a0HdLxkQZnlC7T76sckGCokXFnvJ3yRlQZGRTAoV5K19HfSgCiSwWOSIfcYlg==", "dev": true, + "license": "MIT", "dependencies": { "bs-logger": "0.x", + "ejs": "^3.0.0", "fast-json-stable-stringify": "2.x", "jest-util": "^29.0.0", "json5": "^2.2.3", @@ -18016,10 +18307,11 @@ "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -18029,6 +18321,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -18082,6 +18377,162 @@ "node": ">=12" } }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -18408,6 +18859,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18607,6 +19059,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -19422,6 +19883,18 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index a033858d1f..4d88a8871e 100644 --- a/web/package.json +++ b/web/package.json @@ -41,7 +41,7 @@ "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", - "@types/jest": "^29.5.8", + "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "ajv": "^8.12.0", @@ -78,6 +78,7 @@ "po2json": "^1.0.0-alpha", "qunit": "^2.20.0", "react-refresh": "^0.14.0", + "react-refresh-typescript": "^2.0.9", "sass": "^1.69.5", "sass-loader": "^14.2.1", "string-replace-loader": "^3.0.0", @@ -86,7 +87,8 @@ "stylelint-config-standard-scss": "^13.1.0", "stylelint-webpack-plugin": "^5.0.0", "terser-webpack-plugin": "^5.3.9", - "ts-jest": "^29.0.3", + "ts-jest": "^29.2.2", + "ts-loader": "^9.5.1", "tsconfig-paths-webpack-plugin": "^4.0.0", "typedoc": "^0.25.4", "typedoc-plugin-external-module-map": "^2.0.1", diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 7129415153..5cabeb09fe 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Fri Jul 12 10:41:28 UTC 2024 - David Diaz + +- Allow using TypeScript (gh#openSUSE/agama#1456). + ------------------------------------------------------------------- Tue Jul 9 08:51:34 UTC 2024 - Imobach Gonzalez Sosa diff --git a/web/src/client/http.js b/web/src/client/http.js index a30fca0b9a..682f17c220 100644 --- a/web/src/client/http.js +++ b/web/src/client/http.js @@ -388,4 +388,4 @@ class HTTPClient { } } -export { HTTPClient }; +export { HTTPClient, WSClient }; diff --git a/web/src/client/index.js b/web/src/client/index.js index 43d9eb70da..7816270a2e 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -30,7 +30,7 @@ import { UsersClient } from "./users"; import phase from "./phase"; import { QuestionsClient } from "./questions"; import { NetworkClient } from "./network"; -import { HTTPClient } from "./http"; +import { HTTPClient, WSClient } from "./http"; /** * @typedef {object} InstallerClient @@ -43,6 +43,7 @@ import { HTTPClient } from "./http"; * @property {StorageClient} storage - storage client. * @property {UsersClient} users - users client. * @property {QuestionsClient} questions - questions client. + * @property {() => WSClient} ws - Agama WebSocket client. * @property {() => Promise} issues - issues from all contexts. * @property {(handler: IssuesHandler) => (() => void)} onIssuesChange - registers a handler to run * when issues from any context change. It returns a function to deregister the handler. diff --git a/web/src/queries/l10n.js b/web/src/queries/l10n.ts similarity index 75% rename from web/src/queries/l10n.js rename to web/src/queries/l10n.ts index 66d81f851c..2dd5ee7fda 100644 --- a/web/src/queries/l10n.js +++ b/web/src/queries/l10n.ts @@ -30,7 +30,7 @@ import { timezoneUTCOffset } from "~/utils"; const configQuery = () => { return { queryKey: ["l10n/config"], - queryFn: () => fetch("/api/l10n/config").then((res) => res.json()), + queryFn: () => fetch("/api/l10n/config").then(res => res.json()) }; }; @@ -39,10 +39,10 @@ const configQuery = () => { */ const localesQuery = () => ({ queryKey: ["l10n/locales"], - queryFn: async () => { + queryFn: async (): Promise => { const response = await fetch("/api/l10n/locales"); const locales = await response.json(); - return locales.map(({ id, language, territory }) => { + return locales.map(({ id, language, territory }): Locale => { return { id, name: language, territory }; }); }, @@ -54,10 +54,10 @@ const localesQuery = () => ({ */ const timezonesQuery = () => ({ queryKey: ["l10n/timezones"], - queryFn: async () => { + queryFn: async (): Promise => { const response = await fetch("/api/l10n/timezones"); const timezones = await response.json(); - return timezones.map(({ code, parts, country }) => { + return timezones.map(({ code, parts, country }): Timezone => { const offset = timezoneUTCOffset(code); return { id: code, parts, country, utcOffset: offset }; }); @@ -70,13 +70,13 @@ const timezonesQuery = () => ({ */ const keymapsQuery = () => ({ queryKey: ["l10n/keymaps"], - queryFn: async () => { + queryFn: async (): Promise => { const response = await fetch("/api/l10n/keymaps"); const json = await response.json(); - const keymaps = json.map(({ id, description }) => { + const keymaps = json.map(({ id, description }): Keymap => { return { id, name: description }; }); - return keymaps.sort((a, b) => (a.name < b.name) ? -1 : 1); + return keymaps.sort((a, b) => (a.name < b.name ? -1 : 1)); }, staleTime: Infinity }); @@ -88,13 +88,13 @@ const keymapsQuery = () => ({ */ const useConfigMutation = () => { const query = { - mutationFn: (newConfig) => + mutationFn: newConfig => fetch("/api/l10n/config", { method: "PATCH", body: JSON.stringify(newConfig), headers: { - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } }) }; return useMutation(query); @@ -123,26 +123,22 @@ const useL10nConfigChanges = () => { /// Returns the l10n data. const useL10n = () => { - const [ - { data: config }, - { data: locales }, - { data: keymaps }, - { data: timezones } - ] = useSuspenseQueries({ - queries: [ - configQuery(), - localesQuery(), - keymapsQuery(), - timezonesQuery() - ] - }); + const [{ data: config }, { data: locales }, { data: keymaps }, { data: timezones }] = + useSuspenseQueries({ + queries: [configQuery(), localesQuery(), keymapsQuery(), timezonesQuery()] + }); - const selectedLocale = locales.find((l) => l.id === config.locales[0]); - const selectedKeymap = keymaps.find((k) => k.id === config.keymap); - const selectedTimezone = timezones.find((t) => t.id === config.timezone); + const selectedLocale = locales.find(l => l.id === config.locales[0]); + const selectedKeymap = keymaps.find(k => k.id === config.keymap); + const selectedTimezone = timezones.find(t => t.id === config.timezone); return { - locales, keymaps, timezones, selectedLocale, selectedKeymap, selectedTimezone + locales, + keymaps, + timezones, + selectedLocale, + selectedKeymap, + selectedTimezone }; }; diff --git a/web/src/types/l10n.ts b/web/src/types/l10n.ts new file mode 100644 index 0000000000..64dbcd1354 --- /dev/null +++ b/web/src/types/l10n.ts @@ -0,0 +1,44 @@ +type Keymap = { + /** + * Keyboard id (e.g., "us"). + */ + id: string; + /** + * Keyboard name (e.g., "English (US)"). + */ + name: string; +}; + +type Locale = { + /** + * Language id (e.g., "en_US.UTF-8"). + */ + id: string; + /** + * Language name (e.g., "English"). + */ + name: string; + /** + * Territory name (e.g., "United States"). + */ + territory: string; +}; + +type Timezone = { + /** + * Timezone id (e.g., "Atlantic/Canary"). + */ + id: string; + /** + * Name of the timezone parts (e.g., ["Atlantic", "Canary"]). + */ + parts: string[]; + /** + * Name of the country associated to the zone or empty string (e.g., "Spain"). + */ + country: string; + /** + * UTC offset. + */ + utcOffset: number; +}; diff --git a/web/webpack.config.js b/web/webpack.config.js index 555fb6e13f..87df2a4819 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -12,6 +12,7 @@ const CockpitPoPlugin = require("./src/lib/cockpit-po-plugin"); const StylelintPlugin = require("stylelint-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); +const ReactRefreshTypeScript = require("react-refresh-typescript"); const webpack = require("webpack"); const po_handler = require("./src/lib/webpack-po-handler"); @@ -81,8 +82,8 @@ module.exports = { mode: production ? "production" : "development", resolve: { modules: ["node_modules", path.resolve(__dirname, "src/lib")], - plugins: [new TsconfigPathsPlugin({ extensions: [".js", ".jsx", ".json"] })], - extensions: ["", ".js", ".json", ".jsx"], + plugins: [new TsconfigPathsPlugin({ extensions: [".ts", ".tsx", ".js", ".jsx", ".json"] })], + extensions: [".ts", ".tsx", ".js", ".jsx", ".json"] }, resolveLoader: { modules: ["node_modules", path.resolve(__dirname, "src/lib")], @@ -131,24 +132,39 @@ module.exports = { // Thus, it's needed not mangling function names ending in PageMenu to keep it working in production // until adopting a better solution, if any. terserOptions: { - keep_fnames: /PageMenu$/, + keep_fnames: /PageMenu$/ }, extractComments: { condition: true, filename: `[file].LICENSE.txt?query=[query]&filebase=[base]`, banner(licenseFile) { return `License information can be found in ${licenseFile}`; - }, - }, + } + } }), // remove also the spaces between the tags new HtmlMinimizerPlugin({ minimizerOptions: { conservativeCollapse: false } }), - new CssMinimizerPlugin(), - ], + new CssMinimizerPlugin() + ] }, module: { rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve("ts-loader"), + options: { + getCustomTransformers: () => ({ + before: [development && ReactRefreshTypeScript()].filter(Boolean) + }), + transpileOnly: development + } + } + ] + }, { test: /\.(js|jsx)$/, exclude: /node_modules/, From 99b1b7503bdd3ac0618167c942c6be4fbbb3da9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 12 Jul 2024 12:01:24 +0100 Subject: [PATCH 167/430] fix(service): do not configure Cockpit anymore --- service/lib/agama/cockpit_manager.rb | 175 ------------------ service/lib/agama/dbus/manager_service.rb | 7 - service/test/agama/cockpit_manager_test.rb | 157 ---------------- .../test/agama/dbus/manager_service_test.rb | 3 - 4 files changed, 342 deletions(-) delete mode 100644 service/lib/agama/cockpit_manager.rb delete mode 100644 service/test/agama/cockpit_manager_test.rb diff --git a/service/lib/agama/cockpit_manager.rb b/service/lib/agama/cockpit_manager.rb deleted file mode 100644 index 80b70162ef..0000000000 --- a/service/lib/agama/cockpit_manager.rb +++ /dev/null @@ -1,175 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "yast" -require "yast2/systemd/service" -require "cfa/base_model" -require "transfer/file_from_url" -require "fileutils" - -Yast.import "URL" - -module Agama - # Cockpit configuration file representation - # - # @example Set the AllowUnencrypted option - # file = CockpitConfig.new - # file.load - # file.web_service["AllowUnencrypted"] = "true" - # file.save - class CockpitConfig < CFA::BaseModel - # Constructor - # - # @param path [String] File path - # @param file_handler [.read, .write] Object to read/write the file. - def initialize(path: DEFAULT_PATH, file_handler: nil) - super(CFA::AugeasParser.new("puppet.lns"), path, file_handler: file_handler) - end - - # Returns the augeas tree for the "WebService" section - # - # If the given section does not exist, it returns an empty one - # - # @return [AugeasTree] - def web_service - data["WebService"] ||= CFA::AugeasTree.new - end - end - - # Handles the Cockpit service - # - # This API offers an API to adjust Cockpit configuration and restart/reload the process. At this - # point, just a few options are allowed (@see #setup). - class CockpitManager - include Yast::Logger - include Yast::Transfer::FileFromUrl - include Yast::I18n - - # Directory to store Cockpit certificates - WS_CERTS_DIR = "/etc/cockpit/ws-certs.d" - COCKPIT_SERVICE = "cockpit" - COCKPIT_CONF_PATH = "/etc/cockpit/cockpit.conf" - - def initialize(logger, prefix: "/") - @prefix = prefix - @logger = logger - end - - # Adjust Cockpit configuration and restart the process if needed - # - # If all arguments are nil, the configuration is not modified and the process is not restarted. - # - # @param options [Hash] - # @option ssl [Boolean,nil] SSL is enabled - # @option ssl_cert [String,nil] SSL/TLS certificate URL - # @option ssl_key [String,nil] SSL/TLS key URL - def setup(options) - return if options.values.all?(&:nil?) - - enable_ssl(options["ssl"]) unless options["ssl"].nil? - if options["ssl_cert"] - copy_ssl_cert(options["ssl_cert"]) - copy_ssl_key(options["ssl_key"]) unless options["ssl_key"].nil? - clear_self_signed_cert - end - - restart_cockpit - end - - private - - attr_reader :prefix - - # @return [Logger] - attr_reader :logger - - # Enable/Disable SSL - # - # @param enabled [Boolean] Whether to enable or disable SSL - def enable_ssl(enabled) - path = File.join(prefix, COCKPIT_CONF_PATH) - config = CockpitConfig.new(path: path) - config.load if File.readable?(path) - config.web_service["AllowUnencrypted"] = (!enabled).to_s - config.save - end - - # Copy the SSL certificate to Cockpit's certificates directory - # - # The certificate is renamed as `0-agama.cert`. - # - # @param location [String] Certificate location - def copy_ssl_cert(location) - logger.info "Retrieving SSL certificate from #{location}" - copy_file(location, File.join(prefix, WS_CERTS_DIR, "0-agama.cert")) - end - - # Copy the SSL certificate key to Cockpit's certificates directory - # - # The certificate is renamed as `0-agama.key`. - # - # @param location [String] Certificate key location - def copy_ssl_key(location) - logger.info "Retrieving SSL key from #{location}" - copy_file(location, File.join(prefix, WS_CERTS_DIR, "0-agama.key")) - end - - # Remove Cockpit's self signed certificates if they exist - def clear_self_signed_cert - self_signed = Dir[File.join(prefix, WS_CERTS_DIR, "0-self-signed.*")] - ::FileUtils.rm(self_signed) - end - - # Copy a file from a potentially remote location - # - # @param location [String] File location. It might be an URL-like string (e.g., - # "http://example.net/example.cert"). - # @param target [String] Path to copy the file to. - # @return [Boolean] Whether the file was sucessfully copied or not - def copy_file(location, target) - url = Yast::URL.Parse(location) - - res = get_file_from_url( - scheme: url["scheme"], - host: url["host"], - urlpath: url["path"], - localfile: target, - urltok: url, - destdir: "/" - ) - # TODO: exception? - logger.error "script #{location} could not be retrieved" unless res - res - end - - # Restart the Cockpit service - def restart_cockpit - logger.info "Restarting Cockpit" - service = Yast2::Systemd::Service.find(COCKPIT_SERVICE) - if service.nil? - logger.error "Could not found #{COCKPIT_SERVICE} service" - return - end - - service.restart - end - end -end diff --git a/service/lib/agama/dbus/manager_service.rb b/service/lib/agama/dbus/manager_service.rb index 0f16e08fbd..1b8a3bc456 100644 --- a/service/lib/agama/dbus/manager_service.rb +++ b/service/lib/agama/dbus/manager_service.rb @@ -22,7 +22,6 @@ require "dbus" require "agama/manager" require "agama/users" -require "agama/cockpit_manager" require "agama/dbus/bus" require "agama/dbus/clients/locale" require "agama/dbus/manager" @@ -69,7 +68,6 @@ def initialize(config, logger = nil) # # @note The service runs its startup phase def start - setup_cockpit export # We need locale for data from users locale_client = Clients::Locale.instance @@ -99,11 +97,6 @@ def dispatch # @return [Config] attr_reader :config - def setup_cockpit - cockpit = CockpitManager.new(logger) - cockpit.setup(config.data["web"]) - end - # @return [::DBus::ObjectServer] def service @service ||= bus.request_service(SERVICE_NAME) diff --git a/service/test/agama/cockpit_manager_test.rb b/service/test/agama/cockpit_manager_test.rb deleted file mode 100644 index d54f811a9f..0000000000 --- a/service/test/agama/cockpit_manager_test.rb +++ /dev/null @@ -1,157 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require_relative "../test_helper" -require "tmpdir" -require "fileutils" -require "agama/cockpit_manager" -Yast.import "Installation" - -describe Agama::CockpitManager do - subject(:cockpit) { described_class.new(logger) } - - let(:logger) { Logger.new($stdout, level: :warn) } - - before do - # Avoid problems with FileFromUrl side effects. - allow(Yast::Installation).to receive(:sourcedir).and_return("/") - end - - describe "#setup" do - subject(:cockpit) { described_class.new(logger, prefix: tmpdir) } - - let(:tmpdir) { Dir.mktmpdir } - let(:cockpit_certs) { File.join(tmpdir, "etc", "cockpit", "ws-certs.d") } - let(:cockpit_conf) { File.join(tmpdir, "etc", "cockpit", "cockpit.conf") } - - let(:systemd_service) do - instance_double(Yast2::Systemd::Service, restart: nil) - end - - before do - allow(Yast2::Systemd::Service).to receive(:find).with("cockpit") - .and_return(systemd_service) - end - - around do |example| - FileUtils.mkdir_p(cockpit_certs) - FileUtils.touch(cockpit_conf) - example.run - FileUtils.remove_entry(tmpdir) - end - - context "when TLS/SSL is disabled" do - let(:options) { { "ssl" => false } } - - it "sets AllowUnencrypted to true" do - subject.setup(options) - content = File.read(cockpit_conf) - expect(content).to include("AllowUnencrypted=true") - end - - it "restarts cockpit" do - expect(systemd_service).to receive(:restart) - subject.setup(options) - end - end - - context "when TLS/SSL is enabled" do - let(:options) { { "ssl" => true } } - - it "sets AllowUnencrypted to false" do - subject.setup(options) - content = File.read(cockpit_conf) - expect(content).to include("AllowUnencrypted=false") - end - - it "restarts cockpit" do - expect(systemd_service).to receive(:restart) - subject.setup(options) - end - end - - context "when TLS/SSL is not explictly enabled/disabled" do - let(:options) { {} } - - it "does not change the Cockpit's encryption configuration" do - subject.setup(options) - content = File.read(cockpit_conf) - expect(content).to_not include("AllowUnencrypted") - end - - it "does not restart cockpit" do - expect(systemd_service).to_not receive(:restart) - subject.setup(options) - end - end - - context "when a TLS/SSL certificate and key URLs are given" do - let(:options) do - { "ssl_cert" => "file://" + File.join(FIXTURES_PATH, "agama.cert"), - "ssl_key" => "file://" + File.join(FIXTURES_PATH, "agama.key") } - end - - it "downloads the certificate to Cockpit's certificates directory" do - subject.setup(options) - expect(File).to exist(File.join(cockpit_certs, "0-agama.cert")) - expect(File).to exist(File.join(cockpit_certs, "0-agama.key")) - end - - it "restarts cockpit" do - expect(systemd_service).to receive(:restart) - subject.setup(options) - end - - context "when a self-signed certificate exist" do - before do - FileUtils.touch(File.join(cockpit_certs, "0-self-signed.cert")) - FileUtils.touch(File.join(cockpit_certs, "0-self-signed.key")) - end - - it "removes the self-signed certificate" do - subject.setup(options) - expect(File).to_not exist(File.join(cockpit_certs, "0-self-signed.cert")) - expect(File).to_not exist(File.join(cockpit_certs, "0-self-signed.key")) - end - end - end - - context "when an empty configuration is given" do - it "does not restart cockpit" do - expect(systemd_service).to_not receive(:restart) - subject.setup({}) - end - end - - context "when a self-signed certificate exists" do - before do - FileUtils.touch(File.join(cockpit_certs, "0-self-signed.cert")) - FileUtils.touch(File.join(cockpit_certs, "0-self-signed.key")) - end - - it "does not remove the self-signed certificate" do - subject.setup({}) - expect(File).to exist(File.join(cockpit_certs, "0-self-signed.cert")) - expect(File).to exist(File.join(cockpit_certs, "0-self-signed.key")) - end - end - end -end diff --git a/service/test/agama/dbus/manager_service_test.rb b/service/test/agama/dbus/manager_service_test.rb index ef886cc722..b7503772f0 100644 --- a/service/test/agama/dbus/manager_service_test.rb +++ b/service/test/agama/dbus/manager_service_test.rb @@ -33,8 +33,6 @@ let(:object_server) { instance_double(DBus::ObjectServer, export: nil) } let(:bus) { instance_double(Agama::DBus::Bus, request_name: nil) } - let(:cockpit) { instance_double(Agama::CockpitManager, setup: nil) } - let(:manager_obj) { instance_double(Agama::DBus::Manager, path: "/org/opensuse/Agama/Users1") } let(:users_obj) { instance_double(Agama::DBus::Users, path: "/org/opensuse/Agama/Users1") } @@ -48,7 +46,6 @@ allow(bus).to receive(:request_service).with("org.opensuse.Agama.Manager1") .and_return(object_server) allow(Agama::Manager).to receive(:new).with(config, logger).and_return(manager) - allow(Agama::CockpitManager).to receive(:new).and_return(cockpit) allow(Agama::DBus::Clients::Locale).to receive(:instance).and_return(locale_client) allow(Agama::DBus::Manager).to receive(:new).with(manager, logger).and_return(manager_obj) allow(Agama::DBus::Users).to receive(:new).and_return(users_obj) From e1136312e2fe311e8ebea6f1ed27a5fee422b5a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 12 Jul 2024 12:14:22 +0100 Subject: [PATCH 168/430] doc: update changes file --- service/package/rubygem-agama-yast.changes | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index dc8b967db2..5defe09148 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Fri Jul 12 11:03:14 UTC 2024 - Imobach Gonzalez Sosa + +- Stop trying to set up Cockpit (gh#openSUSE/agama#1459). + ------------------------------------------------------------------- Fri Jul 5 13:12:36 UTC 2024 - José Iván López González From ef05fe848ece765106bed574d32330254537fc58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:00:46 +0100 Subject: [PATCH 169/430] chore: format web base code consistently (#1460) ## Problem For whatever reason, the code web frontend code does not have a consistent format. ## Solution _Prettify_ the code by running `npx prettier . -w` after adapting the configuration we had in `.prettierrc` and updating the list of directories/files it should ignore via `.prettierignore` file. What we have decided it to use as much defaults as possible, ending up with just one override: `printWidth`. Having lines up to 100 characters length looks a good compromise for us in terms of code readability. To make the change possible, conflicts between ESLint and Prettier has been solved by making use of https://github.com/prettier/eslint-config-prettier. Something similar with Stylelint, for which https://github.com/prettier/stylelint-prettier has been used. --- web/.babelrc.json | 27 +- web/.eslintrc.json | 161 +- web/.prettierignore | 6 +- web/.prettierrc | 6 +- web/.stylelintrc.json | 6 +- web/README.md | 2 +- web/__mocks__/svg.js | 4 +- web/babel.config.js | 11 +- web/cspell.json | 8 +- web/jest.config.js | 22 +- web/package-lock.json | 69 + web/package.json | 3 + web/src/App.jsx | 2 +- web/src/App.test.jsx | 16 +- web/src/MainLayout.jsx | 42 +- web/src/SimpleLayout.jsx | 22 +- web/src/agama.js | 10 +- web/src/assets/fonts.scss | 90 +- web/src/assets/styles/app.scss | 6 +- web/src/assets/styles/blocks.scss | 25 +- web/src/assets/styles/composition.scss | 7 +- web/src/assets/styles/global.scss | 15 +- web/src/assets/styles/index.scss | 1 - .../assets/styles/patternfly-overrides.scss | 22 +- web/src/assets/styles/utilities.scss | 6 +- web/src/client/dbus.js | 17 +- web/src/client/dbus.test.js | 16 +- web/src/client/http.js | 11 +- web/src/client/index.js | 22 +- web/src/client/manager.js | 2 +- web/src/client/mixins.js | 11 +- web/src/client/monitor.js | 3 +- web/src/client/network.test.js | 12 +- web/src/client/network/index.js | 89 +- web/src/client/network/model.js | 57 +- web/src/client/network/model.test.js | 4 +- web/src/client/network/utils.js | 28 +- web/src/client/network/utils.test.js | 9 +- web/src/client/phase.js | 2 +- web/src/client/software.js | 7 +- web/src/client/software.test.js | 4 +- web/src/client/status.js | 2 +- web/src/client/storage.js | 131 +- web/src/client/storage.test.js | 653 ++--- web/src/client/users.js | 18 +- web/src/components/core/About.jsx | 29 +- web/src/components/core/About.test.jsx | 6 +- web/src/components/core/ButtonLink.jsx | 12 +- web/src/components/core/CardField.jsx | 20 +- web/src/components/core/Description.jsx | 6 +- web/src/components/core/Drawer.jsx | 71 +- web/src/components/core/EmailInput.jsx | 17 +- web/src/components/core/EmailInput.test.jsx | 15 +- web/src/components/core/EmptyState.jsx | 2 +- .../components/core/ExpandableSelector.jsx | 42 +- .../core/ExpandableSelector.test.jsx | 50 +- web/src/components/core/Fieldset.jsx | 6 +- web/src/components/core/FormLabel.jsx | 11 +- web/src/components/core/FormReadOnlyField.jsx | 2 +- .../components/core/FormValidationError.jsx | 4 +- web/src/components/core/InstallButton.jsx | 2 +- .../components/core/InstallButton.test.jsx | 6 +- web/src/components/core/Installation.jsx | 2 +- .../components/core/InstallationFinished.jsx | 35 +- web/src/components/core/InstallerOptions.jsx | 70 +- web/src/components/core/IssuesHint.jsx | 4 +- web/src/components/core/IssuesHint.test.jsx | 2 +- web/src/components/core/ListSearch.jsx | 9 +- web/src/components/core/ListSearch.test.jsx | 44 +- web/src/components/core/LoginPage.jsx | 28 +- web/src/components/core/LoginPage.test.jsx | 4 +- web/src/components/core/LogsButton.jsx | 28 +- web/src/components/core/LogsButton.test.jsx | 15 +- web/src/components/core/NumericTextInput.jsx | 4 +- web/src/components/core/Page.jsx | 61 +- web/src/components/core/Page.test.jsx | 35 +- .../core/PasswordAndConfirmationInput.jsx | 14 +- .../PasswordAndConfirmationInput.test.jsx | 12 +- web/src/components/core/PasswordInput.jsx | 11 +- .../components/core/PasswordInput.test.jsx | 27 +- web/src/components/core/Popup.jsx | 26 +- web/src/components/core/ProgressReport.jsx | 14 +- web/src/components/core/ProgressText.jsx | 6 +- web/src/components/core/RowActions.jsx | 6 +- web/src/components/core/Section.jsx | 13 +- web/src/components/core/Section.test.jsx | 18 +- web/src/components/core/SectionSkeleton.jsx | 18 +- web/src/components/core/ServerError.jsx | 15 +- web/src/components/core/Tip.jsx | 2 +- web/src/components/core/TreeTable.jsx | 85 +- web/src/components/l10n/KeyboardSelection.jsx | 15 +- .../l10n/KeyboardSelection.test.jsx | 6 +- web/src/components/l10n/L10nPage.jsx | 30 +- web/src/components/l10n/L10nPage.test.jsx | 12 +- web/src/components/l10n/LocaleSelection.jsx | 18 +- .../components/l10n/LocaleSelection.test.jsx | 8 +- web/src/components/l10n/TimezoneSelection.jsx | 23 +- .../l10n/TimezoneSelection.test.jsx | 8 +- web/src/components/layout/Center.jsx | 4 +- web/src/components/layout/Icon.jsx | 8 +- web/src/components/layout/Icon.test.jsx | 16 +- .../components/network/AddressesDataList.jsx | 30 +- .../components/network/ConnectionsTable.jsx | 16 +- .../network/ConnectionsTable.test.jsx | 20 +- web/src/components/network/DnsDataList.jsx | 49 +- web/src/components/network/IpAddressInput.jsx | 10 +- web/src/components/network/IpPrefixInput.jsx | 10 +- web/src/components/network/IpSettingsForm.jsx | 46 +- web/src/components/network/NetworkPage.jsx | 66 +- .../components/network/NetworkPage.test.jsx | 25 +- .../components/network/WifiConnectionForm.jsx | 44 +- .../network/WifiConnectionForm.test.jsx | 6 +- .../network/WifiHiddenNetworkForm.jsx | 5 +- .../network/WifiHiddenNetworkForm.test.jsx | 4 +- .../network/WifiNetworksListPage.jsx | 75 +- .../components/network/WifiSelectorPage.jsx | 15 +- web/src/components/network/routes.js | 10 +- .../components/overview/L10nSection.test.jsx | 2 +- web/src/components/overview/OverviewPage.jsx | 32 +- .../components/overview/OverviewPage.test.jsx | 6 +- .../components/overview/SoftwareSection.jsx | 4 +- .../overview/SoftwareSection.test.jsx | 8 +- .../components/overview/StorageSection.jsx | 34 +- .../overview/StorageSection.test.jsx | 10 +- web/src/components/overview/routes.js | 4 +- .../product/ProductRegistrationForm.test.jsx | 8 +- .../product/ProductRegistrationPage.jsx | 15 +- .../product/ProductSelectionPage.jsx | 16 +- .../product/ProductSelectionPage.test.jsx | 14 +- .../components/questions/GenericQuestion.jsx | 4 +- .../questions/GenericQuestion.test.jsx | 9 +- .../questions/LuksActivationQuestion.jsx | 8 +- .../questions/LuksActivationQuestion.test.jsx | 23 +- .../components/questions/QuestionActions.jsx | 24 +- .../questions/QuestionActions.test.jsx | 15 +- web/src/components/questions/Questions.jsx | 11 +- .../components/questions/Questions.test.jsx | 7 +- web/src/components/software/SoftwarePage.jsx | 26 +- .../software/SoftwarePatternsSelection.jsx | 118 +- .../SoftwarePatternsSelection.test.jsx | 17 +- web/src/components/software/UsedSize.jsx | 4 +- web/src/components/software/icons/README.md | 3 +- web/src/components/software/routes.js | 8 +- .../components/storage/BootConfigField.jsx | 25 +- .../storage/BootConfigField.test.jsx | 4 +- web/src/components/storage/BootSelection.jsx | 42 +- .../components/storage/BootSelection.test.jsx | 12 +- .../components/storage/DASDFormatProgress.jsx | 12 +- web/src/components/storage/DASDPage.jsx | 34 +- web/src/components/storage/DASDTable.jsx | 125 +- .../components/storage/DeviceSelection.jsx | 30 +- .../storage/DeviceSelectorTable.jsx | 27 +- .../components/storage/DevicesFormSelect.jsx | 8 +- web/src/components/storage/DevicesManager.js | 45 +- .../components/storage/DevicesManager.test.js | 58 +- .../components/storage/DevicesTechMenu.jsx | 25 +- .../storage/DevicesTechMenu.test.jsx | 6 +- .../components/storage/EncryptionField.jsx | 15 +- .../storage/EncryptionSettingsDialog.jsx | 37 +- .../storage/EncryptionSettingsDialog.test.jsx | 15 +- .../storage/InstallationDeviceField.jsx | 23 +- .../storage/InstallationDeviceField.test.jsx | 12 +- .../components/storage/PartitionsField.jsx | 309 ++- .../storage/PartitionsField.test.jsx | 78 +- .../storage/ProposalActionsDialog.jsx | 23 +- .../storage/ProposalActionsDialog.test.jsx | 96 +- .../storage/ProposalActionsSummary.jsx | 117 +- .../storage/ProposalActionsSummary.test.jsx | 12 +- web/src/components/storage/ProposalPage.jsx | 63 +- .../components/storage/ProposalPage.test.jsx | 67 +- .../storage/ProposalResultSection.jsx | 25 +- .../storage/ProposalResultSection.test.jsx | 4 +- .../storage/ProposalResultTable.jsx | 22 +- .../storage/ProposalSettingsSection.jsx | 32 +- .../storage/ProposalSettingsSection.test.jsx | 12 +- .../storage/ProposalTransactionalInfo.jsx | 12 +- .../ProposalTransactionalInfo.test.jsx | 4 +- web/src/components/storage/SnapshotsField.jsx | 20 +- .../storage/SnapshotsField.test.jsx | 4 +- .../components/storage/SpaceActionsTable.jsx | 57 +- .../storage/SpaceActionsTable.test.jsx | 39 +- .../storage/SpacePolicySelection.jsx | 19 +- web/src/components/storage/VolumeDialog.jsx | 81 +- .../components/storage/VolumeDialog.test.jsx | 64 +- web/src/components/storage/VolumeFields.jsx | 76 +- .../components/storage/VolumeFields.test.jsx | 39 +- .../storage/VolumeLocationDialog.jsx | 34 +- .../storage/VolumeLocationDialog.test.jsx | 28 +- .../storage/VolumeLocationSelectorTable.jsx | 26 +- web/src/components/storage/ZFCPDiskForm.jsx | 46 +- .../components/storage/ZFCPDiskForm.test.jsx | 10 +- web/src/components/storage/ZFCPPage.jsx | 179 +- web/src/components/storage/ZFCPPage.test.jsx | 83 +- web/src/components/storage/device-utils.jsx | 13 +- .../components/storage/device-utils.test.jsx | 20 +- .../components/storage/iscsi/AuthFields.jsx | 44 +- .../components/storage/iscsi/DiscoverForm.jsx | 61 +- .../components/storage/iscsi/EditNodeForm.jsx | 9 +- .../storage/iscsi/InitiatorForm.jsx | 6 +- .../storage/iscsi/InitiatorPresenter.jsx | 11 +- .../storage/iscsi/InitiatorSection.jsx | 5 +- .../components/storage/iscsi/LoginForm.jsx | 24 +- .../storage/iscsi/NodeStartupOptions.js | 2 +- .../storage/iscsi/NodesPresenter.jsx | 44 +- .../storage/iscsi/TargetsSection.jsx | 41 +- web/src/components/storage/routes.js | 11 +- .../storage/test-data/full-result-example.js | 2405 ++++++++--------- web/src/components/storage/utils.js | 39 +- web/src/components/storage/utils.test.js | 36 +- web/src/components/users/FirstUser.jsx | 22 +- web/src/components/users/FirstUserForm.jsx | 62 +- web/src/components/users/RootAuthMethods.jsx | 62 +- .../components/users/RootAuthMethods.test.jsx | 48 +- .../components/users/RootPasswordPopup.jsx | 14 +- .../users/RootPasswordPopup.test.jsx | 2 +- web/src/components/users/RootSSHKeyPopup.jsx | 4 +- .../components/users/RootSSHKeyPopup.test.jsx | 2 +- web/src/components/users/routes.js | 10 +- web/src/components/users/utils.js | 18 +- web/src/components/users/utils.test.js | 58 +- web/src/context/app.jsx | 4 +- web/src/context/auth.jsx | 10 +- web/src/context/installer.jsx | 33 +- web/src/context/installer.test.jsx | 6 +- web/src/context/installerL10n.jsx | 114 +- web/src/context/installerL10n.test.jsx | 2 +- web/src/context/root.jsx | 6 +- web/src/hooks/useNodeSiblings.js | 6 +- web/src/hooks/useNodeSiblings.test.js | 37 +- web/src/i18n.js | 15 +- web/src/index.html | 4 +- web/src/index.js | 2 +- web/src/languages.json | 22 +- web/src/queries/l10n.ts | 30 +- web/src/queries/software.js | 24 +- web/src/router.js | 31 +- web/src/routes/l10n.js | 22 +- web/src/routes/products.js | 8 +- web/src/test-utils.js | 46 +- web/src/test-utils.test.js | 6 +- web/src/utils.js | 56 +- web/src/utils.test.js | 51 +- web/svgo.config.js | 8 +- web/tsconfig.json | 16 +- web/typedoc.json | 21 +- web/webpack.config.js | 24 +- 246 files changed, 4975 insertions(+), 4543 deletions(-) diff --git a/web/.babelrc.json b/web/.babelrc.json index dfe57f214c..146fc6a5e5 100644 --- a/web/.babelrc.json +++ b/web/.babelrc.json @@ -1,14 +1,17 @@ { - "presets": [ - ["@babel/env", { - "targets": { - "chrome": "57", - "firefox": "52", - "safari": "10.3", - "edge": "16", - "opera": "44" - } - }], - "@babel/preset-react" - ] + "presets": [ + [ + "@babel/env", + { + "targets": { + "chrome": "57", + "firefox": "52", + "safari": "10.3", + "edge": "16", + "opera": "44" + } + } + ], + "@babel/preset-react" + ] } diff --git a/web/.eslintrc.json b/web/.eslintrc.json index 1e82edd3c9..4b23faa524 100644 --- a/web/.eslintrc.json +++ b/web/.eslintrc.json @@ -1,83 +1,94 @@ { - "root": true, - "env": { - "browser": true, - "es6": true, - "jest": true + "root": true, + "env": { + "browser": true, + "es6": true, + "jest": true + }, + "extends": [ + "eslint:recommended", + "standard", + "standard-jsx", + "standard-react", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 7, + "ecmaFeatures": { + "jsx": true }, - "extends": [ - "eslint:recommended", - "standard", - "standard-jsx", - "standard-react", - "plugin:@typescript-eslint/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 7, - "ecmaFeatures": { - "jsx": true - }, - "sourceType": "module" - }, - "plugins": ["agama-i18n", "flowtype", "i18next", "react", "react-hooks", "@typescript-eslint"], - "rules": { - "agama-i18n/string-literals": "error", - "i18next/no-literal-string": "error", - "indent": ["error", 2, - { - "ObjectExpression": "first", - "CallExpression": {"arguments": "first"}, - "MemberExpression": 1, - "ignoredNodes": [ "JSXAttribute" ], - "SwitchCase": 1 - }], - "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }], - "no-var": "error", - "no-multi-str": "off", - "no-use-before-define": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-use-before-define": "warn", - "@typescript-eslint/ban-ts-comment": "off", - "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], - "prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }], - "react/jsx-indent": ["error", 2], - "semi": ["error", "always", { "omitLastInOneLineBlock": true }], - - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "error", - - "camelcase": "off", - "comma-dangle": "off", - "curly": "off", - "jsx-quotes": "off", - "key-spacing": "off", - "no-console": "off", - "quotes": "off", - "react/jsx-curly-spacing": "off", - "react/jsx-indent-props": "off", - "react/prop-types": "off", - "space-before-function-paren": "off", - "n/no-callback-literal": "off" - }, - "overrides": [ + "sourceType": "module" + }, + "plugins": [ + "agama-i18n", + "flowtype", + "i18next", + "react", + "react-hooks", + "@typescript-eslint" + ], + "rules": { + "agama-i18n/string-literals": "error", + "i18next/no-literal-string": "error", + "no-var": "error", + "no-multi-str": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-use-before-define": "warn", + "@typescript-eslint/ban-ts-comment": "off", + "lines-between-class-members": [ + "error", + "always", { - // do not check translations in the testing or development files - "files": ["*.test.*", "test-utils.js"], - "rules": { - "i18next/no-literal-string": "off" - } - }, + "exceptAfterSingleLine": true + } + ], + "prefer-promise-reject-errors": [ + "error", { - // do not check translation arguments in the test, it checks some internals by passing variables - "files": ["i18n.test.js"], - "rules": { - "agama-i18n/string-literals": "off" - } + "allowEmptyReject": true } ], - "globals": { - "require": false, - "module": false + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "camelcase": "off", + "comma-dangle": "off", + "curly": "off", + "jsx-quotes": "off", + "key-spacing": "off", + "no-console": "off", + "quotes": "off", + "react/jsx-curly-spacing": "off", + "react/jsx-indent-props": "off", + "react/prop-types": "off", + "space-before-function-paren": "off", + "n/no-callback-literal": "off" + }, + "overrides": [ + { + // do not check translations in the testing or development files + "files": [ + "*.test.*", + "test-utils.js" + ], + "rules": { + "i18next/no-literal-string": "off" + } + }, + { + // do not check translation arguments in the test, it checks some internals by passing variables + "files": [ + "i18n.test.js" + ], + "rules": { + "agama-i18n/string-literals": "off" + } } + ], + "globals": { + "require": false, + "module": false + } } diff --git a/web/.prettierignore b/web/.prettierignore index 1af773a716..85883df3d9 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -1 +1,5 @@ -src/lib/cockpit.js +dist +po +public +share +src/lib diff --git a/web/.prettierrc b/web/.prettierrc index 585de22956..de753c537d 100644 --- a/web/.prettierrc +++ b/web/.prettierrc @@ -1,7 +1,3 @@ { - "arrowParens": "avoid", - "printWidth": 100, - "semi": true, - "singleQuote": false, - "trailingComma": "none" + "printWidth": 100 } diff --git a/web/.stylelintrc.json b/web/.stylelintrc.json index 2415dd3d08..a7365de1cd 100644 --- a/web/.stylelintrc.json +++ b/web/.stylelintrc.json @@ -1,7 +1,11 @@ { + "plugins": [ + "stylelint-prettier" + ], "extends": [ "stylelint-config-standard", - "stylelint-config-standard-scss" + "stylelint-config-standard-scss", + "stylelint-prettier/recommended" ], "rules": { "at-rule-empty-line-before": null, diff --git a/web/README.md b/web/README.md index 79ee998455..41a5b62405 100644 --- a/web/README.md +++ b/web/README.md @@ -30,7 +30,7 @@ machine (a virtual machine as well). In that case run AGAMA_SERVER=https://: npm run server -- --open ``` -Where `AGAMA_SERVER` is the IP address, the hostname or the full URL of the +Where `AGAMA_SERVER` is the IP address, the hostname or the full URL of the running Agama server instance. This is especially useful if you use the Live ISO which does not contain any development tools, you can develop the web frontend easily from your workstation. diff --git a/web/__mocks__/svg.js b/web/__mocks__/svg.js index f6f5bae744..c1b1273712 100644 --- a/web/__mocks__/svg.js +++ b/web/__mocks__/svg.js @@ -1,6 +1,6 @@ -import React from 'react'; +import React from "react"; -export default ({...props}) => ( +export default ({ ...props }) => ( // Simple SVG square based on a wikimedia example https://commons.wikimedia.org/wiki/SVG_examples diff --git a/web/babel.config.js b/web/babel.config.js index da01f98218..c2bda356f1 100644 --- a/web/babel.config.js +++ b/web/babel.config.js @@ -1,16 +1,13 @@ const { NODE_ENV } = process.env; -const presets = [ - '@babel/preset-react', - ['@babel/preset-env', { targets: { node: 'current' } }] -]; +const presets = ["@babel/preset-react", ["@babel/preset-env", { targets: { node: "current" } }]]; const plugins = []; -if (!['production', 'test'].includes(NODE_ENV)) { - plugins.push('react-refresh/babel'); +if (!["production", "test"].includes(NODE_ENV)) { + plugins.push("react-refresh/babel"); } module.exports = { presets, - plugins + plugins, }; diff --git a/web/cspell.json b/web/cspell.json index d1926048ce..ae6c026f21 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -84,11 +84,5 @@ ] } ], - "dictionaries": [ - "custom", - "css", - "en-common-misspelling", - "fullstack", - "html" - ] + "dictionaries": ["custom", "css", "en-common-misspelling", "fullstack", "html"] } diff --git a/web/jest.config.js b/web/jest.config.js index f416de4c8f..670ff97b57 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -3,8 +3,8 @@ * https://jestjs.io/docs/configuration */ -const { pathsToModuleNameMapper } = require('ts-jest'); -const { compilerOptions } = require('./tsconfig'); +const { pathsToModuleNameMapper } = require("ts-jest"); +const { compilerOptions } = require("./tsconfig"); module.exports = { // All imported modules in your tests should be mocked automatically @@ -23,18 +23,13 @@ module.exports = { collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "src/**/*.{js,jsx}", - "!src/lib/*.js" - ], + collectCoverageFrom: ["src/**/*.{js,jsx}", "!src/lib/*.js"], // The directory where Jest should output its coverage files coverageDirectory: "coverage", // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: [ - "/node_modules/" - ], + coveragePathIgnorePatterns: ["/node_modules/"], // Indicates which provider should be used to instrument code for coverage // coverageProvider: "babel", @@ -66,8 +61,7 @@ module.exports = { // globalTeardown: undefined, // A set of global variables that need to be available in all test environments - globals: { - }, + globals: {}, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // maxWorkers: "50%", @@ -141,9 +135,7 @@ module.exports = { // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], - setupFilesAfterEnv: [ - "/src/setupTests.js" - ], + setupFilesAfterEnv: ["/src/setupTests.js"], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, @@ -190,7 +182,7 @@ module.exports = { // transform: undefined, transform: { "\\.m?jsx?$": "babel-jest", - "\\.(css|svg)$": "jest-transform-stub" + "\\.(css|svg)$": "jest-transform-stub", }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation diff --git a/web/package-lock.json b/web/package-lock.json index aebca1daf9..4499acd7f6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -48,6 +48,7 @@ "css-loader": "^7.1.1", "css-minimizer-webpack-plugin": "^6.0.0", "eslint": "^8.53.0", + "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", "eslint-config-standard-jsx": "^11.0.0", "eslint-config-standard-react": "^13.0.0", @@ -71,6 +72,7 @@ "jsdoc": "^4.0.2", "mini-css-extract-plugin": "^2.7.6", "po2json": "^1.0.0-alpha", + "prettier": "^3.3.2", "qunit": "^2.20.0", "react-refresh": "^0.14.0", "react-refresh-typescript": "^2.0.9", @@ -80,6 +82,7 @@ "stylelint": "^16.5.0", "stylelint-config-standard": "^36.0.0", "stylelint-config-standard-scss": "^13.1.0", + "stylelint-prettier": "^5.0.0", "stylelint-webpack-plugin": "^5.0.0", "terser-webpack-plugin": "^5.3.9", "ts-jest": "^29.2.2", @@ -8714,6 +8717,19 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-config-standard": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", @@ -9847,6 +9863,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-equals": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", @@ -15920,6 +15943,35 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -17721,6 +17773,23 @@ } } }, + "node_modules/stylelint-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stylelint-prettier/-/stylelint-prettier-5.0.0.tgz", + "integrity": "sha512-RHfSlRJIsaVg5Br94gZVdWlz/rBTyQzZflNE6dXvSxt/GthWMY3gEHsWZEBaVGg7GM+XrtVSp4RznFlB7i0oyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "prettier": ">=3.0.0", + "stylelint": ">=16.0.0" + } + }, "node_modules/stylelint-scss": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.3.0.tgz", diff --git a/web/package.json b/web/package.json index 4d88a8871e..861ea8933a 100644 --- a/web/package.json +++ b/web/package.json @@ -53,6 +53,7 @@ "css-loader": "^7.1.1", "css-minimizer-webpack-plugin": "^6.0.0", "eslint": "^8.53.0", + "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", "eslint-config-standard-jsx": "^11.0.0", "eslint-config-standard-react": "^13.0.0", @@ -76,6 +77,7 @@ "jsdoc": "^4.0.2", "mini-css-extract-plugin": "^2.7.6", "po2json": "^1.0.0-alpha", + "prettier": "^3.3.2", "qunit": "^2.20.0", "react-refresh": "^0.14.0", "react-refresh-typescript": "^2.0.9", @@ -85,6 +87,7 @@ "stylelint": "^16.5.0", "stylelint-config-standard": "^36.0.0", "stylelint-config-standard-scss": "^13.1.0", + "stylelint-prettier": "^5.0.0", "stylelint-webpack-plugin": "^5.0.0", "terser-webpack-plugin": "^5.3.9", "ts-jest": "^29.2.2", diff --git a/web/src/App.jsx b/web/src/App.jsx index 81a7a8d03b..4524a290ac 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -59,7 +59,7 @@ function App() { return ; } - if ((selectedProduct === undefined) && (location.pathname !== "/products")) { + if (selectedProduct === undefined && location.pathname !== "/products") { return ; } diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index 2fd0ac3986..74f65dbf48 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -41,27 +41,27 @@ jest.mock("~/queries/software", () => ({ useProduct: () => { return { products: mockProducts, - selectedProduct: mockSelectedProduct + selectedProduct: mockSelectedProduct, }; }, - useProductChanges: () => jest.fn() + useProductChanges: () => jest.fn(), })); jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), - useL10nConfigChanges: () => jest.fn() + useL10nConfigChanges: () => jest.fn(), })); const mockClientStatus = { connected: true, error: false, phase: STARTUP, - status: BUSY + status: BUSY, }; jest.mock("~/context/installer", () => ({ ...jest.requireActual("~/context/installer"), - useInstallerClientStatus: () => mockClientStatus + useInstallerClientStatus: () => mockClientStatus, })); // Mock some components, @@ -80,14 +80,14 @@ describe("App", () => { l10n: { getUIKeymap: jest.fn().mockResolvedValue("en"), getUILocale: jest.fn().mockResolvedValue("en_us"), - setUILocale: jest.fn().mockResolvedValue("en_us") - } + setUILocale: jest.fn().mockResolvedValue("en_us"), + }, }; }); mockProducts = [ { id: "openSUSE", name: "openSUSE Tumbleweed" }, - { id: "Leap Micro", name: "openSUSE Micro" } + { id: "Leap Micro", name: "openSUSE Micro" }, ]; }); diff --git a/web/src/MainLayout.jsx b/web/src/MainLayout.jsx index 6ed3d31994..d85734d3d0 100644 --- a/web/src/MainLayout.jsx +++ b/web/src/MainLayout.jsx @@ -23,10 +23,22 @@ import React, { Suspense } from "react"; import { Outlet, NavLink, useNavigate } from "react-router-dom"; import { Button, - Masthead, MastheadContent, MastheadToggle, MastheadMain, MastheadBrand, - Nav, NavItem, NavList, - Page, PageSidebar, PageSidebarBody, PageToggleButton, - Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem + Masthead, + MastheadContent, + MastheadToggle, + MastheadMain, + MastheadBrand, + Nav, + NavItem, + NavList, + Page, + PageSidebar, + PageSidebarBody, + PageToggleButton, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, } from "@patternfly/react-core"; import { Icon, Loading } from "~/components/layout"; import { About, InstallerOptions, LogsButton } from "~/components/core"; @@ -90,7 +102,7 @@ const ChangeProductButton = () => { const Sidebar = () => { // TODO: Improve this and/or extract the NavItem to a wrapper component. - const links = rootRoutes.map(r => { + const links = rootRoutes.map((r) => { if (!r.handle || r.handle.hidden) return null; // eslint-disable-next-line agama-i18n/string-literals @@ -99,12 +111,14 @@ const Sidebar = () => { return ( - [className, isActive ? "pf-m-current" : ""].join(" ")}> - {name} - - } + component={({ className }) => ( + [className, isActive ? "pf-m-current" : ""].join(" ")} + > + {name} + + )} /> ); }); @@ -129,11 +143,7 @@ const Sidebar = () => { */ export default function Root() { return ( - } - sidebar={} - > + } sidebar={}> }> diff --git a/web/src/SimpleLayout.jsx b/web/src/SimpleLayout.jsx index 99ea0b3320..bb0d65d9bf 100644 --- a/web/src/SimpleLayout.jsx +++ b/web/src/SimpleLayout.jsx @@ -22,9 +22,13 @@ import React, { Suspense } from "react"; import { Outlet } from "react-router-dom"; import { - Masthead, MastheadContent, + Masthead, + MastheadContent, Page, - Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, } from "@patternfly/react-core"; import { InstallerOptions } from "./components/core"; import { _ } from "~/i18n"; @@ -34,7 +38,11 @@ import { Loading } from "./components/layout"; * Simple layout for displaying content that comes before product configuration * TODO: improve documentation */ -export default function SimpleLayout({ showOutlet = true, showInstallerOptions = false, children }) { +export default function SimpleLayout({ + showOutlet = true, + showInstallerOptions = false, + children, +}) { return ( @@ -42,17 +50,13 @@ export default function SimpleLayout({ showOutlet = true, showInstallerOptions = - - {showInstallerOptions && } - + {showInstallerOptions && } - }> - {showOutlet ? : children} - + }>{showOutlet ? : children} ); } diff --git a/web/src/agama.js b/web/src/agama.js index f39ecbf1fd..cd1d89287f 100644 --- a/web/src/agama.js +++ b/web/src/agama.js @@ -41,10 +41,8 @@ agama.locale = function locale(po) { const header = po[""]; if (header) { - if (header["plural-forms"]) - plural_fn = header["plural-forms"]; - if (header.language) - agama.language = header.language; + if (header["plural-forms"]) plural_fn = header["plural-forms"]; + if (header.language) agama.language = header.language; } } else if (po === null) { translations = {}; @@ -87,7 +85,7 @@ agama.ngettext = function ngettext(str1, strN, n) { // the plural function either returns direct index (integer) in the plural // translations or a boolean indicating simple plural form which // needs to be converted to index 0 (singular) or 1 (plural) - let index = (plural_index === true ? 1 : plural_index || 0); + let index = plural_index === true ? 1 : plural_index || 0; // skip the `null` item in the list generated by cockpit-po-plugin // TODO: get rid of that later @@ -98,7 +96,7 @@ agama.ngettext = function ngettext(str1, strN, n) { } // fallback, return the original text - return (n === 1) ? str1 : strN; + return n === 1 ? str1 : strN; }; // register a global object so it can be accessed from a separate po.js script diff --git a/web/src/assets/fonts.scss b/web/src/assets/fonts.scss index 4d8c1d1641..98b1961d65 100644 --- a/web/src/assets/fonts.scss +++ b/web/src/assets/fonts.scss @@ -13,38 +13,38 @@ /* LatoLatin-Regular */ @font-face { - font-family: Lato; - src: url("./fonts/LatoLatin-Regular.woff2") format("woff2"); - font-style: normal; - font-weight: normal; - text-rendering: optimizelegibility; + font-family: Lato; + src: url("./fonts/LatoLatin-Regular.woff2") format("woff2"); + font-style: normal; + font-weight: normal; + text-rendering: optimizelegibility; } /* LatoLatin-Italic */ @font-face { - font-family: Lato; - src: url("./fonts/LatoLatin-Italic.woff2") format("woff2"); - font-style: italic; - font-weight: normal; - text-rendering: optimizelegibility; + font-family: Lato; + src: url("./fonts/LatoLatin-Italic.woff2") format("woff2"); + font-style: italic; + font-weight: normal; + text-rendering: optimizelegibility; } /* LatoLatin-Bold */ @font-face { - font-family: Lato; - src: url("./fonts/LatoLatin-Bold.woff2") format("woff2"); - font-style: normal; - font-weight: bold; - text-rendering: optimizelegibility; + font-family: Lato; + src: url("./fonts/LatoLatin-Bold.woff2") format("woff2"); + font-style: normal; + font-weight: bold; + text-rendering: optimizelegibility; } /* LatoLatin-BoldItalic */ @font-face { - font-family: Lato; - src: url("./fonts/LatoLatin-BoldItalic.woff2") format("woff2"); - font-style: italic; - font-weight: bold; - text-rendering: optimizelegibility; + font-family: Lato; + src: url("./fonts/LatoLatin-BoldItalic.woff2") format("woff2"); + font-style: italic; + font-weight: bold; + text-rendering: optimizelegibility; } /** @@ -61,9 +61,11 @@ font-family: Poppins; font-style: normal; font-weight: 300; - src: local(""), - url("./fonts/poppins-v19-latin-ext_latin_devanagari-300.woff2") format("woff2"), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./fonts/poppins-v19-latin-ext_latin_devanagari-300.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + src: + local(""), + url("./fonts/poppins-v19-latin-ext_latin_devanagari-300.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./fonts/poppins-v19-latin-ext_latin_devanagari-300.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* poppins-300italic - latin-ext_latin_devanagari */ @@ -71,9 +73,11 @@ font-family: Poppins; font-style: italic; font-weight: 300; - src: local(""), - url("./fonts/poppins-v19-latin-ext_latin_devanagari-300italic.woff2") format("woff2"), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./fonts/poppins-v19-latin-ext_latin_devanagari-300italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + src: + local(""), + url("./fonts/poppins-v19-latin-ext_latin_devanagari-300italic.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./fonts/poppins-v19-latin-ext_latin_devanagari-300italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* poppins-regular - latin-ext_latin_devanagari */ @@ -81,9 +85,11 @@ font-family: Poppins; font-style: normal; font-weight: 400; - src: local(""), - url("./fonts/poppins-v19-latin-ext_latin_devanagari-regular.woff2") format("woff2"), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./fonts/poppins-v19-latin-ext_latin_devanagari-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + src: + local(""), + url("./fonts/poppins-v19-latin-ext_latin_devanagari-regular.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./fonts/poppins-v19-latin-ext_latin_devanagari-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* poppins-500 - latin-ext_latin_devanagari */ @@ -91,9 +97,11 @@ font-family: Poppins; font-style: normal; font-weight: 500; - src: local(""), - url("./fonts/poppins-v19-latin-ext_latin_devanagari-500.woff2") format("woff2"), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./fonts/poppins-v19-latin-ext_latin_devanagari-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + src: + local(""), + url("./fonts/poppins-v19-latin-ext_latin_devanagari-500.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./fonts/poppins-v19-latin-ext_latin_devanagari-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /* poppins-500italic - latin-ext_latin_devanagari */ @@ -101,9 +109,11 @@ font-family: Poppins; font-style: italic; font-weight: 500; - src: local(""), - url("./fonts/poppins-v19-latin-ext_latin_devanagari-500italic.woff2") format("woff2"), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./fonts/poppins-v19-latin-ext_latin_devanagari-500italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + src: + local(""), + url("./fonts/poppins-v19-latin-ext_latin_devanagari-500italic.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./fonts/poppins-v19-latin-ext_latin_devanagari-500italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } /** @@ -118,7 +128,11 @@ font-family: "Roboto Mono"; font-style: normal; font-weight: 400; - src: local(""), - url("./fonts/roboto-mono-v13-vietnamese_latin-ext_latin_greek_cyrillic-ext_cyrillic-regular.woff2") format("woff2"), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./fonts/roboto-mono-v13-vietnamese_latin-ext_latin_greek_cyrillic-ext_cyrillic-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + src: + local(""), + url("./fonts/roboto-mono-v13-vietnamese_latin-ext_latin_greek_cyrillic-ext_cyrillic-regular.woff2") + format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./fonts/roboto-mono-v13-vietnamese_latin-ext_latin_greek_cyrillic-ext_cyrillic-regular.woff") + format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } diff --git a/web/src/assets/styles/app.scss b/web/src/assets/styles/app.scss index dab621c696..c8dfab146e 100644 --- a/web/src/assets/styles/app.scss +++ b/web/src/assets/styles/app.scss @@ -1,7 +1,11 @@ // Better alignment for expandable section with a sibling list ul.pf-v5-c-list + div.pf-v5-c-expandable-section { > button { - margin-inline-start: calc(var(--pf-v5-global--spacer--lg) - var(--pf-v5-global--spacer--sm) - var(--pf-v5-global--icon--FontSize--sm)); + margin-inline-start: calc( + var(--pf-v5-global--spacer--lg) - var(--pf-v5-global--spacer--sm) - var( + --pf-v5-global--icon--FontSize--sm + ) + ); } > div { diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index f580648b72..be65e47243 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -19,7 +19,7 @@ [data-type="agama/page-menu"] { > button { - --pf-v5-c-button--PaddingRight: 0 + --pf-v5-c-button--PaddingRight: 0; } a { @@ -66,7 +66,7 @@ ul[data-type="agama/list"] { background: var(--color-gray-light); margin-block-end: 0; - &:nth-child(n+2) { + &:nth-child(n + 2) { border-top: 0; } @@ -244,7 +244,8 @@ table[data-type="agama/tree-table"] { padding-inline-start: calc(var(--spacer-large) * 1.4); } - &.pf-m-tree-view-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr).pf-m-tree-view-details-expanded { + &.pf-m-tree-view-grid-md.pf-v5-c-table + tr:where(.pf-v5-c-table__tr).pf-m-tree-view-details-expanded { padding-block-end: var(--spacer-smaller); } @@ -258,11 +259,15 @@ table[data-type="agama/tree-table"] { display: inherit; } - &.pf-m-tree-view-grid-md.pf-v5-c-table tbody:where(.pf-v5-c-table__tbody) tr:where(.pf-v5-c-table__tr)::before { + &.pf-m-tree-view-grid-md.pf-v5-c-table + tbody:where(.pf-v5-c-table__tbody) + tr:where(.pf-v5-c-table__tr)::before { inset-inline-start: 0; } - &.pf-v5-c-table.pf-m-compact tr:where(.pf-v5-c-table__tr):not(.pf-v5-c-table__expandable-row) > *:last-child { + &.pf-v5-c-table.pf-m-compact + tr:where(.pf-v5-c-table__tr):not(.pf-v5-c-table__expandable-row) + > *:last-child { padding-inline-end: 8px; } @@ -277,7 +282,7 @@ table.devices-table { tr.dimmed-row { background-color: #fff; opacity: 0.8; - background: repeating-linear-gradient( -45deg, #fcfcff, #fcfcff 3px, #fff 3px, #fff 10px ); + background: repeating-linear-gradient(-45deg, #fcfcff, #fcfcff 3px, #fff 3px, #fff 10px); td { color: var(--color-gray-dimmed); @@ -304,7 +309,7 @@ table.proposal-result { tbody tr[aria-level="2"] th .pf-v5-c-table__tree-view-main { padding-inline-start: calc( - var(--pf-v5-c-table--m-compact--cell--first-last-child--PaddingLeft) + var(--spacer-large) + var(--pf-v5-c-table--m-compact--cell--first-last-child--PaddingLeft) + var(--spacer-large) ); } /** End of temporary hack */ @@ -324,7 +329,7 @@ table.proposal-result { &.selected::after { --arrow-size: var(--spacer-small, 10px); - content:''; + content: ""; position: absolute; bottom: -1px; left: 50%; @@ -357,8 +362,8 @@ table.proposal-result { [role="dialog"] { section:not([class^="pf-c"]) { > svg:first-child { - block-size: 24px; - inline-size: 24px; + block-size: 24px; + inline-size: 24px; } h2 { diff --git a/web/src/assets/styles/composition.scss b/web/src/assets/styles/composition.scss index eebf2a6c1d..e6f7956fd3 100644 --- a/web/src/assets/styles/composition.scss +++ b/web/src/assets/styles/composition.scss @@ -6,14 +6,17 @@ display: flex; flex-direction: column; flex: 1 1 0; - gap: 0 + gap: 0; } form > div:nth-child(2) { overflow-y: auto; min-block-size: 120px; margin-block-end: var(--spacer-medium); - table { background: transparent; } + + table { + background: transparent; + } } form > div:last-child { diff --git a/web/src/assets/styles/global.scss b/web/src/assets/styles/global.scss index 6bf961bd7d..0a0faf460e 100644 --- a/web/src/assets/styles/global.scss +++ b/web/src/assets/styles/global.scss @@ -1,24 +1,29 @@ // Global CSS starts -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { // margin: 0; font-family: var(--ff-headlines); font-weight: var(--fw-bold); } h1 { - font-size: var(--pf-v5-global--FontSize--2xl) + font-size: var(--pf-v5-global--FontSize--2xl); } h2 { - font-size: var(--pf-v5-global--FontSize--xl) + font-size: var(--pf-v5-global--FontSize--xl); } h3 { - font-size: var(--pf-v5-global--FontSize--lg) + font-size: var(--pf-v5-global--FontSize--lg); } h4 { - font-size: var(--pf-v5-global--FontSize--md) + font-size: var(--pf-v5-global--FontSize--md); } a { diff --git a/web/src/assets/styles/index.scss b/web/src/assets/styles/index.scss index e97db6b414..d8a72887da 100644 --- a/web/src/assets/styles/index.scss +++ b/web/src/assets/styles/index.scss @@ -9,4 +9,3 @@ @use "~/assets/styles/utilities.scss"; @use "~/assets/styles/composition.scss"; @use "~/assets/styles/blocks.scss"; - diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index 4cf2b70cd6..4fbdc7b943 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -131,7 +131,9 @@ .pf-v5-c-switch { // We prefer having same label color for checked and not checked switches - --pf-v5-c-switch__input--not-checked__label--Color: var(--pf-v5-c-switch__input--checked__label--Color); + --pf-v5-c-switch__input--not-checked__label--Color: var( + --pf-v5-c-switch__input--checked__label--Color + ); } // Make the switch focus looks like the rest @@ -143,9 +145,7 @@ // Avoid form select toggle icon overlap input Text .pf-v5-c-form-control__toggle-icon { padding-inline-end: 0; - margin-inline-start: calc( - var(--pf-v5-c-form-control__toggle-icon--PaddingRight) * 2 - ); + margin-inline-start: calc(var(--pf-v5-c-form-control__toggle-icon--PaddingRight) * 2); } // Adjust icons for a menu item @@ -230,7 +230,8 @@ .pf-v5-c-table tr[aria-level="1"] { border-block-end: 0; - border-block-start: var(--pf-v5-c-table--border-width--base) solid var(--pf-v5-c-table--BorderColor); + border-block-start: var(--pf-v5-c-table--border-width--base) solid + var(--pf-v5-c-table--BorderColor); } .pf-v5-c-table tr[aria-level="2"] { @@ -251,7 +252,6 @@ } } - // New-ui overrides // ================ @@ -260,7 +260,7 @@ fill: var(--pf-v5-c-nav__link--Color); } -.pf-v5-c-page__sidebar-body{ +.pf-v5-c-page__sidebar-body { fill: white; } @@ -275,8 +275,12 @@ // that knowst that link "isActive") .pf-v5-c-tabs__link.pf-m-current { - --pf-v5-c-tabs__link--after--BorderColor: var(--pf-v5-c-tabs__item--m-current__link--after--BorderColor); - --pf-v5-c-tabs__link--after--BorderWidth: var(--pf-v5-c-tabs__item--m-current__link--after--BorderWidth); + --pf-v5-c-tabs__link--after--BorderColor: var( + --pf-v5-c-tabs__item--m-current__link--after--BorderColor + ); + --pf-v5-c-tabs__link--after--BorderWidth: var( + --pf-v5-c-tabs__item--m-current__link--after--BorderWidth + ); } // Color for icons in Masthead diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss index 980c327205..37aad08505 100644 --- a/web/src/assets/styles/utilities.scss +++ b/web/src/assets/styles/utilities.scss @@ -125,7 +125,11 @@ background-color: #fff; background-repeat: no-repeat; background-attachment: local, local, scroll, scroll; - background-size: 100% 48px, 100% 48px, 100% 16px, 100% 16px; + background-size: + 100% 48px, + 100% 48px, + 100% 16px, + 100% 16px; } // FIXME: drop as soon as Tip component gets rethought / refactored diff --git a/web/src/client/dbus.js b/web/src/client/dbus.js index da36d1c604..d3b74cc18d 100644 --- a/web/src/client/dbus.js +++ b/web/src/client/dbus.js @@ -116,9 +116,7 @@ class DBusClient { * @return {Promise} DBusProxies object */ async proxies(iface, path_namespace, options) { - const all = await this.client.proxies( - iface, path_namespace, { watch: true, ...options } - ); + const all = await this.client.proxies(iface, path_namespace, { watch: true, ...options }); await all.wait(); return all; } @@ -148,15 +146,16 @@ class DBusClient { let property; try { - const result = await this.client.call( - path, "org.freedesktop.DBus.Properties", "Get", [iface, name] - ); + const result = await this.client.call(path, "org.freedesktop.DBus.Properties", "Get", [ + iface, + name, + ]); property = result[0]; } catch (error) { console.warn(`Could not get the ${name} property in ${iface}`, error); } - return (property === undefined) ? null : property.v; + return property === undefined ? null : property.v; } /** @@ -172,14 +171,14 @@ class DBusClient { { path, interface: "org.freedesktop.DBus.Properties", - member: "PropertiesChanged" + member: "PropertiesChanged", }, (_path, _iface, _signal, args) => { const [source_iface, changes, invalid] = args; if (iface === source_iface) { handler(changes, invalid); } - } + }, ); return remove; } diff --git a/web/src/client/dbus.test.js b/web/src/client/dbus.test.js index ec0e270765..38f9c67ff0 100644 --- a/web/src/client/dbus.test.js +++ b/web/src/client/dbus.test.js @@ -25,13 +25,13 @@ import DBusClient from "./dbus"; import cockpit from "../lib/cockpit"; const proxyObject = { - wait: jest.fn().mockResolvedValue(null) + wait: jest.fn().mockResolvedValue(null), }; const cockpitDBusClient = { proxy: jest.fn().mockReturnValue(proxyObject), proxies: jest.fn().mockReturnValue(proxyObject), - call: jest.fn().mockReturnValue(true) + call: jest.fn().mockReturnValue(true), }; describe("DBusClient", () => { @@ -43,11 +43,13 @@ describe("DBusClient", () => { it("returns a proxy for the given iface and path", async () => { const client = new DBusClient("org.opensuse.Agama.Manager1"); const proxy = await client.proxy( - "org.opensuse.Agama.Manager1", "/org/opensuse/Agama/Manager1" + "org.opensuse.Agama.Manager1", + "/org/opensuse/Agama/Manager1", ); expect(cockpitDBusClient.proxy).toHaveBeenCalledWith( - "org.opensuse.Agama.Manager1", "/org/opensuse/Agama/Manager1", - { watch: true } + "org.opensuse.Agama.Manager1", + "/org/opensuse/Agama/Manager1", + { watch: true }, ); expect(proxy).toBe(proxyObject); }); @@ -71,13 +73,13 @@ describe("DBusClient", () => { "org.opensuse.Agama.Software1", "/org/opensuse/Agama/Software1", "SelectProduct", - ["alp"] + ["alp"], ); expect(cockpitDBusClient.call).toHaveBeenCalledWith( "org.opensuse.Agama.Software1", "/org/opensuse/Agama/Software1", "SelectProduct", - ["alp"] + ["alp"], ); expect(result).toEqual(true); }); diff --git a/web/src/client/http.js b/web/src/client/http.js index 682f17c220..3398fbb5bf 100644 --- a/web/src/client/http.js +++ b/web/src/client/http.js @@ -61,7 +61,7 @@ class WSClient { error: [], close: [], open: [], - events: [] + events: [], }; this.reconnectAttempts = 0; @@ -70,17 +70,18 @@ class WSClient { wsState() { const state = this.client.readyState; - if ((state !== SocketStates.CONNECTED) && (this.reconnectAttempts >= MAX_ATTEMPTS)) return SocketStates.UNRECOVERABLE; + if (state !== SocketStates.CONNECTED && this.reconnectAttempts >= MAX_ATTEMPTS) + return SocketStates.UNRECOVERABLE; return state; } isRecoverable() { - return (this.wsState() !== SocketStates.UNRECOVERABLE); + return this.wsState() !== SocketStates.UNRECOVERABLE; } isConnected() { - return (this.wsState() === SocketStates.CONNECTED); + return this.wsState() === SocketStates.CONNECTED; } buildClient() { @@ -254,7 +255,7 @@ class HTTPClient { const wsUrl = new URL(this.url.toString()); wsUrl.pathname = wsUrl.pathname.concat("api/ws"); - wsUrl.protocol = (this.url.protocol === "http:") ? "ws" : "wss"; + wsUrl.protocol = this.url.protocol === "http:" ? "ws" : "wss"; this._ws = new WSClient(wsUrl); return this._ws; } diff --git a/web/src/client/index.js b/web/src/client/index.js index 7816270a2e..b7131840a0 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -70,7 +70,7 @@ import { HTTPClient, WSClient } from "./http"; const createIssuesList = (product = [], software = [], storage = [], users = []) => { const list = { product, storage, software, users }; - list.isEmpty = !Object.values(list).some(v => v.length > 0); + list.isEmpty = !Object.values(list).some((v) => v.length > 0); return list; }; @@ -80,7 +80,7 @@ const createIssuesList = (product = [], software = [], storage = [], users = []) * @param {URL} url - URL of the HTTP API. * @return {InstallerClient} */ -const createClient = url => { +const createClient = (url) => { const client = new HTTPClient(url); const l10n = new L10nClient(client); // TODO: unify with the manager client @@ -115,16 +115,16 @@ const createClient = url => { * @param {IssuesHandler} handler - Callback function. * @return {() => void} - Function to deregister the callback. */ - const onIssuesChange = handler => { + const onIssuesChange = (handler) => { const unsubscribeCallbacks = []; - unsubscribeCallbacks.push(product.onIssuesChange(i => handler({ product: i }))); - unsubscribeCallbacks.push(storage.onIssuesChange(i => handler({ storage: i }))); - unsubscribeCallbacks.push(software.onIssuesChange(i => handler({ software: i }))); - unsubscribeCallbacks.push(users.onIssuesChange(i => handler({ users: i }))); + unsubscribeCallbacks.push(product.onIssuesChange((i) => handler({ product: i }))); + unsubscribeCallbacks.push(storage.onIssuesChange((i) => handler({ storage: i }))); + unsubscribeCallbacks.push(software.onIssuesChange((i) => handler({ software: i }))); + unsubscribeCallbacks.push(users.onIssuesChange((i) => handler({ users: i }))); return () => { - unsubscribeCallbacks.forEach(cb => cb()); + unsubscribeCallbacks.forEach((cb) => cb()); }; }; @@ -145,9 +145,9 @@ const createClient = url => { onIssuesChange, isConnected, isRecoverable, - onConnect: handler => client.ws().onOpen(handler), - onDisconnect: handler => client.ws().onClose(handler), - ws: () => client.ws() + onConnect: (handler) => client.ws().onOpen(handler), + onDisconnect: (handler) => client.ws().onClose(handler), + ws: () => client.ws(), }; }; diff --git a/web/src/client/manager.js b/web/src/client/manager.js index 4517d61e16..365a831b01 100644 --- a/web/src/client/manager.js +++ b/web/src/client/manager.js @@ -152,6 +152,6 @@ class ManagerClient extends WithProgress( WithStatus(ManagerBaseClient, "/manager/status", MANAGER_SERVICE), "/manager/progress", MANAGER_SERVICE, -) { } +) {} export { ManagerClient }; diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index 3c39361d07..5da50e515b 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -61,11 +61,7 @@ * @return {void} */ -const ISSUES_SOURCES = [ - "unknown", - "system", - "config", -]; +const ISSUES_SOURCES = ["unknown", "system", "config"]; const buildIssue = ({ description, details, source, severity }) => { return { @@ -219,8 +215,7 @@ const WithProgress = (superclass, progress_path, service_name) => finished: false, }; } else { - const { steps, currentStep, maxSteps, currentTitle, finished } = - await response.json(); + const { steps, currentStep, maxSteps, currentTitle, finished } = await response.json(); return { steps, total: maxSteps, @@ -266,7 +261,7 @@ const WithProgress = (superclass, progress_path, service_name) => /** * @param {string} message - Error message */ -const createError = message => { +const createError = (message) => { return { message }; }; diff --git a/web/src/client/monitor.js b/web/src/client/monitor.js index 51f2c0b54c..a298f64f17 100644 --- a/web/src/client/monitor.js +++ b/web/src/client/monitor.js @@ -24,7 +24,8 @@ import DBusClient from "./dbus"; const NAME_OWNER_CHANGED = { - interface: "org.freedesktop.DBus", member: "NameOwnerChanged" + interface: "org.freedesktop.DBus", + member: "NameOwnerChanged", }; /** diff --git a/web/src/client/network.test.js b/web/src/client/network.test.js index a42410ef70..ec2fef58e5 100644 --- a/web/src/client/network.test.js +++ b/web/src/client/network.test.js @@ -33,7 +33,7 @@ const mockWiredConnection = { method6: "manual", addresses: ["192.168.122.100/24"], nameservers: ["192.168.122.1"], - gateway4: "192.168.122.1" + gateway4: "192.168.122.1", }; const mockWirelessConnection = { @@ -44,9 +44,9 @@ const mockWirelessConnection = { passworkd: "agama.test", security: "wpa-psk", ssid: "Agama", - mode: "infrastructure" + mode: "infrastructure", }, - status: "down" + status: "down", }; const mockConnection = { @@ -57,14 +57,14 @@ const mockConnection = { method6: "manual", addresses: [{ address: "192.168.122.100", prefix: 24 }], nameservers: ["192.168.122.1"], - gateway4: "192.168.122.1" + gateway4: "192.168.122.1", }; const mockSettings = { hostname: "localhost.localdomain", connectivity: true, wireless_enabled: true, - networking_enabled: true + networking_enabled: true, }; const mockJsonFn = jest.fn(); @@ -93,7 +93,7 @@ describe("NetworkClient", () => { const client = new NetworkClient(http); mockJsonFn.mockResolvedValue([mockWiredConnection, mockWirelessConnection]); const connections = await client.connections(); - const eth0 = connections.find(c => c.id === "eth0"); + const eth0 = connections.find((c) => c.id === "eth0"); expect(eth0).toEqual(mockConnection); }); }); diff --git a/web/src/client/network/index.js b/web/src/client/network/index.js index dd8f02180e..e7a4df7545 100644 --- a/web/src/client/network/index.js +++ b/web/src/client/network/index.js @@ -35,7 +35,7 @@ const DeviceType = Object.freeze({ ETHERNET: 1, WIRELESS: 2, DUMMY: 3, - BOND: 4 + BOND: 4, }); /** @@ -54,7 +54,7 @@ const NetworkEventTypes = Object.freeze({ CONNECTION_ADDED: "connectionAdded", CONNECTION_UPDATED: "connectionUpdated", CONNECTION_REMOVED: "connectionRemoved", - SETTINGS_UPDATED: "settingsUpdated" + SETTINGS_UPDATED: "settingsUpdated", }); /** @@ -119,18 +119,20 @@ class NetworkClient { * @return {Device} */ fromApiDevice(device) { - const nameservers = (device?.ipConfig?.nameservers || []); + const nameservers = device?.ipConfig?.nameservers || []; const { ipConfig = {}, ...dev } = device; const routes4 = (ipConfig.routes4 || []).map((route) => { const [ip, netmask] = route.destination.split("/"); - const destination = (netmask !== undefined) ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; + const destination = + netmask !== undefined ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; return { ...route, destination }; }); const routes6 = (ipConfig.routes6 || []).map((route) => { const [ip, netmask] = route.destination.split("/"); - const destination = (netmask !== undefined) ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; + const destination = + netmask !== undefined ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; return { ...route, destination }; }); @@ -164,7 +166,7 @@ class NetworkClient { } fromApiConnection(connection) { - const nameservers = (connection.nameservers || []); + const nameservers = connection.nameservers || []; const addresses = (connection.addresses || []).map((address) => { const [ip, netmask] = address.split("/"); if (netmask !== undefined) { @@ -214,7 +216,7 @@ class NetworkClient { ssid: ap.ssid, hwAddress: ap.hw_address, strength: ap.strength, - security: securityFromFlags(ap.flags, ap.wpaFlags, ap.rsnFlags) + security: securityFromFlags(ap.flags, ap.wpaFlags, ap.rsnFlags), }); }); } @@ -242,31 +244,34 @@ class NetworkClient { return accessPoints .sort((a, b) => b.strength - a.strength) - .reduce((networks, ap) => { - // Do not include networks without SSID - if (!ap.ssid || ap.ssid === "") return networks; - // Do not include "duplicates" - if (knownSsids.includes(ap.ssid)) return networks; - - const network = { - ...ap, - settings: connections.find(c => c.wireless?.ssid === ap.ssid), - device: devices.find(c => c.connection === ap.ssid) - }; - - // Group networks - if (network.device) { - networks.connected.push(network); - } else if (network.settings) { - networks.configured.push(network); - } else { - networks.others.push(network); - } - - knownSsids.push(network.ssid); - - return networks; - }, { connected: [], configured: [], others: [] }); + .reduce( + (networks, ap) => { + // Do not include networks without SSID + if (!ap.ssid || ap.ssid === "") return networks; + // Do not include "duplicates" + if (knownSsids.includes(ap.ssid)) return networks; + + const network = { + ...ap, + settings: connections.find((c) => c.wireless?.ssid === ap.ssid), + device: devices.find((c) => c.connection === ap.ssid), + }; + + // Group networks + if (network.device) { + networks.connected.push(network); + } else if (network.settings) { + networks.configured.push(network); + } else { + networks.others.push(network); + } + + knownSsids.push(network.ssid); + + return networks; + }, + { connected: [], configured: [], others: [] }, + ); } /** @@ -312,7 +317,10 @@ class NetworkClient { * @return {Promise} the added connection */ async addConnection(connection) { - const response = await this.client.post("/network/connections", this.toApiConnection(connection)); + const response = await this.client.post( + "/network/connections", + this.toApiConnection(connection), + ); if (!response.ok) { console.error("Failed to post list of connections", response); return null; @@ -371,14 +379,14 @@ class NetworkClient { */ async addresses() { const conns = await this.connections(); - return conns.flatMap(c => c.addresses); + return conns.flatMap((c) => c.addresses); } /* - * Returns network general settings - * + * Returns network general settings + * * @return {Promise} - */ + */ async settings() { const response = await this.client.get("/network/state"); if (!response.ok) { @@ -410,9 +418,4 @@ class NetworkClient { } } -export { - ConnectionState, - ConnectionTypes, - NetworkClient, - NetworkEventTypes -}; +export { ConnectionState, ConnectionTypes, NetworkClient, NetworkEventTypes }; diff --git a/web/src/client/network/model.js b/web/src/client/network/model.js index d8b4d910e9..14aa4b31bd 100644 --- a/web/src/client/network/model.js +++ b/web/src/client/network/model.js @@ -33,7 +33,7 @@ const ConnectionState = Object.freeze({ ACTIVATING: 1, ACTIVATED: 2, DEACTIVATING: 3, - DEACTIVATED: 4 + DEACTIVATED: 4, }); const DeviceState = Object.freeze({ @@ -46,7 +46,7 @@ const DeviceState = Object.freeze({ NEEDAUTH: "needAuth", ACTIVATED: "activated", DEACTIVATING: "deactivating", - FAILED: "failed" + FAILED: "failed", }); /** @@ -72,14 +72,14 @@ const ConnectionTypes = Object.freeze({ BOND: "bond", BRIDGE: "bridge", VLAN: "vlan", - UNKNOWN: "unknown" + UNKNOWN: "unknown", }); const SecurityProtocols = Object.freeze({ WEP: "WEP", WPA: "WPA1", RSN: "WPA2", - _8021X: "802.1X" + _8021X: "802.1X", }); // security protocols @@ -198,7 +198,17 @@ const ApSecurityFlags = Object.freeze({ * @param {object} [options.wireless] Wireless Settings * @return {Connection} */ -const createConnection = ({ id, iface, method4, method6, gateway4, gateway6, addresses, nameservers, wireless }) => { +const createConnection = ({ + id, + iface, + method4, + method6, + gateway4, + gateway6, + addresses, + nameservers, + wireless, +}) => { const connection = { id, iface, @@ -207,7 +217,7 @@ const createConnection = ({ id, iface, method4, method6, gateway4, gateway6, add gateway4: gateway4 || "", gateway6: gateway6 || "", addresses: addresses || [], - nameservers: nameservers || [] + nameservers: nameservers || [], }; if (wireless) connection.wireless = wireless; @@ -215,7 +225,18 @@ const createConnection = ({ id, iface, method4, method6, gateway4, gateway6, add return connection; }; -const createDevice = ({ name, macAddress, method4, method6, gateway4, gateway6, addresses, nameservers, routes4, routes6 }) => { +const createDevice = ({ + name, + macAddress, + method4, + method6, + gateway4, + gateway6, + addresses, + nameservers, + routes4, + routes6, +}) => { return { name, macAddress, @@ -226,7 +247,7 @@ const createDevice = ({ name, macAddress, method4, method6, gateway4, gateway6, addresses: addresses || [], nameservers: nameservers || [], routes4: routes4 || [], - routes6: routes6 || [] + routes6: routes6 || [], }; }; @@ -240,14 +261,12 @@ const createDevice = ({ name, macAddress, method4, method6, gateway4, gateway6, * @param {string[]} [options.security] - Supported security protocols * @return {AccessPoint} */ -const createAccessPoint = ({ ssid, hwAddress, strength, security }) => ( - { - ssid, - hwAddress, - strength, - security: security || [] - } -); +const createAccessPoint = ({ ssid, hwAddress, strength, security }) => ({ + ssid, + hwAddress, + strength, + security: security || [], +}); /** * @param {number} flags - AP flags @@ -258,7 +277,7 @@ const createAccessPoint = ({ ssid, hwAddress, strength, security }) => ( const securityFromFlags = (flags, wpa_flags, rsn_flags) => { const security = []; - if ((flags & ApFlags.PRIVACY) && (wpa_flags === 0) && (rsn_flags === 0)) { + if (flags & ApFlags.PRIVACY && wpa_flags === 0 && rsn_flags === 0) { security.push(SecurityProtocols.WEP); } @@ -268,9 +287,7 @@ const securityFromFlags = (flags, wpa_flags, rsn_flags) => { if (rsn_flags > 0) { security.push(SecurityProtocols.RSN); } - if ( - (wpa_flags & ApSecurityFlags.KEY_MGMT_8021_X) || (rsn_flags & ApSecurityFlags.KEY_MGMT_8021_X) - ) { + if (wpa_flags & ApSecurityFlags.KEY_MGMT_8021_X || rsn_flags & ApSecurityFlags.KEY_MGMT_8021_X) { security.push(SecurityProtocols._8021X); } diff --git a/web/src/client/network/model.test.js b/web/src/client/network/model.test.js index e953552772..c25d998242 100644 --- a/web/src/client/network/model.test.js +++ b/web/src/client/network/model.test.js @@ -36,7 +36,7 @@ describe("createConnection", () => { addresses: [], nameservers: [], gateway4: "", - gateway6: "" + gateway6: "", }); expect(connection.wireless).toBeUndefined(); }); @@ -69,7 +69,7 @@ describe("createAccessPoint", () => { ssid: "WIFI1", hwAddress: "11:22:33:44:55:66", strength: 90, - security: [] + security: [], }); }); }); diff --git a/web/src/client/network/utils.js b/web/src/client/network/utils.js index 0e8ac55e82..b1dc153f1f 100644 --- a/web/src/client/network/utils.js +++ b/web/src/client/network/utils.js @@ -87,22 +87,23 @@ const intToIPString = (address) => { }; /** Convert a IP address from text to network byte-order -* -* FIXME: Currently it is assumed 'le' ordering which should be read from NetworkManager State -* -* @param {string} text - string representing an IPv4 address -* @return {number} IP address as network byte-order -*/ + * + * FIXME: Currently it is assumed 'le' ordering which should be read from NetworkManager State + * + * @param {string} text - string representing an IPv4 address + * @return {number} IP address as network byte-order + */ const stringToIPInt = (text) => { - if (text === "") - return 0; + if (text === "") return 0; const parts = text.split("."); const bytes = parts.map((s) => parseInt(s.trim())); let num = 0; const shift = (b) => 0x100 * num + b; - for (const n of bytes.reverse()) { num = shift(n) } + for (const n of bytes.reverse()) { + num = shift(n); + } return num; }; @@ -121,11 +122,4 @@ const formatIp = (addr) => { } }; -export { - isValidIp, - isValidIpPrefix, - intToIPString, - stringToIPInt, - formatIp, - ipPrefixFor -}; +export { isValidIp, isValidIpPrefix, intToIPString, stringToIPInt, formatIp, ipPrefixFor }; diff --git a/web/src/client/network/utils.test.js b/web/src/client/network/utils.test.js index de1a27fc9b..be79d3b627 100644 --- a/web/src/client/network/utils.test.js +++ b/web/src/client/network/utils.test.js @@ -21,7 +21,14 @@ // @ts-check -import { isValidIp, isValidIpPrefix, intToIPString, stringToIPInt, formatIp, ipPrefixFor } from "./utils"; +import { + isValidIp, + isValidIpPrefix, + intToIPString, + stringToIPInt, + formatIp, + ipPrefixFor, +} from "./utils"; describe("#isValidIp", () => { it("returns true when the IP is valid", () => { diff --git a/web/src/client/phase.js b/web/src/client/phase.js index 21edeb8278..5024a61448 100644 --- a/web/src/client/phase.js +++ b/web/src/client/phase.js @@ -26,5 +26,5 @@ export const INSTALL = 2; export default { STARTUP, CONFIG, - INSTALL + INSTALL, }; diff --git a/web/src/client/software.js b/web/src/client/software.js index 8c4ba092d0..0ecc03b073 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -336,7 +336,10 @@ class ProductBaseClient { } } -class ProductClient - extends WithIssues(ProductBaseClient, "/software/issues/product", PRODUCT_PATH) {} +class ProductClient extends WithIssues( + ProductBaseClient, + "/software/issues/product", + PRODUCT_PATH, +) {} export { ProductClient, SelectedBy, SoftwareClient }; diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js index 007271894a..301f596b15 100644 --- a/web/src/client/software.test.js +++ b/web/src/client/software.test.js @@ -103,7 +103,7 @@ describe("ProductClient", () => { mockJsonFn.mockResolvedValue({ key: "", email: "", - requirement: "Optional" + requirement: "Optional", }); const http = new HTTPClient(new URL("http://localhost")); const client = new ProductClient(http); @@ -121,7 +121,7 @@ describe("ProductClient", () => { mockJsonFn.mockResolvedValue({ key: "111222", email: "test@test.com", - requirement: "Mandatory" + requirement: "Mandatory", }); const http = new HTTPClient(new URL("http://localhost")); const client = new ProductClient(http); diff --git a/web/src/client/status.js b/web/src/client/status.js index c2a31dec24..0eb3b42306 100644 --- a/web/src/client/status.js +++ b/web/src/client/status.js @@ -24,5 +24,5 @@ export const BUSY = 1; export default { BUSY, - IDLE + IDLE, }; diff --git a/web/src/client/storage.js b/web/src/client/storage.js index d7d59fbfc4..ab58fd31d7 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -174,7 +174,7 @@ class DBusClient { const ProposalTargets = Object.freeze({ DISK: "disk", NEW_LVM_VG: "newLvmVg", - REUSED_LVM_VG: "reusedLvmVg" + REUSED_LVM_VG: "reusedLvmVg", }); /** @@ -187,7 +187,7 @@ const VolumeTargets = Object.freeze({ NEW_PARTITION: "new_partition", NEW_VG: "new_vg", DEVICE: "device", - FILESYSTEM: "filesystem" + FILESYSTEM: "filesystem", }); /** @@ -198,7 +198,7 @@ const VolumeTargets = Object.freeze({ */ const EncryptionMethods = Object.freeze({ LUKS2: "luks2", - TPM: "tpm_fde" + TPM: "tpm_fde", }); /** @@ -240,20 +240,25 @@ class DevicesManager { name: "", description: "", isDrive: false, - type: "" + type: "", }; }; /** @type {(names: string[]) => StorageDevice[]} */ const buildCollectionFromNames = (names) => { - return names.map(name => ({ ...buildDefaultDevice(), name })); + return names.map((name) => ({ ...buildDefaultDevice(), name })); }; /** @type {(sids: String[], jsonDevices: object[]) => StorageDevice[]} */ const buildCollection = (sids, jsonDevices) => { if (sids === null || sids === undefined) return []; - return sids.map(sid => buildDevice(jsonDevices.find(dev => dev.deviceInfo?.sid === sid), jsonDevices)); + return sids.map((sid) => + buildDevice( + jsonDevices.find((dev) => dev.deviceInfo?.sid === sid), + jsonDevices, + ), + ); }; /** @type {(device: StorageDevice, info: object) => void} */ @@ -314,19 +319,19 @@ class DevicesManager { type: tableInfo.type, partitions, unpartitionedSize: device.size - partitions.reduce((s, p) => s + p.size, 0), - unusedSlots: tableInfo.unusedSlots.map(s => Object.assign({}, s)) + unusedSlots: tableInfo.unusedSlots.map((s) => Object.assign({}, s)), }; }; /** @type {(device: StorageDevice, filesystemInfo: object) => void} */ const addFilesystemInfo = (device, filesystemInfo) => { - const buildMountPath = path => path.length > 0 ? path : undefined; - const buildLabel = label => label.length > 0 ? label : undefined; + const buildMountPath = (path) => (path.length > 0 ? path : undefined); + const buildLabel = (label) => (label.length > 0 ? label : undefined); device.filesystem = { sid: filesystemInfo.sid, type: filesystemInfo.type, mountPath: buildMountPath(filesystemInfo.mountPath), - label: buildLabel(filesystemInfo.label) + label: buildLabel(filesystemInfo.label), }; }; @@ -334,7 +339,7 @@ class DevicesManager { const addComponentInfo = (device, info) => { device.component = { type: info.type, - deviceNames: info.deviceNames + deviceNames: info.deviceNames, }; }; @@ -370,7 +375,7 @@ class DevicesManager { return []; } const jsonDevices = await response.json(); - return jsonDevices.map(d => buildDevice(d, jsonDevices)); + return jsonDevices.map((d) => buildDevice(d, jsonDevices)); } } @@ -394,7 +399,7 @@ class ProposalManager { */ async getAvailableDevices() { const findDevice = (devices, sid) => { - const device = devices.find(d => d.sid === sid); + const device = devices.find((d) => d.sid === sid); if (device === undefined) console.warn("Device not found: ", sid); @@ -409,7 +414,7 @@ class ProposalManager { return []; } const usable_devices = await response.json(); - return usable_devices.map(sid => findDevice(systemDevices, sid)).filter(d => d); + return usable_devices.map((sid) => findDevice(systemDevices, sid)).filter((d) => d); } /** @@ -430,18 +435,18 @@ class ProposalManager { const isAvailable = (device) => { const isChildren = (device, parentDevice) => { const partitions = parentDevice.partitionTable?.partitions || []; - return !!partitions.find(d => d.name === device.name); + return !!partitions.find((d) => d.name === device.name); }; - return !!availableDevices.find(d => d.name === device.name || isChildren(device, d)); + return !!availableDevices.find((d) => d.name === device.name || isChildren(device, d)); }; /** @type {(device: StorageDevice[]) => boolean} */ const allAvailable = (devices) => devices.every(isAvailable); const system = await this.system.getDevices(); - const mds = system.filter(d => d.type === "md" && allAvailable(d.devices)); - const vgs = system.filter(d => d.type === "lvmVg" && allAvailable(d.physicalVolumes)); + const mds = system.filter((d) => d.type === "md" && allAvailable(d.devices)); + const vgs = system.filter((d) => d.type === "lvmVg" && allAvailable(d.physicalVolumes)); return [...availableDevices, ...mds, ...vgs]; } @@ -458,7 +463,7 @@ class ProposalManager { return []; } - return response.json().then(params => params.mountPoints); + return response.json().then((params) => params.mountPoints); } /** @@ -473,7 +478,7 @@ class ProposalManager { return []; } - return response.json().then(params => params.encryptionMethods); + return response.json().then((params) => params.encryptionMethods); } /** @@ -493,7 +498,7 @@ class ProposalManager { const systemDevices = await this.system.getDevices(); const productMountPoints = await this.getProductMountPoints(); - return response.json().then(volume => { + return response.json().then((volume) => { return this.buildVolume(volume, systemDevices, productMountPoints); }); } @@ -502,7 +507,7 @@ class ProposalManager { * Gets the values of the current proposal * * @return {Promise} - */ + */ async getResult() { const settingsResponse = await this.client.get("/storage/proposal/settings"); if (!settingsResponse.ok) { @@ -524,9 +529,12 @@ class ProposalManager { */ const buildTarget = (value) => { switch (value) { - case "disk": return "DISK"; - case "newLvmVg": return "NEW_LVM_VG"; - case "reusedLvmVg": return "REUSED_LVM_VG"; + case "disk": + return "DISK"; + case "newLvmVg": + return "NEW_LVM_VG"; + case "reusedLvmVg": + return "REUSED_LVM_VG"; default: console.info(`Unknown proposal target "${value}", using "disk".`); return "DISK"; @@ -536,7 +544,7 @@ class ProposalManager { /** @todo Read installation devices from D-Bus. */ const buildInstallationDevices = (settings, devices) => { const findDevice = (name) => { - const device = devices.find(d => d.name === name); + const device = devices.find((d) => d.name === name); if (device === undefined) console.error("Device object not found: ", name); @@ -546,19 +554,19 @@ class ProposalManager { // Only consider the device assigned to a volume as installation device if it is needed // to find space in that device. For example, devices directly formatted or mounted are not // considered as installation devices. - const volumes = settings.volumes.filter(vol => ( - [VolumeTargets.NEW_PARTITION, VolumeTargets.NEW_VG].includes(vol.target)) + const volumes = settings.volumes.filter((vol) => + [VolumeTargets.NEW_PARTITION, VolumeTargets.NEW_VG].includes(vol.target), ); const values = [ settings.targetDevice, settings.targetPVDevices, - volumes.map(v => v.targetDevice) + volumes.map((v) => v.targetDevice), ].flat(); if (settings.configureBoot) values.push(settings.bootDevice); - const names = uniq(compact(values)).filter(d => d.length > 0); + const names = uniq(compact(values)).filter((d) => d.length > 0); // #findDevice returns undefined if no device is found with the given name. return compact(names.sort().map(findDevice)); @@ -574,14 +582,16 @@ class ProposalManager { settings: { ...settings, target: buildTarget(settings.target), - volumes: settings.volumes.map(v => this.buildVolume(v, systemDevices, productMountPoints)), + volumes: settings.volumes.map((v) => + this.buildVolume(v, systemDevices, productMountPoints), + ), // NOTE: strictly speaking, installation devices does not belong to the settings. It // should be a separate method instead of an attribute in the settings object. // Nevertheless, it was added here for simplicity and to avoid passing more props in some // react components. Please, do not use settings as a jumble. - installationDevices: buildInstallationDevices(settings, systemDevices) + installationDevices: buildInstallationDevices(settings, systemDevices), }, - actions + actions, }; } @@ -602,7 +612,7 @@ class ProposalManager { mountPath: volume.mountPath, snapshots: volume.snapshots, target: VolumeTargets[volume.target], - targetDevice: volume.targetDevice?.name + targetDevice: volume.targetDevice?.name, }; }; @@ -618,7 +628,7 @@ class ProposalManager { target: ProposalTargets[settings.target], targetDevice: settings.targetDevice, targetPVDevices: settings.targetPVDevices, - volumes: settings.volumes?.map(buildHttpVolume) + volumes: settings.volumes?.map(buildHttpVolume), }; }; @@ -659,11 +669,16 @@ class ProposalManager { */ const buildTarget = (value) => { switch (value) { - case "default": return "DEFAULT"; - case "new_partition": return "NEW_PARTITION"; - case "new_vg": return "NEW_VG"; - case "device": return "DEVICE"; - case "filesystem": return "FILESYSTEM"; + case "default": + return "DEFAULT"; + case "new_partition": + return "NEW_PARTITION"; + case "new_vg": + return "NEW_VG"; + case "device": + return "DEVICE"; + case "filesystem": + return "FILESYSTEM"; default: console.info(`Unknown volume target "${value}", using "default".`); return "DEFAULT"; @@ -673,7 +688,7 @@ class ProposalManager { const volume = { ...rawVolume, target: buildTarget(rawVolume.target), - targetDevice: devices.find(d => d.name === rawVolume.targetDevice) + targetDevice: devices.find((d) => d.name === rawVolume.targetDevice), }; // Indicate whether a volume is defined by the product. @@ -731,7 +746,7 @@ class DASDManager { return { path: job.path, running: job.Running, - exitCode: job.ExitCode + exitCode: job.ExitCode, }; } @@ -762,7 +777,7 @@ class DASDManager { */ async format(devices) { const proxy = await this.managerProxy(); - const devicesPath = devices.map(d => this.devicePath(d)); + const devicesPath = devices.map((d) => this.devicePath(d)); proxy.Format(devicesPath); } @@ -774,7 +789,7 @@ class DASDManager { */ async setDIAG(devices, value) { const proxy = await this.managerProxy(); - const devicesPath = devices.map(d => this.devicePath(d)); + const devicesPath = devices.map((d) => this.devicePath(d)); proxy.SetDiag(devicesPath, value); } @@ -785,7 +800,7 @@ class DASDManager { */ async enableDevices(devices) { const proxy = await this.managerProxy(); - const devicesPath = devices.map(d => this.devicePath(d)); + const devicesPath = devices.map((d) => this.devicePath(d)); proxy.Enable(devicesPath); } @@ -796,7 +811,7 @@ class DASDManager { */ async disableDevices(devices) { const proxy = await this.managerProxy(); - const devicesPath = devices.map(d => this.devicePath(d)); + const devicesPath = devices.map((d) => this.devicePath(d)); proxy.Disable(devicesPath); } @@ -817,7 +832,8 @@ class DASDManager { async getJobs() { const proxy = await this.jobsProxy(); - return Object.values(proxy).filter(p => p.Running) + return Object.values(proxy) + .filter((p) => p.Running) .map(this.buildJob); } @@ -920,7 +936,7 @@ class DASDManager { name: device.DeviceName, partitionInfo: enabled ? device.PartitionInfo : "", status: device.Status, - type: device.Type + type: device.Type, }; } @@ -1146,7 +1162,10 @@ class ZFCPManager { */ async controllersProxy() { if (!this.proxies.controllers) - this.proxies.controllers = await this.client().proxies(ZFCP_CONTROLLER_IFACE, ZFCP_CONTROLLERS_NAMESPACE); + this.proxies.controllers = await this.client().proxies( + ZFCP_CONTROLLER_IFACE, + ZFCP_CONTROLLERS_NAMESPACE, + ); return this.proxies.controllers; } @@ -1258,7 +1277,7 @@ class ZFCPManager { id: dbusBasename(proxy.path), active: proxy.Active, lunScan: proxy.LUNScan, - channel: proxy.Channel + channel: proxy.Channel, }; } @@ -1289,7 +1308,7 @@ class ZFCPManager { name: proxy.Name, channel: proxy.Channel, wwpn: proxy.WWPN, - lun: proxy.LUN + lun: proxy.LUN, }; } @@ -1608,8 +1627,12 @@ class StorageBaseClient { */ class StorageClient extends WithIssues( WithProgress( - WithStatus(StorageBaseClient, "/storage/status", SERVICE_NAME), "/storage/progress", SERVICE_NAME - ), "/storage/issues", SERVICE_NAME -) { } + WithStatus(StorageBaseClient, "/storage/status", SERVICE_NAME), + "/storage/progress", + SERVICE_NAME, + ), + "/storage/issues", + SERVICE_NAME, +) {} export { StorageClient, EncryptionMethods }; diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 704b485633..f481599a25 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -60,7 +60,7 @@ const sda = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }; @@ -77,10 +77,10 @@ const sda1 = { start: 123, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], udevPaths: [], - isEFI: true + isEFI: true, }; /** @type {StorageDevice} */ @@ -95,10 +95,10 @@ const sda2 = { start: 1789, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], udevPaths: [], - isEFI: false + isEFI: false, }; /** @type {StorageDevice} */ @@ -121,9 +121,9 @@ const sdb = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }; /** @type {StorageDevice} */ @@ -146,9 +146,9 @@ const sdc = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; /** @type {StorageDevice} */ @@ -171,9 +171,9 @@ const sdd = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; /** @type {StorageDevice} */ @@ -196,9 +196,9 @@ const sde = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; /** @type {StorageDevice} */ @@ -216,10 +216,10 @@ const md0 = { encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, devices: [], - systems : ["openSUSE Leap 15.2"], + systems: ["openSUSE Leap 15.2"], udevIds: [], udevPaths: [], - filesystem: { sid: 100, type: "ext4", mountPath: "/test", label: "system" } + filesystem: { sid: 100, type: "ext4", mountPath: "/test", label: "system" }, }; /** @type {StorageDevice} */ @@ -243,9 +243,9 @@ const raid = { encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, devices: [], - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; /** @type {StorageDevice} */ @@ -268,9 +268,9 @@ const multipath = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; /** @type {StorageDevice} */ @@ -293,9 +293,9 @@ const dasd = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; /** @type {StorageDevice} */ @@ -318,9 +318,9 @@ const sdf = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; /** @type {StorageDevice} */ @@ -335,10 +335,10 @@ const sdf1 = { start: 1024, encrypted: true, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], udevPaths: [], - isEFI: false + isEFI: false, }; /** @type {StorageDevice} */ @@ -348,7 +348,7 @@ const lvmVg = { type: "lvmVg", name: "/dev/vg0", description: "LVM", - size: 512 + size: 512, }; /** @type {StorageDevice} */ @@ -363,9 +363,9 @@ const lvmLv1 = { start: 0, encrypted: false, shrinking: { supported: 128 }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; // Define relationship between devices @@ -374,49 +374,49 @@ sda.partitionTable = { type: "gpt", partitions: [sda1, sda2], unpartitionedSize: 256, - unusedSlots: [{ start: 1234, size: 256 }] + unusedSlots: [{ start: 1234, size: 256 }], }; sda1.component = { type: "md_device", - deviceNames: ["/dev/md0"] + deviceNames: ["/dev/md0"], }; sda2.component = { type: "md_device", - deviceNames: ["/dev/md0"] + deviceNames: ["/dev/md0"], }; sdb.component = { type: "raid_device", - deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"] + deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"], }; sdc.component = { type: "raid_device", - deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"] + deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"], }; sdd.component = { type: "multipath_wire", - deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"] + deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"], }; sde.component = { type: "multipath_wire", - deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"] + deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"], }; sdf.partitionTable = { type: "gpt", partitions: [sdf1], unpartitionedSize: 1536, - unusedSlots: [] + unusedSlots: [], }; sdf1.component = { type: "physical_volume", - deviceNames: ["/dev/vg0"] + deviceNames: ["/dev/vg0"], }; md0.devices = [sda1, sda2]; @@ -427,15 +427,15 @@ raid.devices = [ name: "/dev/sdb", description: "", isDrive: false, - type: "" + type: "", }, { sid: 0, name: "/dev/sdc", description: "", isDrive: false, - type: "" - } + type: "", + }, ]; multipath.wires = [ @@ -444,22 +444,36 @@ multipath.wires = [ name: "/dev/sdd", description: "", isDrive: false, - type: "" + type: "", }, { sid: 0, name: "/dev/sde", description: "", isDrive: false, - type: "" - } + type: "", + }, ]; lvmVg.logicalVolumes = [lvmLv1]; lvmVg.physicalVolumes = [sdf1]; const systemDevices = { - sda, sda1, sda2, sdb, sdc, sdd, sde, md0, raid, multipath, dasd, sdf, sdf1, lvmVg, lvmLv1 + sda, + sda1, + sda2, + sdb, + sdc, + sdd, + sde, + md0, + raid, + multipath, + dasd, + sdf, + sdf1, + lvmVg, + lvmLv1, }; // Staging devices @@ -486,9 +500,9 @@ const sdbStaging = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }; const stagingDevices = { sdb: sdbStaging }; @@ -507,7 +521,7 @@ const contexts = { spacePolicy: "custom", spaceActions: [ { device: "/dev/sda", action: "force_delete" }, - { device: "/dev/sdb", action: "resize" } + { device: "/dev/sdb", action: "resize" }, ], volumes: [ { @@ -527,8 +541,8 @@ const contexts = { snapshotsConfigurable: true, snapshotsAffectSizes: true, adjustByRam: false, - sizeRelevantVolumes: ["/home"] - } + sizeRelevantVolumes: ["/home"], + }, }, { mountPath: "/home", @@ -547,19 +561,19 @@ const contexts = { snapshotsConfigurable: false, snapshotsAffectSizes: false, adjustByRam: false, - sizeRelevantVolumes: [] - } - } - ] + sizeRelevantVolumes: [], + }, + }, + ], }, - actions: [{ device: 2, text: "Mount /dev/sdb1 as root", subvol: false, delete: false }] + actions: [{ device: 2, text: "Mount /dev/sdb1 as root", subvol: false, delete: false }], }; }, withAvailableDevices: () => [59, 62], withIssues: () => [ { description: "Issue 1", details: "", source: 1, severity: 1 }, { description: "Issue 2", details: "", source: 1, severity: 0 }, - { description: "Issue 3", details: "", source: 2, severity: 1 } + { description: "Issue 3", details: "", source: 2, severity: 1 }, ], withoutISCSINodes: () => { cockpitProxies.iscsiNodes = {}; @@ -600,7 +614,7 @@ const contexts = { Formatted: false, Id: "0.0.019e", PartitionInfo: "", - Type: "ECKD" + Type: "ECKD", }, "/org/opensuse/Agama/Storage1/dasds/9": { path: "/org/opensuse/Agama/Storage1/dasds/9", @@ -611,8 +625,8 @@ const contexts = { Formatted: false, Id: "0.0.ffff", PartitionInfo: "/dev/dasd_sample_9", - Type: "FBA" - } + Type: "FBA", + }, }; }, withoutZFCPControllers: () => { @@ -624,14 +638,14 @@ const contexts = { path: "/org/opensuse/Agama/Storage1/zfcp_controllers/1", Active: false, LUNScan: false, - Channel: "0.0.fa00" + Channel: "0.0.fa00", }, "/org/opensuse/Agama/Storage1/zfcp_controllers/2": { path: "/org/opensuse/Agama/Storage1/zfcp_controllers/2", Active: false, LUNScan: false, - Channel: "0.0.fc00" - } + Channel: "0.0.fc00", + }, }; }, withoutZFCPDisks: () => { @@ -644,15 +658,15 @@ const contexts = { Name: "/dev/sda", Channel: "0.0.fa00", WWPN: "0x500507630703d3b3", - LUN: "0x0000000000000000" + LUN: "0x0000000000000000", }, "/org/opensuse/Agama/Storage1/zfcp_disks/2": { path: "/org/opensuse/Agama/Storage1/zfcp_disks/2", Name: "/dev/sdb", Channel: "0.0.fa00", WWPN: "0x500507630703d3b3", - LUN: "0x0000000000000001" - } + LUN: "0x0000000000000001", + }, }; }, withSystemDevices: () => [ @@ -660,7 +674,7 @@ const contexts = { deviceInfo: { sid: 59, name: "/dev/sda", - description: "" + description: "", }, blockDevice: { active: true, @@ -670,7 +684,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"] + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }, drive: { type: "disk", @@ -682,20 +696,20 @@ const contexts = { transport: "usb", info: { dellBOSS: false, - sdCard: true - } + sdCard: true, + }, }, partitionTable: { type: "gpt", partitions: [60, 61], - unusedSlots: [{ start: 1234, size: 256 }] - } + unusedSlots: [{ start: 1234, size: 256 }], + }, }, { deviceInfo: { sid: 60, name: "/dev/sda1", - description: "" + description: "", }, partition: { efi: true }, blockDevice: { @@ -706,19 +720,19 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, component: { type: "md_device", deviceNames: ["/dev/md0"], - devices: [66] - } + devices: [66], + }, }, { deviceInfo: { sid: 61, name: "/dev/sda2", - description: "" + description: "", }, partition: { efi: false }, blockDevice: { @@ -729,19 +743,19 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, component: { type: "md_device", deviceNames: ["/dev/md0"], - devices: [66] - } + devices: [66], + }, }, { deviceInfo: { sid: 62, name: "/dev/sdb", - description: "" + description: "", }, blockDevice: { active: true, @@ -751,7 +765,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }, drive: { type: "disk", @@ -763,20 +777,20 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } + sdCard: false, + }, }, component: { type: "raid_device", deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"], - devices: [67] - } + devices: [67], + }, }, { deviceInfo: { sid: 63, name: "/dev/sdc", - description: "" + description: "", }, blockDevice: { active: true, @@ -786,7 +800,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, drive: { type: "disk", @@ -798,20 +812,20 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } + sdCard: false, + }, }, component: { type: "raid_device", deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"], - devices: [67] - } + devices: [67], + }, }, { deviceInfo: { sid: 64, name: "/dev/sdd", - description: "" + description: "", }, blockDevice: { active: true, @@ -821,7 +835,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, drive: { type: "disk", @@ -833,20 +847,20 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } + sdCard: false, + }, }, component: { type: "multipath_wire", deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"], - devices: [68] - } + devices: [68], + }, }, { deviceInfo: { sid: 65, name: "/dev/sde", - description: "" + description: "", }, blockDevice: { active: true, @@ -856,7 +870,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, drive: { type: "disk", @@ -868,20 +882,20 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } + sdCard: false, + }, }, component: { type: "multipath_wire", deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"], - devices: [68] - } + devices: [68], + }, }, { deviceInfo: { sid: 66, name: "/dev/md0", - description: "EXT4 RAID" + description: "EXT4 RAID", }, blockDevice: { active: true, @@ -891,25 +905,25 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: ["openSUSE Leap 15.2"], udevIds: [], - udevPaths: [] + udevPaths: [], }, md: { level: "raid0", uuid: "12345:abcde", - devices: [60, 61] + devices: [60, 61], }, filesystem: { sid: 100, type: "ext4", mountPath: "/test", - label: "system" - } + label: "system", + }, }, { deviceInfo: { sid: 67, name: "/dev/mapper/isw_ddgdcbibhd_244", - description: "" + description: "", }, blockDevice: { active: true, @@ -919,7 +933,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, drive: { type: "raid", @@ -931,18 +945,18 @@ const contexts = { transport: "", info: { dellBOSS: true, - sdCard: false - } + sdCard: false, + }, }, raid: { - devices: ["/dev/sdb", "/dev/sdc"] - } + devices: ["/dev/sdb", "/dev/sdc"], + }, }, { deviceInfo: { sid: 68, name: "/dev/mapper/36005076305ffc73a00000000000013b4", - description: "" + description: "", }, blockDevice: { active: true, @@ -952,7 +966,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, drive: { type: "multipath", @@ -964,18 +978,18 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } + sdCard: false, + }, }, multipath: { - wires: ["/dev/sdd", "/dev/sde"] - } + wires: ["/dev/sdd", "/dev/sde"], + }, }, { deviceInfo: { sid: 69, name: "/dev/dasda", - description: "" + description: "", }, blockDevice: { active: true, @@ -985,7 +999,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, drive: { type: "dasd", @@ -997,15 +1011,15 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } - } + sdCard: false, + }, + }, }, { deviceInfo: { sid: 70, name: "/dev/sdf", - description: "" + description: "", }, blockDevice: { active: true, @@ -1015,7 +1029,7 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, drive: { type: "disk", @@ -1027,20 +1041,20 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } + sdCard: false, + }, }, partitionTable: { type: "gpt", partitions: [71], - unusedSlots: [] - } + unusedSlots: [], + }, }, { deviceInfo: { sid: 71, name: "/dev/sdf1", - description: "PV of vg0" + description: "PV of vg0", }, partition: { efi: false }, blockDevice: { @@ -1051,32 +1065,32 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, component: { type: "physical_volume", deviceNames: ["/dev/vg0"], - devices: [72] - } + devices: [72], + }, }, { deviceInfo: { sid: 72, name: "/dev/vg0", - description: "LVM" + description: "LVM", }, lvmVg: { type: "physical_volume", size: 512, physicalVolumes: [71], - logicalVolumes: [73] - } + logicalVolumes: [73], + }, }, { deviceInfo: { sid: 73, name: "/dev/vg0/lv1", - description: "" + description: "", }, blockDevice: { active: true, @@ -1086,11 +1100,11 @@ const contexts = { shrinking: { supported: 128 }, systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }, lvmLv: { - volumeGroup: [72] - } + volumeGroup: [72], + }, }, ], withStagingDevices: () => [ @@ -1098,7 +1112,7 @@ const contexts = { deviceInfo: { sid: 62, name: "/dev/sdb", - description: "" + description: "", }, drive: { type: "disk", @@ -1110,8 +1124,8 @@ const contexts = { transport: "", info: { dellBOSS: false, - sdCard: false - } + sdCard: false, + }, }, blockDevice: { active: true, @@ -1121,28 +1135,37 @@ const contexts = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] - } - } - ] + udevPaths: ["pci-0000:00-19"], + }, + }, + ], }; const mockProxy = (iface, path) => { switch (iface) { - case "org.opensuse.Agama.Storage1.ISCSI.Initiator": return cockpitProxies.iscsiInitiator; - case "org.opensuse.Agama.Storage1.ISCSI.Node": return cockpitProxies.iscsiNode[path]; - case "org.opensuse.Agama.Storage1.DASD.Manager": return cockpitProxies.dasdManager; - case "org.opensuse.Agama.Storage1.ZFCP.Manager": return cockpitProxies.zfcpManager; - case "org.opensuse.Agama.Storage1.ZFCP.Controller": return cockpitProxies.zfcpController[path]; + case "org.opensuse.Agama.Storage1.ISCSI.Initiator": + return cockpitProxies.iscsiInitiator; + case "org.opensuse.Agama.Storage1.ISCSI.Node": + return cockpitProxies.iscsiNode[path]; + case "org.opensuse.Agama.Storage1.DASD.Manager": + return cockpitProxies.dasdManager; + case "org.opensuse.Agama.Storage1.ZFCP.Manager": + return cockpitProxies.zfcpManager; + case "org.opensuse.Agama.Storage1.ZFCP.Controller": + return cockpitProxies.zfcpController[path]; } }; const mockProxies = (iface) => { switch (iface) { - case "org.opensuse.Agama.Storage1.ISCSI.Node": return cockpitProxies.iscsiNodes; - case "org.opensuse.Agama.Storage1.DASD.Device": return cockpitProxies.dasdDevices; - case "org.opensuse.Agama.Storage1.ZFCP.Controller": return cockpitProxies.zfcpControllers; - case "org.opensuse.Agama.Storage1.ZFCP.Disk": return cockpitProxies.zfcpDisks; + case "org.opensuse.Agama.Storage1.ISCSI.Node": + return cockpitProxies.iscsiNodes; + case "org.opensuse.Agama.Storage1.DASD.Device": + return cockpitProxies.dasdDevices; + case "org.opensuse.Agama.Storage1.ZFCP.Controller": + return cockpitProxies.zfcpControllers; + case "org.opensuse.Agama.Storage1.ZFCP.Disk": + return cockpitProxies.zfcpDisks; } }; @@ -1189,7 +1212,7 @@ let http; jest.mock("./http", () => { return { - HTTPClient: jest.fn().mockImplementation(() => mockHTTPClient) + HTTPClient: jest.fn().mockImplementation(() => mockHTTPClient), }; }); @@ -1202,7 +1225,7 @@ beforeEach(() => { proxy: mockProxy, proxies: mockProxies, onObjectChanged: mockOnObjectChanged, - call: mockCall + call: mockCall, }; }); @@ -1276,13 +1299,10 @@ describe("#isDeprecated", () => { describe("when the HTTP call fails", () => { beforeEach(() => { - mockGetFn.mockImplementation(path => { - if (path === "/storage/devices/dirty") - return { ok: false, json: undefined }; - else - return { ok: true, json: mockJsonFn }; - } - ); + mockGetFn.mockImplementation((path) => { + if (path === "/storage/devices/dirty") return { ok: false, json: undefined }; + else return { ok: true, json: mockJsonFn }; + }); client = new StorageClient(http); }); @@ -1305,10 +1325,7 @@ describe.skip("#onDeprecate", () => { describe("if the system was not deprecated", () => { beforeEach(() => { - emitSignal( - "/org/opensuse/Agama/Storage1", - "org.opensuse.Agama.Storage1", - {}); + emitSignal("/org/opensuse/Agama/Storage1", "org.opensuse.Agama.Storage1", {}); }); it("does not run the handler", async () => { @@ -1318,10 +1335,9 @@ describe.skip("#onDeprecate", () => { describe("if the system was deprecated", () => { beforeEach(() => { - emitSignal( - "/org/opensuse/Agama/Storage1", - "org.opensuse.Agama.Storage1", - { DeprecatedSystem: true }); + emitSignal("/org/opensuse/Agama/Storage1", "org.opensuse.Agama.Storage1", { + DeprecatedSystem: true, + }); }); it("runs the handler", async () => { @@ -1353,11 +1369,13 @@ describe("#getIssues", () => { it("returns the list of issues", async () => { const issues = await client.getIssues(); - expect(issues).toEqual(expect.arrayContaining([ - { description: "Issue 1", details: "", source: "system", severity: "error" }, - { description: "Issue 2", details: "", source: "system", severity: "warn" }, - { description: "Issue 3", details: "", source: "config", severity: "error" } - ])); + expect(issues).toEqual( + expect.arrayContaining([ + { description: "Issue 1", details: "", source: "system", severity: "error" }, + { description: "Issue 2", details: "", source: "system", severity: "warn" }, + { description: "Issue 3", details: "", source: "config", severity: "error" }, + ]), + ); }); }); }); @@ -1370,7 +1388,9 @@ describe("#getErrors", () => { it("returns the issues with error severity", async () => { const errors = await client.getErrors(); - expect(errors.map(e => e.description)).toEqual(expect.arrayContaining(["Issue 1", "Issue 3"])); + expect(errors.map((e) => e.description)).toEqual( + expect.arrayContaining(["Issue 1", "Issue 3"]), + ); }); }); @@ -1382,10 +1402,14 @@ describe.skip("#onIssuesChange", () => { const handler = jest.fn(); client.onIssuesChange(handler); - emitSignal( - "/org/opensuse/Agama/Storage1", - "org.opensuse.Agama1.Issues", - { All: { v: [["Issue 1", "", 1, 0], ["Issue 2", "", 2, 1]] } }); + emitSignal("/org/opensuse/Agama/Storage1", "org.opensuse.Agama1.Issues", { + All: { + v: [ + ["Issue 1", "", 1, 0], + ["Issue 2", "", 2, 1], + ], + }, + }); expect(handler).toHaveBeenCalledWith([ { description: "Issue 1", details: "", source: "system", severity: "warn" }, @@ -1424,13 +1448,10 @@ describe("#system", () => { describe("when the HTTP call fails", () => { beforeEach(() => { - mockGetFn.mockImplementation(path => { - if (path === "/storage/devices/system") - return { ok: false, json: undefined }; - else - return { ok: true, json: mockJsonFn }; - } - ); + mockGetFn.mockImplementation((path) => { + if (path === "/storage/devices/system") return { ok: false, json: undefined }; + else return { ok: true, json: mockJsonFn }; + }); client = new StorageClient(http); }); @@ -1473,13 +1494,10 @@ describe("#staging", () => { describe("when the HTTP call fails", () => { beforeEach(() => { - mockGetFn.mockImplementation(path => { - if (path === "/storage/devices/result") - return { ok: false, json: undefined }; - else - return { ok: true, json: mockJsonFn }; - } - ); + mockGetFn.mockImplementation((path) => { + if (path === "/storage/devices/result") return { ok: false, json: undefined }; + else return { ok: true, json: mockJsonFn }; + }); client = new StorageClient(http); }); @@ -1499,7 +1517,7 @@ describe("#proposal", () => { beforeEach(() => { response = { ok: true, json: jest.fn().mockResolvedValue(contexts.withAvailableDevices()) }; - mockGetFn.mockImplementation(path => { + mockGetFn.mockImplementation((path) => { switch (path) { case "/storage/devices/system": return { ok: true, json: jest.fn().mockResolvedValue(contexts.withSystemDevices()) }; @@ -1543,13 +1561,10 @@ describe("#proposal", () => { describe("when the HTTP call fails", () => { beforeEach(() => { - mockGetFn.mockImplementation(path => { - if (path === "/storage/product/params") - return { ok: false, json: undefined }; - else - return { ok: true, json: mockJsonFn }; - } - ); + mockGetFn.mockImplementation((path) => { + if (path === "/storage/product/params") return { ok: false, json: undefined }; + else return { ok: true, json: mockJsonFn }; + }); client = new StorageClient(http); }); @@ -1574,13 +1589,10 @@ describe("#proposal", () => { describe("when the HTTP call fails", () => { beforeEach(() => { - mockGetFn.mockImplementation(path => { - if (path === "/storage/product/params") - return { ok: false, json: undefined }; - else - return { ok: true, json: mockJsonFn }; - } - ); + mockGetFn.mockImplementation((path) => { + if (path === "/storage/product/params") return { ok: false, json: undefined }; + else return { ok: true, json: mockJsonFn }; + }); client = new StorageClient(http); }); @@ -1619,9 +1631,9 @@ describe("#proposal", () => { snapshotsConfigurable: false, snapshotsAffectSizes: false, adjustByRam: false, - sizeRelevantVolumes: [] - } - }) + sizeRelevantVolumes: [], + }, + }), }; default: return { @@ -1643,19 +1655,22 @@ describe("#proposal", () => { snapshotsConfigurable: false, snapshotsAffectSizes: false, adjustByRam: false, - sizeRelevantVolumes: [] - } - }) + sizeRelevantVolumes: [], + }, + }), }; } }; - mockGetFn.mockImplementation(path => { + mockGetFn.mockImplementation((path) => { switch (path) { case "/storage/devices/system": return { ok: true, json: jest.fn().mockResolvedValue(contexts.withSystemDevices()) }; case "/storage/product/params": - return { ok: true, json: jest.fn().mockResolvedValue({ mountPoints: ["/", "swap", "/home"] }) }; + return { + ok: true, + json: jest.fn().mockResolvedValue({ mountPoints: ["/", "swap", "/home"] }), + }; // GET for /storage/product/volume_for?path=XX default: return response(path); @@ -1686,8 +1701,8 @@ describe("#proposal", () => { snapshotsAffectSizes: false, adjustByRam: false, sizeRelevantVolumes: [], - productDefined: true - } + productDefined: true, + }, }); const generic = await client.proposal.defaultVolume(""); @@ -1710,8 +1725,8 @@ describe("#proposal", () => { snapshotsAffectSizes: false, adjustByRam: false, sizeRelevantVolumes: [], - productDefined: false - } + productDefined: false, + }, }); }); @@ -1750,7 +1765,7 @@ describe("#proposal", () => { const proposal = contexts.withProposal(); mockJsonFn.mockResolvedValue(proposal.settings); - mockGetFn.mockImplementation(path => { + mockGetFn.mockImplementation((path) => { switch (path) { case "/storage/devices/system": return { ok: true, json: jest.fn().mockResolvedValue(contexts.withSystemDevices()) }; @@ -1759,7 +1774,10 @@ describe("#proposal", () => { case "/storage/proposal/actions": return { ok: true, json: jest.fn().mockResolvedValue(proposal.actions) }; case "/storage/product/params": - return { ok: true, json: jest.fn().mockResolvedValue({ mountPoints: ["/", "swap"] }) }; + return { + ok: true, + json: jest.fn().mockResolvedValue({ mountPoints: ["/", "swap"] }), + }; } }); }); @@ -1777,7 +1795,7 @@ describe("#proposal", () => { spacePolicy: "custom", spaceActions: [ { device: "/dev/sda", action: "force_delete" }, - { device: "/dev/sdb", action: "resize" } + { device: "/dev/sdb", action: "resize" }, ], volumes: [ { @@ -1797,8 +1815,8 @@ describe("#proposal", () => { snapshotsConfigurable: true, snapshotsAffectSizes: true, sizeRelevantVolumes: ["/home"], - productDefined: true - } + productDefined: true, + }, }, { mountPath: "/home", @@ -1817,26 +1835,28 @@ describe("#proposal", () => { snapshotsConfigurable: false, snapshotsAffectSizes: false, sizeRelevantVolumes: [], - productDefined: false - } - } - ] + productDefined: false, + }, + }, + ], }); - expect(settings.installationDevices.map(d => d.name).sort()).toStrictEqual( - ["/dev/sda", "/dev/sdb"].sort() + expect(settings.installationDevices.map((d) => d.name).sort()).toStrictEqual( + ["/dev/sda", "/dev/sdb"].sort(), ); expect(actions).toStrictEqual([ - { device: 2, text: "Mount /dev/sdb1 as root", subvol: false, delete: false } + { device: 2, text: "Mount /dev/sdb1 as root", subvol: false, delete: false }, ]); }); describe("if boot is not configured", () => { beforeEach(() => { - mockJsonFn.mockResolvedValue( - { ...contexts.withProposal().settings, configureBoot: false, bootDevice: "/dev/sdc" } - ); + mockJsonFn.mockResolvedValue({ + ...contexts.withProposal().settings, + configureBoot: false, + bootDevice: "/dev/sdc", + }); }); it("does not include the boot device as installation device", async () => { @@ -1851,7 +1871,7 @@ describe("#proposal", () => { let response = { ok: true, json: jest.fn().mockResolvedValue(true) }; beforeEach(() => { - mockPutFn.mockImplementation(path => { + mockPutFn.mockImplementation((path) => { if (path === "/storage/proposal/settings") return response; return { ok: true }; @@ -1881,13 +1901,13 @@ describe("#proposal", () => { minSize: 1024, maxSize: 2048, autoSize: false, - snapshots: true + snapshots: true, }, { mountPath: "/test2", - minSize: 1024 - } - ] + minSize: 1024, + }, + ], }); expect(mockPutFn).toHaveBeenCalledWith("/storage/proposal/settings", { @@ -1905,13 +1925,13 @@ describe("#proposal", () => { minSize: 1024, maxSize: 2048, autoSize: false, - snapshots: true + snapshots: true, }, { mountPath: "/test2", - minSize: 1024 - } - ] + minSize: 1024, + }, + ], }); }); @@ -1921,7 +1941,9 @@ describe("#proposal", () => { spaceActions: [{ device: "/dev/sda", action: "resize" }], }); - expect(mockPutFn).toHaveBeenCalledWith("/storage/proposal/settings", { spacePolicy: "delete" }); + expect(mockPutFn).toHaveBeenCalledWith("/storage/proposal/settings", { + spacePolicy: "delete", + }); }); it("returns false if the call fails", async () => { @@ -1958,7 +1980,7 @@ describe.skip("#dasd", () => { hexId: 414, name: "sample_dasd_device", partitionInfo: "", - type: "ECKD" + type: "ECKD", }; const probeFn = jest.fn(); @@ -1972,7 +1994,7 @@ describe.skip("#dasd", () => { Probe: probeFn, SetDiag: setDiagFn, Enable: enableFn, - Disable: disableFn + Disable: disableFn, }; contexts.withDASDDevices(); }); @@ -2008,7 +2030,7 @@ describe.skip("#dasd", () => { hexId: 414, name: "dasd_sample_8", partitionInfo: "", - type: "ECKD" + type: "ECKD", }); expect(result).toContainEqual({ id: "9", @@ -2020,7 +2042,7 @@ describe.skip("#dasd", () => { hexId: 65535, name: "dasd_sample_9", partitionInfo: "/dev/dasd_sample_9", - type: "FBA" + type: "FBA", }); }); }); @@ -2029,16 +2051,10 @@ describe.skip("#dasd", () => { describe("#setDIAG", () => { it("requests for setting DIAG for given devices", async () => { await client.dasd.setDIAG([sampleDasdDevice], true); - expect(setDiagFn).toHaveBeenCalledWith( - ["/org/opensuse/Agama/Storage1/dasds/8"], - true - ); + expect(setDiagFn).toHaveBeenCalledWith(["/org/opensuse/Agama/Storage1/dasds/8"], true); await client.dasd.setDIAG([sampleDasdDevice], false); - expect(setDiagFn).toHaveBeenCalledWith( - ["/org/opensuse/Agama/Storage1/dasds/8"], - false - ); + expect(setDiagFn).toHaveBeenCalledWith(["/org/opensuse/Agama/Storage1/dasds/8"], false); }); }); @@ -2063,25 +2079,23 @@ describe.skip("#zfcp", () => { let disksCallbacks; const mockEventListener = (proxy, callbacks) => { - proxy.addEventListener = jest.fn().mockImplementation( - (signal, handler) => { - if (!callbacks[signal]) callbacks[signal] = []; - callbacks[signal].push(handler); - } - ); + proxy.addEventListener = jest.fn().mockImplementation((signal, handler) => { + if (!callbacks[signal]) callbacks[signal] = []; + callbacks[signal].push(handler); + }); proxy.removeEventListener = jest.fn(); }; const emitSignals = (callbacks, signal, proxy) => { - callbacks[signal].forEach(handler => handler(null, proxy)); + callbacks[signal].forEach((handler) => handler(null, proxy)); }; beforeEach(() => { client = new StorageClient(); cockpitProxies.zfcpManager = { Probe: probeFn, - AllowLUNScan: true + AllowLUNScan: true, }; controllersCallbacks = {}; @@ -2171,13 +2185,13 @@ describe.skip("#zfcp", () => { id: "1", active: false, lunScan: false, - channel: "0.0.fa00" + channel: "0.0.fa00", }); expect(result).toContainEqual({ id: "2", active: false, lunScan: false, - channel: "0.0.fc00" + channel: "0.0.fc00", }); }); }); @@ -2208,14 +2222,14 @@ describe.skip("#zfcp", () => { name: "/dev/sda", channel: "0.0.fa00", wwpn: "0x500507630703d3b3", - lun: "0x0000000000000000" + lun: "0x0000000000000000", }); expect(result).toContainEqual({ id: "2", name: "/dev/sdb", channel: "0.0.fa00", wwpn: "0x500507630703d3b3", - lun: "0x0000000000000001" + lun: "0x0000000000000001", }); }); }); @@ -2225,12 +2239,12 @@ describe.skip("#zfcp", () => { const wwpns = ["0x500507630703d3b3", "0x500507630708d3b3"]; const controllerProxy = { - GetWWPNs: jest.fn().mockReturnValue(wwpns) + GetWWPNs: jest.fn().mockReturnValue(wwpns), }; beforeEach(() => { cockpitProxies.zfcpController = { - "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy, }; }); @@ -2253,16 +2267,16 @@ describe.skip("#zfcp", () => { describe("#getLUNs", () => { const luns = { - "0x500507630703d3b3": ["0x0000000000000000", "0x0000000000000001", "0x0000000000000002"] + "0x500507630703d3b3": ["0x0000000000000000", "0x0000000000000001", "0x0000000000000002"], }; const controllerProxy = { - GetLUNs: jest.fn().mockImplementation(wwpn => luns[wwpn]) + GetLUNs: jest.fn().mockImplementation((wwpn) => luns[wwpn]), }; beforeEach(() => { cockpitProxies.zfcpController = { - "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy, }; }); @@ -2287,12 +2301,12 @@ describe.skip("#zfcp", () => { const activateFn = jest.fn().mockReturnValue(0); const controllerProxy = { - Activate: activateFn + Activate: activateFn, }; beforeEach(() => { cockpitProxies.zfcpController = { - "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy, }; }); @@ -2318,17 +2332,21 @@ describe.skip("#zfcp", () => { const activateDiskFn = jest.fn().mockReturnValue(0); const controllerProxy = { - ActivateDisk: activateDiskFn + ActivateDisk: activateDiskFn, }; beforeEach(() => { cockpitProxies.zfcpController = { - "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy, }; }); it("tries to activate the given zFCP disk", async () => { - const result = await client.zfcp.activateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + const result = await client.zfcp.activateDisk( + { id: "1" }, + "0x500507630703d3b3", + "0x0000000000000000", + ); expect(activateDiskFn).toHaveBeenCalledWith("0x500507630703d3b3", "0x0000000000000000"); expect(result).toEqual(0); }); @@ -2339,7 +2357,11 @@ describe.skip("#zfcp", () => { }); it("returns undefined", async () => { - const result = await client.zfcp.activateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + const result = await client.zfcp.activateDisk( + { id: "1" }, + "0x500507630703d3b3", + "0x0000000000000000", + ); expect(result).toBeUndefined(); }); }); @@ -2349,17 +2371,21 @@ describe.skip("#zfcp", () => { const deactivateDiskFn = jest.fn().mockReturnValue(0); const controllerProxy = { - ActivateDisk: deactivateDiskFn + ActivateDisk: deactivateDiskFn, }; beforeEach(() => { cockpitProxies.zfcpController = { - "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy + "/org/opensuse/Agama/Storage1/zfcp_controllers/1": controllerProxy, }; }); it("tries to deactivate the given zFCP disk", async () => { - const result = await client.zfcp.activateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + const result = await client.zfcp.activateDisk( + { id: "1" }, + "0x500507630703d3b3", + "0x0000000000000000", + ); expect(deactivateDiskFn).toHaveBeenCalledWith("0x500507630703d3b3", "0x0000000000000000"); expect(result).toEqual(0); }); @@ -2370,7 +2396,11 @@ describe.skip("#zfcp", () => { }); it("returns undefined", async () => { - const result = await client.zfcp.deactivateDisk({ id: "1" }, "0x500507630703d3b3", "0x0000000000000000"); + const result = await client.zfcp.deactivateDisk( + { id: "1" }, + "0x500507630703d3b3", + "0x0000000000000000", + ); expect(result).toBeUndefined(); }); }); @@ -2385,11 +2415,14 @@ describe.skip("#zfcp", () => { path: "/org/opensuse/Agama/Storage1/zfcp_controllers/1", Active: true, LUNScan: true, - Channel: "0.0.fa00" + Channel: "0.0.fa00", }); expect(handler).toHaveBeenCalledWith({ - id: "1", active: true, lunScan: true, channel: "0.0.fa00" + id: "1", + active: true, + lunScan: true, + channel: "0.0.fa00", }); }); }); @@ -2404,7 +2437,7 @@ describe.skip("#zfcp", () => { Name: "/dev/sda", Channel: "0.0.fa00", WWPN: "0x500507630703d3b3", - LUN: "0x0000000000000000" + LUN: "0x0000000000000000", }); expect(handler).toHaveBeenCalledWith({ @@ -2412,7 +2445,7 @@ describe.skip("#zfcp", () => { name: "/dev/sda", channel: "0.0.fa00", wwpn: "0x500507630703d3b3", - lun: "0x0000000000000000" + lun: "0x0000000000000000", }); }); }); @@ -2427,7 +2460,7 @@ describe.skip("#zfcp", () => { Name: "/dev/sda", Channel: "0.0.fa00", WWPN: "0x500507630703d3b3", - LUN: "0x0000000000000000" + LUN: "0x0000000000000000", }); expect(handler).toHaveBeenCalledWith({ @@ -2435,7 +2468,7 @@ describe.skip("#zfcp", () => { name: "/dev/sda", channel: "0.0.fa00", wwpn: "0x500507630703d3b3", - lun: "0x0000000000000000" + lun: "0x0000000000000000", }); }); }); @@ -2450,7 +2483,7 @@ describe.skip("#zfcp", () => { Name: "/dev/sda", Channel: "0.0.fa00", WWPN: "0x500507630703d3b3", - LUN: "0x0000000000000000" + LUN: "0x0000000000000000", }); expect(handler).toHaveBeenCalledWith({ @@ -2458,7 +2491,7 @@ describe.skip("#zfcp", () => { name: "/dev/sda", channel: "0.0.fa00", wwpn: "0x500507630703d3b3", - lun: "0x0000000000000000" + lun: "0x0000000000000000", }); }); }); @@ -2573,19 +2606,18 @@ describe("#iscsi", () => { reversePassword: "nonsecret", }; await client.iscsi.discover("192.168.100.101", 3260, options); - expect(mockPostFn).toHaveBeenCalledWith( - "/storage/iscsi/discover", - { address: "192.168.100.101", port: 3260, options }, - ); + expect(mockPostFn).toHaveBeenCalledWith("/storage/iscsi/discover", { + address: "192.168.100.101", + port: 3260, + options, + }); }); }); describe("#delete", () => { it("deletes the given iSCSI node", async () => { await client.iscsi.delete({ id: "1" }); - expect(mockDeleteFn).toHaveBeenCalledWith( - "/storage/iscsi/nodes/1", - ); + expect(mockDeleteFn).toHaveBeenCalledWith("/storage/iscsi/nodes/1"); }); }); @@ -2602,16 +2634,11 @@ describe("#iscsi", () => { const result = await client.iscsi.login({ id: "1" }, auth); expect(result).toEqual(0); - expect(mockPostFn).toHaveBeenCalledWith( - "/storage/iscsi/nodes/1/login", - auth, - ); + expect(mockPostFn).toHaveBeenCalledWith("/storage/iscsi/nodes/1/login", auth); }); it("returns 1 when the startup is invalid", async () => { - mockPostFn.mockImplementation(() => ( - { ok: false, json: mockJsonFn } - )); + mockPostFn.mockImplementation(() => ({ ok: false, json: mockJsonFn })); mockJsonFn.mockResolvedValue("InvalidStartup"); const result = await client.iscsi.login({ id: "1" }, { ...auth, startup: "invalid" }); @@ -2619,9 +2646,7 @@ describe("#iscsi", () => { }); it("returns 2 in case of an error different from an invalid startup value", async () => { - mockPostFn.mockImplementation(() => ( - { ok: false, json: mockJsonFn } - )); + mockPostFn.mockImplementation(() => ({ ok: false, json: mockJsonFn })); mockJsonFn.mockResolvedValue("Failed"); const result = await client.iscsi.login({ id: "1" }, { ...auth, startup: "invalid" }); diff --git a/web/src/client/users.js b/web/src/client/users.js index 761d4f8714..1e1bc2c768 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -26,10 +26,10 @@ import { WithIssues } from "./mixins"; const SERVICE_NAME = "org.opensuse.Agama.Manager1"; /** -* @typedef {object} UserResult -* @property {boolean} result - whether the action succeeded or not -* @property {string[]} issues - issues found when applying the action -*/ + * @typedef {object} UserResult + * @property {boolean} result - whether the action succeeded or not + * @property {string[]} issues - issues found when applying the action + */ /** * @typedef {object} User @@ -41,11 +41,11 @@ const SERVICE_NAME = "org.opensuse.Agama.Manager1"; */ /** -* @typedef {object} UserSettings -* @property {User} [firstUser] - first user -* @property {boolean} [rootPasswordSet] - whether the root password is set -* @property {string} [rootSSHKey] - root SSH public key -*/ + * @typedef {object} UserSettings + * @property {User} [firstUser] - first user + * @property {boolean} [rootPasswordSet] - whether the root password is set + * @property {string} [rootSSHKey] - root SSH public key + */ /** * Users client diff --git a/web/src/components/core/About.jsx b/web/src/components/core/About.jsx index 736dbef7a0..15ac6ced4f 100644 --- a/web/src/components/core/About.jsx +++ b/web/src/components/core/About.jsx @@ -66,32 +66,31 @@ export default function About({ {buttonText} - + { // TRANSLATORS: content of the "About" popup (1/2) - _("Agama is an experimental installer for (open)SUSE systems. It \ + _( + "Agama is an experimental installer for (open)SUSE systems. It \ is still under development so, please, do not use it in \ production environments. If you want to give it a try, we \ recommend using a virtual machine to prevent any possible \ -data loss.") +data loss.", + ) } - { - sprintf( - // TRANSLATORS: content of the "About" popup (2/2) - // %s is replaced by the project URL - _("For more information, please visit the project's repository at %s."), - "https://github.com/openSUSE/agama" - ) - } + {sprintf( + // TRANSLATORS: content of the "About" popup (2/2) + // %s is replaced by the project URL + _("For more information, please visit the project's repository at %s."), + "https://github.com/openSUSE/agama", + )} - {_("Close")} + + {_("Close")} + diff --git a/web/src/components/core/About.test.jsx b/web/src/components/core/About.test.jsx index 6c379cd1b6..d0c9d1e498 100644 --- a/web/src/components/core/About.test.jsx +++ b/web/src/components/core/About.test.jsx @@ -29,19 +29,19 @@ import About from "./About"; describe("About", () => { it("renders a help icon inside the button by default", () => { const { container } = plainRender(); - const icon = container.querySelector('svg'); + const icon = container.querySelector("svg"); expect(icon).toHaveAttribute("data-icon-name", "help"); }); it("does not render a help icon inside the button if showIcon=false", () => { const { container } = plainRender(); - const icon = container.querySelector('svg'); + const icon = container.querySelector("svg"); expect(icon).toBeNull(); }); it("allows setting its icon size", () => { const { container } = plainRender(); - const icon = container.querySelector('svg'); + const icon = container.querySelector("svg"); expect(icon.classList.contains("icon-xxs")).toBe(true); }); diff --git a/web/src/components/core/ButtonLink.jsx b/web/src/components/core/ButtonLink.jsx index 5afe4448fd..87492b79a9 100644 --- a/web/src/components/core/ButtonLink.jsx +++ b/web/src/components/core/ButtonLink.jsx @@ -23,7 +23,7 @@ import React from "react"; import { Link } from "react-router-dom"; -import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; +import buttonStyles from "@patternfly/react-styles/css/components/Button/button"; // TODO: Evaluate which is better, this approach or just using a // PF/Button with onClick callback and "component" prop sets as "a" @@ -32,12 +32,10 @@ export default function ButtonLink({ to, isPrimary = false, children, ...props } return ( {children} diff --git a/web/src/components/core/CardField.jsx b/web/src/components/core/CardField.jsx index e38356a010..5695cb4c16 100644 --- a/web/src/components/core/CardField.jsx +++ b/web/src/components/core/CardField.jsx @@ -23,10 +23,15 @@ import React from "react"; import { - Card, CardHeader, CardTitle, CardBody, CardFooter, - Flex, FlexItem, + Card, + CardHeader, + CardTitle, + CardBody, + CardFooter, + Flex, + FlexItem, } from "@patternfly/react-core"; -import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; // FIXME: improve name and documentation // TODO: allows having a drawer, see storage/ProposalResultActions @@ -45,8 +50,7 @@ const CardField = ({ children = [], cardProps = {}, cardHeaderProps = {}, - cardDescriptionProps = {} - + cardDescriptionProps = {}, }) => { // TODO: replace aria-label with the proper aria-labelledby return ( @@ -63,7 +67,11 @@ const CardField = ({ - {description &&
      {description}
      } + {description && ( + +
      {description}
      +
      + )} {children} {actions && {actions}} diff --git a/web/src/components/core/Description.jsx b/web/src/components/core/Description.jsx index 0d5e271f1a..829f149fe5 100644 --- a/web/src/components/core/Description.jsx +++ b/web/src/components/core/Description.jsx @@ -33,11 +33,13 @@ import { Popover, Button } from "@patternfly/react-core"; * @param {React.ReactNode} props.children - The wrapped content. * @param {import("@patternfly/react-core").PopoverProps} [props.otherProps] */ -export default function Description ({ description, children, ...otherProps }) { +export default function Description({ description, children, ...otherProps }) { if (description) { return ( - + ); } diff --git a/web/src/components/core/Drawer.jsx b/web/src/components/core/Drawer.jsx index 7a6c85f6a4..504f09afa6 100644 --- a/web/src/components/core/Drawer.jsx +++ b/web/src/components/core/Drawer.jsx @@ -23,8 +23,14 @@ import React, { forwardRef, useImperativeHandle, useState } from "react"; import { - Drawer as PFDrawer, DrawerPanelBody, - DrawerPanelContent, DrawerContent, DrawerContentBody, DrawerHead, DrawerActions, DrawerCloseButton, + Drawer as PFDrawer, + DrawerPanelBody, + DrawerPanelContent, + DrawerContent, + DrawerContentBody, + DrawerHead, + DrawerActions, + DrawerCloseButton, } from "@patternfly/react-core"; /** @@ -37,41 +43,36 @@ import { * * @todo write documentation */ -const Drawer = forwardRef( - ({ panelHeader, panelContent, isExpanded = false, children }, ref) => { - const [isOpen, setIsOpen] = useState(isExpanded); - const open = () => setIsOpen(true); - const close = () => setIsOpen(false); - const publicAPI = () => ({ open, close }); +const Drawer = forwardRef(({ panelHeader, panelContent, isExpanded = false, children }, ref) => { + const [isOpen, setIsOpen] = useState(isExpanded); + const open = () => setIsOpen(true); + const close = () => setIsOpen(false); + const publicAPI = () => ({ open, close }); - useImperativeHandle(ref, publicAPI, []); + useImperativeHandle(ref, publicAPI, []); - const onEscape = event => event.key === 'Escape' && close(); + const onEscape = (event) => event.key === "Escape" && close(); - return ( - - - - {panelHeader} - - - - - - {panelContent} - - - } - colorVariant="no-background" - > - - {children} - - - - ); - }); + return ( + + + + {panelHeader} + + + + + {panelContent} + + } + colorVariant="no-background" + > + {children} + + + ); +}); export default Drawer; diff --git a/web/src/components/core/EmailInput.jsx b/web/src/components/core/EmailInput.jsx index dc84711157..7c80797ec5 100644 --- a/web/src/components/core/EmailInput.jsx +++ b/web/src/components/core/EmailInput.jsx @@ -32,23 +32,24 @@ import { noop } from "~/utils"; * @returns {boolean} */ const validateEmail = (email) => { - const regexp = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; + const regexp = + /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; const validateFormat = (email) => { - const parts = email.split('@'); + const parts = email.split("@"); return parts.length === 2 && regexp.test(email); }; const validateSizes = (email) => { - const [account, address] = email.split('@'); + const [account, address] = email.split("@"); if (account.length > 64) return false; if (address.length > 255) return false; - const domainParts = address.split('.'); + const domainParts = address.split("."); - if (domainParts.find(p => p.length > 63)) return false; + if (domainParts.find((p) => p.length > 63)) return false; return true; }; @@ -76,11 +77,7 @@ export default function EmailInput({ onValidate = noop, ...props }) { return ( - + ); } diff --git a/web/src/components/core/EmailInput.test.jsx b/web/src/components/core/EmailInput.test.jsx index 0c64ce08b7..786b3c4bd5 100644 --- a/web/src/components/core/EmailInput.test.jsx +++ b/web/src/components/core/EmailInput.test.jsx @@ -28,15 +28,10 @@ import { plainRender } from "~/test-utils"; describe("EmailInput component", () => { it("renders an email input", () => { plainRender( - + , ); - const inputField = screen.getByRole('textbox', { name: "User email" }); + const inputField = screen.getByRole("textbox", { name: "User email" }); expect(inputField).toHaveAttribute("type", "email"); }); @@ -63,7 +58,7 @@ describe("EmailInput component", () => { it("triggers onChange callback", async () => { const { user } = plainRender(); - const emailInput = screen.getByRole('textbox', { name: "Test email" }); + const emailInput = screen.getByRole("textbox", { name: "Test email" }); expect(screen.queryByText("Email value updated!")).toBeNull(); @@ -73,7 +68,7 @@ describe("EmailInput component", () => { it("triggers onValidate callback", async () => { const { user } = plainRender(); - const emailInput = screen.getByRole('textbox', { name: "Test email" }); + const emailInput = screen.getByRole("textbox", { name: "Test email" }); expect(screen.queryByText("Email is not valid!")).toBeNull(); @@ -83,7 +78,7 @@ describe("EmailInput component", () => { it("marks the input as invalid if the value is not a valid email", async () => { const { user } = plainRender(); - const emailInput = screen.getByRole('textbox', { name: "Test email" }); + const emailInput = screen.getByRole("textbox", { name: "Test email" }); await user.type(emailInput, "foo"); diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx index 32c256aa46..487a9279c4 100644 --- a/web/src/components/core/EmptyState.jsx +++ b/web/src/components/core/EmptyState.jsx @@ -57,7 +57,7 @@ export default function EmptyStateWrapper({ children, ...rest }) { - if (noPadding) rest.className = [rest.className, 'no-padding'].join(" ").trim(); + if (noPadding) rest.className = [rest.className, "no-padding"].join(" ").trim(); return ( diff --git a/web/src/components/core/ExpandableSelector.jsx b/web/src/components/core/ExpandableSelector.jsx index aaad695abd..8d3b5e09e3 100644 --- a/web/src/components/core/ExpandableSelector.jsx +++ b/web/src/components/core/ExpandableSelector.jsx @@ -22,7 +22,16 @@ // @ts-check import React, { useState } from "react"; -import { Table, Thead, Tr, Th, Tbody, Td, ExpandableRowContent, RowSelectVariant } from "@patternfly/react-table"; +import { + Table, + Thead, + Tr, + Th, + Tbody, + Td, + ExpandableRowContent, + RowSelectVariant, +} from "@patternfly/react-table"; /** * @typedef {import("@patternfly/react-table").TableProps} TableProps @@ -61,7 +70,11 @@ const TableHeader = ({ columns }) => ( - { columns?.map((c, i) => {c.name}) } + {columns?.map((c, i) => ( + + {c.name} + + ))} ); @@ -88,7 +101,7 @@ const sanitizeSelection = (selection, allowMultiple) => { if (!allowMultiple && selection.length > 1) { console.error( "`itemsSelected` prop can only have more than one item when selector `isMultiple`. " + - "Using only the first element" + "Using only the first element", ); return [selection[0]]; @@ -136,8 +149,9 @@ export default function ExpandableSelector({ const selection = sanitizeSelection(itemsSelected, isMultiple); const isItemSelected = (item) => { const selected = selection.find((selectionItem) => { - return Object.hasOwn(selectionItem, itemIdKey) && - selectionItem[itemIdKey] === item[itemIdKey]; + return ( + Object.hasOwn(selectionItem, itemIdKey) && selectionItem[itemIdKey] === item[itemIdKey] + ); }); return selected !== undefined || selection.includes(item); @@ -145,7 +159,7 @@ export default function ExpandableSelector({ const isItemExpanded = (key) => expandedItemsKeys.includes(key); const toggleExpanded = (key) => { if (isItemExpanded(key)) { - setExpandedItemsKeys(expandedItemsKeys.filter(k => k !== key)); + setExpandedItemsKeys(expandedItemsKeys.filter((k) => k !== key)); } else { setExpandedItemsKeys([...expandedItemsKeys, key]); } @@ -158,7 +172,7 @@ export default function ExpandableSelector({ } if (isItemSelected(item)) { - onSelectionChange(selection.filter(i => i !== item)); + onSelectionChange(selection.filter((i) => i !== item)); } else { onSelectionChange([...selection, item]); } @@ -178,14 +192,14 @@ export default function ExpandableSelector({ rowIndex, onSelect: () => updateSelection(item), isSelected: isItemSelected(item), - variant: isMultiple ? RowSelectVariant.checkbox : RowSelectVariant.radio + variant: isMultiple ? RowSelectVariant.checkbox : RowSelectVariant.radio, }; return ( - { columns?.map((column, index) => ( + {columns?.map((column, index) => ( {column.value(item)} @@ -215,13 +229,13 @@ export default function ExpandableSelector({ rowIndex, onSelect: () => updateSelection(item), isSelected: isItemSelected(item), - variant: isMultiple ? RowSelectVariant.checkbox : RowSelectVariant.radio + variant: isMultiple ? RowSelectVariant.checkbox : RowSelectVariant.radio, }; const renderChildren = () => { if (!validChildren) return; - return children.map(item => renderItemChild(item, isItemExpanded(itemKey), sharedData)); + return children.map((item) => renderItemChild(item, isItemExpanded(itemKey), sharedData)); }; // TODO: Add label to Tbody? @@ -230,13 +244,13 @@ export default function ExpandableSelector({ - { columns?.map((column, index) => ( + {columns?.map((column, index) => ( {column.value(item)} ))} - { renderChildren() } + {renderChildren()} ); }; @@ -244,7 +258,7 @@ export default function ExpandableSelector({ // @see SharedData const sharedData = { rowIndex: 0 }; - const TableBody = () => items?.map(item => renderItem(item, sharedData)); + const TableBody = () => items?.map((item) => renderItem(item, sharedData)); return ( diff --git a/web/src/components/core/ExpandableSelector.test.jsx b/web/src/components/core/ExpandableSelector.test.jsx index eee55dd6be..3726b59a4a 100644 --- a/web/src/components/core/ExpandableSelector.test.jsx +++ b/web/src/components/core/ExpandableSelector.test.jsx @@ -40,7 +40,7 @@ const sda = { name: "/dev/sda", size: 1024, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }; @@ -53,9 +53,9 @@ const sda1 = { name: "/dev/sda1", size: 512, shrinking: { supported: 128 }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; const sda2 = { @@ -66,15 +66,15 @@ const sda2 = { name: "/dev/sda2", size: 512, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; sda.partitionTable = { type: "gpt", partitions: [sda1, sda2], - unpartitionedSize: 512 + unpartitionedSize: 512, }; const sdb = { @@ -93,24 +93,22 @@ const sdb = { name: "/dev/sdb", size: 2048, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }; const lv1 = { sid: "163", name: "/dev/system/vg/lv1", - content: "Personal Data" + content: "Personal Data", }; const vg = { sid: "162", type: "vg", name: "/dev/system/vg", - lvs: [ - lv1 - ] + lvs: [lv1], }; const columns = [ @@ -122,7 +120,7 @@ const columns = [ if (item.type === "vg") return `${item.lvs.length} logical volume(s)`; return item.content; - } + }, }, { name: "Size", value: (item) => item.size }, ]; @@ -135,11 +133,9 @@ const commonProps = { items: [sda, sdb, vg], itemIdKey: "sid", initialExpandedKeys: [sda.sid, vg.sid], - itemChildren: (item) => ( - item.isDrive ? item.partitionTable?.partitions : item.lvs - ), + itemChildren: (item) => (item.isDrive ? item.partitionTable?.partitions : item.lvs), onSelectionChange: onChangeFn, - "aria-label": "Device selector" + "aria-label": "Device selector", }; describe("ExpandableSelector", () => { @@ -203,7 +199,7 @@ describe("ExpandableSelector", () => { it("renders as expanded items which value for `itemIdKey` is included in `initialExpandedKeys` prop", () => { plainRender( - + , ); const table = screen.getByRole("grid"); within(table).getByRole("row", { name: /dev\/sda1 512/ }); @@ -212,7 +208,7 @@ describe("ExpandableSelector", () => { it("keeps track of expanded items", async () => { const { user } = plainRender( - + , ); const table = screen.getByRole("grid"); const sdaRow = within(table).getByRole("row", { name: /sda 1024/ }); @@ -233,9 +229,7 @@ describe("ExpandableSelector", () => { }); it("uses 'id' as key when `itemIdKey` prop is not given", () => { - plainRender( - - ); + plainRender(); const table = screen.getByRole("grid"); // Since itemIdKey does not match the id used for the item, they are @@ -246,7 +240,7 @@ describe("ExpandableSelector", () => { it("uses given `itemIdKey` as key", () => { plainRender( - + , ); const table = screen.getByRole("grid"); @@ -269,7 +263,7 @@ describe("ExpandableSelector", () => { plainRender(); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("prop must be an array"), - "Whatever" + "Whatever", ); }); @@ -331,7 +325,7 @@ describe("ExpandableSelector", () => { it("outputs to console.error", () => { plainRender(); expect(console.error).toHaveBeenCalledWith( - expect.stringContaining("Using only the first element") + expect.stringContaining("Using only the first element"), ); }); @@ -423,12 +417,14 @@ describe("ExpandableSelector", () => { const lv1Row = within(table).getByRole("row", { name: /Personal Data/ }); const selection = screen.getAllByRole("checkbox", { checked: true }); expect(selection.length).toEqual(2); - [sda1Row, lv1Row].forEach(row => within(row).getByRole("checkbox", { checked: true })); + [sda1Row, lv1Row].forEach((row) => within(row).getByRole("checkbox", { checked: true })); }); describe("and user selects an already selected item", () => { it("triggers the `onSelectionChange` callback with a collection not including the item", async () => { - const { user } = plainRender(); + const { user } = plainRender( + , + ); const sda1row = screen.getByRole("row", { name: /dev\/sda1/ }); const sda1radio = within(sda1row).getByRole("checkbox"); await user.click(sda1radio); diff --git a/web/src/components/core/Fieldset.jsx b/web/src/components/core/Fieldset.jsx index 5169347b91..f7a69e8298 100644 --- a/web/src/components/core/Fieldset.jsx +++ b/web/src/components/core/Fieldset.jsx @@ -42,11 +42,7 @@ import { Stack } from "@patternfly/react-core"; * @param {JSX.Element} [props.children] - the section content * @param {object} [props.otherProps] fieldset element attributes, see {@link https://html.spec.whatwg.org/#the-fieldset-element} */ -export default function Fieldset({ - legend, - children, - ...otherProps -}) { +export default function Fieldset({ legend, children, ...otherProps }) { return (
      diff --git a/web/src/components/core/FormLabel.jsx b/web/src/components/core/FormLabel.jsx index a6cafa793d..8e5a2be394 100644 --- a/web/src/components/core/FormLabel.jsx +++ b/web/src/components/core/FormLabel.jsx @@ -20,11 +20,11 @@ */ import React from "react"; -import styles from '@patternfly/react-styles/css/components/Form/form'; +import styles from "@patternfly/react-styles/css/components/Form/form"; /** * A missing PatternFly FormLabel, see: -* https://github.com/patternfly/patternfly-react/blob/d68f302609a6abf8da34d1c33b153f604d6b329d/packages/react-core/src/components/Form/FormGroup.tsx#L108-L123 + * https://github.com/patternfly/patternfly-react/blob/d68f302609a6abf8da34d1c33b153f604d6b329d/packages/react-core/src/components/Form/FormGroup.tsx#L108-L123 * * @param {object} props - component props * @param {boolean} [props.isRequired=false] - whether the associated field is mandatory @@ -34,10 +34,13 @@ import styles from '@patternfly/react-styles/css/components/Form/form'; */ export default function FormLabel({ isRequired = false, fieldId, children }) { return ( - @@ -75,7 +83,9 @@ function InstallationFinished() { const iguana = await client.manager.useIguana(); // FIXME: This logic should likely not be placed here, it's too coupled to storage internals. // Something to fix when this whole page is refactored in a (hopefully near) future. - const { settings: { encryptionPassword, encryptionMethod } } = await client.storage.proposal.getResult(); + const { + settings: { encryptionPassword, encryptionMethod }, + } = await client.storage.proposal.getResult(); setUsingIguana(iguana); setUsingTpm(encryptionPassword?.length > 0 && encryptionMethod === EncryptionMethods.TPM); } @@ -99,12 +109,17 @@ function InstallationFinished() { icon={} /> - + {_("The installation on your machine is complete.")} {usingIguana ? _("At this point you can power off the machine.") - : _("At this point you can reboot the machine to log in to the new system.")} + : _( + "At this point you can reboot the machine to log in to the new system.", + )} {usingTpm && } diff --git a/web/src/components/core/InstallerOptions.jsx b/web/src/components/core/InstallerOptions.jsx index 440ad04545..34783bcf49 100644 --- a/web/src/components/core/InstallerOptions.jsx +++ b/web/src/components/core/InstallerOptions.jsx @@ -23,7 +23,14 @@ import React, { useState } from "react"; import { useLocation } from "react-router-dom"; -import { Button, Flex, Form, FormGroup, FormSelect, FormSelectOption } from "@patternfly/react-core"; +import { + Button, + Flex, + Form, + FormGroup, + FormSelect, + FormSelectOption, +} from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { Popup } from "~/components/core"; import { _ } from "~/i18n"; @@ -47,7 +54,7 @@ export default function InstallerOptions() { language: initialLanguage, keymap: initialKeymap, changeLanguage, - changeKeymap + changeKeymap, } = useInstallerL10n(); const [language, setLanguage] = useState(initialLanguage); const [keymap, setKeymap] = useState(initialKeymap); @@ -85,16 +92,10 @@ export default function InstallerOptions() { aria-label={_("Show installer options")} /> - +
      - + ( - ) - )} + + ))} - - {localConnection() - ? ( - setKeymap(value)} - > - {keymaps.map((keymap, index) => ( - ) - )} - - ) - : _("Cannot be changed in remote installation")} + + {localConnection() ? ( + setKeymap(value)} + > + {keymaps.map((keymap, index) => ( + + ))} + + ) : ( + _("Cannot be changed in remote installation") + )}
      - {_("Accept")} + + {_("Accept")} +
      diff --git a/web/src/components/core/IssuesHint.jsx b/web/src/components/core/IssuesHint.jsx index 81d557a017..525c737232 100644 --- a/web/src/components/core/IssuesHint.jsx +++ b/web/src/components/core/IssuesHint.jsx @@ -34,7 +34,9 @@ export default function IssuesHint({ issues }) { {_("Before starting the installation, you need to address the following problems:")}

      - {issues.map((i, idx) => {i.description})} + {issues.map((i, idx) => ( + {i.description} + ))} diff --git a/web/src/components/core/IssuesHint.test.jsx b/web/src/components/core/IssuesHint.test.jsx index 62c48ddfe2..4965a95ee8 100644 --- a/web/src/components/core/IssuesHint.test.jsx +++ b/web/src/components/core/IssuesHint.test.jsx @@ -28,7 +28,7 @@ it("renders a list of issues", () => { const issue = { description: "You need to create a user", source: "config", - severity: "error" + severity: "error", }; plainRender(); expect(screen.getByText(issue.description)).toBeInTheDocument(); diff --git a/web/src/components/core/ListSearch.jsx b/web/src/components/core/ListSearch.jsx index 33451d9a26..5f187f0d37 100644 --- a/web/src/components/core/ListSearch.jsx +++ b/web/src/components/core/ListSearch.jsx @@ -28,10 +28,7 @@ const search = (elements, term) => { const value = term.toLowerCase(); const match = (element) => { - return Object.values(element) - .join('') - .toLowerCase() - .includes(value); + return Object.values(element).join("").toLowerCase().includes(value); }; return elements.filter(match); @@ -50,7 +47,7 @@ const search = (elements, term) => { export default function ListSearch({ placeholder = _("Search"), elements = [], - onChange: onChangeProp = noop + onChange: onChangeProp = noop, }) { const [value, setValue] = useState(""); const [resultSize, setResultSize] = useState(elements.length); @@ -60,7 +57,7 @@ export default function ListSearch({ onChangeProp(result); }; - const searchHandler = useDebounce(term => { + const searchHandler = useDebounce((term) => { updateResult(search(elements, term)); }, 500); diff --git a/web/src/components/core/ListSearch.test.jsx b/web/src/components/core/ListSearch.test.jsx index b89c318e76..54583c0440 100644 --- a/web/src/components/core/ListSearch.test.jsx +++ b/web/src/components/core/ListSearch.test.jsx @@ -28,7 +28,7 @@ const fruits = [ { name: "Apple", color: "red", size: "medium" }, { name: "Banana", color: "yellow", size: "medium" }, { name: "Grape", color: "green", size: "small" }, - { name: "Pear", color: "green", size: "medium" } + { name: "Pear", color: "green", size: "medium" }, ]; const FruitList = ({ fruits }) => { @@ -38,7 +38,11 @@ const FruitList = ({ fruits }) => { <>
        - {filteredFruits.map((f, i) =>
      • {f.name}
      • )} + {filteredFruits.map((f, i) => ( +
      • + {f.name} +
      • + ))}
      ); @@ -51,8 +55,8 @@ it("searches for elements matching the given term (case-insensitive)", async () // Search for "medium" size fruit await user.type(searchInput, "medium"); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: /grape/ })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: /grape/ })).not.toBeInTheDocument(), ); screen.getByRole("option", { name: "Apple" }); screen.getByRole("option", { name: "Banana" }); @@ -61,11 +65,11 @@ it("searches for elements matching the given term (case-insensitive)", async () // Search for "green" fruit await user.clear(searchInput); await user.type(searchInput, "Green"); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Apple" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Apple" })).not.toBeInTheDocument(), ); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument(), ); screen.getByRole("option", { name: "Grape" }); screen.getByRole("option", { name: "Pear" }); @@ -73,11 +77,11 @@ it("searches for elements matching the given term (case-insensitive)", async () // Search for known fruit await user.clear(searchInput); await user.type(searchInput, "ap"); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument(), ); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Pear" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Pear" })).not.toBeInTheDocument(), ); screen.getByRole("option", { name: "Apple" }); screen.getByRole("option", { name: "Grape" }); @@ -85,16 +89,16 @@ it("searches for elements matching the given term (case-insensitive)", async () // Search for unknown fruit await user.clear(searchInput); await user.type(searchInput, "tomato"); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Apple" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Apple" })).not.toBeInTheDocument(), ); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument(), ); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Grape" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Grape" })).not.toBeInTheDocument(), ); - await waitFor(() => ( - expect(screen.queryByRole("option", { name: "Pear" })).not.toBeInTheDocument()) + await waitFor(() => + expect(screen.queryByRole("option", { name: "Pear" })).not.toBeInTheDocument(), ); }); diff --git a/web/src/components/core/LoginPage.jsx b/web/src/components/core/LoginPage.jsx index 3781ced107..76acd31cb8 100644 --- a/web/src/components/core/LoginPage.jsx +++ b/web/src/components/core/LoginPage.jsx @@ -27,10 +27,13 @@ import { ActionGroup, Button, Card, - Flex, FlexItem, - Form, FormGroup, - Grid, GridItem, - Stack + Flex, + FlexItem, + Form, + FormGroup, + Grid, + GridItem, + Stack, } from "@patternfly/react-core"; import { About, EmptyState, FormValidationError, Page, PasswordInput } from "~/components/core"; import { Center } from "~/components/layout"; @@ -73,8 +76,10 @@ export default function LoginPage() { // TRANSLATORS: description why root password is needed. The text in the // square brackets [] is displayed in bold, use only please, do not translate // it and keep the brackets. - const [rootExplanationStart, rootUser, rootExplanationEnd] = _("The installer requires [root] \ -user privileges.").split(/[[\]]/); + const [rootExplanationStart, rootUser, rootExplanationEnd] = _( + "The installer requires [root] \ +user privileges.", + ).split(/[[\]]/); return ( @@ -82,18 +87,11 @@ user privileges.").split(/[[\]]/); - +

      {rootExplanationStart} {rootUser} {rootExplanationEnd}

      -

      - {_("Please, provide its password to log in to the system.")} -

      +

      {_("Please, provide its password to log in to the system.")}

      diff --git a/web/src/components/core/LoginPage.test.jsx b/web/src/components/core/LoginPage.test.jsx index a47fccbd48..d95ed34d51 100644 --- a/web/src/components/core/LoginPage.test.jsx +++ b/web/src/components/core/LoginPage.test.jsx @@ -35,9 +35,9 @@ jest.mock("~/context/auth", () => ({ return { isAuthenticated: mockIsAuthenticated, login: mockLoginFn, - error: mockLoginError + error: mockLoginError, }; - } + }, })); describe.skip("LoginPage", () => { diff --git a/web/src/components/core/LogsButton.jsx b/web/src/components/core/LogsButton.jsx index a6bfb8f16b..c3315cab54 100644 --- a/web/src/components/core/LogsButton.jsx +++ b/web/src/components/core/LogsButton.jsx @@ -50,7 +50,7 @@ const LogsButton = ({ ...props }) => { * @param {string} url - the file location to download from */ const autoDownload = (url) => { - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; a.download = FILENAME; @@ -60,12 +60,12 @@ const LogsButton = ({ ...props }) => { const clickHandler = () => { setTimeout(() => { URL.revokeObjectURL(url); - a.removeEventListener('click', clickHandler); + a.removeEventListener("click", clickHandler); }, 150); }; // Add the click event listener on the anchor element - a.addEventListener('click', clickHandler, false); + a.addEventListener("click", clickHandler, false); // Programmatically trigger a click on the anchor element // Needed for make the download to happen automatically without attaching the anchor element to @@ -76,9 +76,7 @@ const LogsButton = ({ ...props }) => { const collectAndDownload = () => { setError(null); setIsCollecting(true); - cancellablePromise( - client.manager.fetchLogs().then((response) => response.blob()), - ) + cancellablePromise(client.manager.fetchLogs().then((response) => response.blob())) .then(URL.createObjectURL) .then(autoDownload) .catch((error) => { @@ -104,23 +102,29 @@ const LogsButton = ({ ...props }) => { - {isCollecting && + {isCollecting && ( } + title={_( + "The browser will run the logs download as soon as they are ready. Please, be patient.", + )} + /> + )} - {error && + {error && ( } + /> + )} - {_("Close")} + + {_("Close")} + diff --git a/web/src/components/core/LogsButton.test.jsx b/web/src/components/core/LogsButton.test.jsx index 6eef301fcd..eab60e1c44 100644 --- a/web/src/components/core/LogsButton.test.jsx +++ b/web/src/components/core/LogsButton.test.jsx @@ -42,7 +42,7 @@ beforeEach(() => { return { manager: { fetchLogs: fetchLogsFn, - } + }, }; }); }); @@ -89,7 +89,7 @@ describe("LogsButton", () => { describe("and logs are collected successfully", () => { beforeEach(() => { fetchLogsFn.mockResolvedValue({ - blob: jest.fn().mockResolvedValue(new Blob(["testing"])) + blob: jest.fn().mockResolvedValue(new Blob(["testing"])), }); }); @@ -104,12 +104,12 @@ describe("LogsButton", () => { // "Download logs". document._createElement = document.createElement; - const anchorMock = document.createElement('a'); + const anchorMock = document.createElement("a"); anchorMock.setAttribute = jest.fn(); anchorMock.click = jest.fn(); jest.spyOn(document, "createElement").mockImplementation((tag) => { - return (tag === 'a') ? anchorMock : document._createElement(tag); + return tag === "a" ? anchorMock : document._createElement(tag); }); // Now, let's simulate the "Download logs" user click @@ -117,9 +117,12 @@ describe("LogsButton", () => { await user.click(button); // And test what we're looking for - expect(document.createElement).toHaveBeenCalledWith('a'); + expect(document.createElement).toHaveBeenCalledWith("a"); expect(anchorMock).toHaveAttribute("href", "fake-blob-url"); - expect(anchorMock).toHaveAttribute("download", expect.stringMatching(/agama-installation-logs/)); + expect(anchorMock).toHaveAttribute( + "download", + expect.stringMatching(/agama-installation-logs/), + ); expect(anchorMock.click).toHaveBeenCalled(); // Be polite and restore document.createElement function, diff --git a/web/src/components/core/NumericTextInput.jsx b/web/src/components/core/NumericTextInput.jsx index a8a095ba6e..1aa66ebc5e 100644 --- a/web/src/components/core/NumericTextInput.jsx +++ b/web/src/components/core/NumericTextInput.jsx @@ -56,7 +56,5 @@ export default function NumericTextInput({ value = "", onChange = noop, ...textI } }; - return ( - - ); + return ; } diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.jsx index a6c70af5a0..cdfae2514b 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.jsx @@ -25,14 +25,17 @@ import React from "react"; import { NavLink, Outlet, useNavigate, useMatches, useLocation } from "react-router-dom"; import { Button, - Card, CardBody, CardHeader, + Card, + CardBody, + CardHeader, Flex, - PageGroup, PageSection, - Stack + PageGroup, + PageSection, + Stack, } from "@patternfly/react-core"; import { _ } from "~/i18n"; -import tabsStyles from '@patternfly/react-styles/css/components/Tabs/tabs'; -import flexStyles from '@patternfly/react-styles/css/utilities/Flex/flex'; +import tabsStyles from "@patternfly/react-styles/css/components/Tabs/tabs"; +import flexStyles from "@patternfly/react-styles/css/utilities/Flex/flex"; /** * @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps @@ -95,17 +98,21 @@ const CancelAction = ({ text = _("Cancel"), navigateTo }) => { // FIXME: would replace Actions const NextActions = ({ children }) => ( - + - - {children} - + {children} ); const MainContent = ({ children, ...props }) => ( - {children} + + {children} + ); const Navigation = ({ routes }) => { @@ -121,13 +128,21 @@ const Navigation = ({ routes }) => { @@ -136,14 +151,8 @@ const Navigation = ({ routes }) => { const Header = ({ hasGutter = true, children, ...props }) => { return ( - - - {children} - + + {children} ); }; @@ -179,7 +188,7 @@ const CardSection = ({ title, children, ...props }) => { const Page = () => { const location = useLocation(); const matches = useMatches(); - const currentRoute = matches.find(r => r.pathname === location.pathname); + const currentRoute = matches.find((r) => r.pathname === location.pathname); const titleFromRoute = currentRoute?.handle?.name; return ( diff --git a/web/src/components/core/Page.test.jsx b/web/src/components/core/Page.test.jsx index 096c3d63ca..4733dbf555 100644 --- a/web/src/components/core/Page.test.jsx +++ b/web/src/components/core/Page.test.jsx @@ -48,7 +48,7 @@ describe.skip("Page", () => { // if defined outside, the mock is cleared automatically createClient.mockImplementation(() => { return { - l10n: l10nClientMock + l10n: l10nClientMock, }; }); }); @@ -95,7 +95,7 @@ describe.skip("Page", () => {
      Page content
      , - { withL10n: true } + { withL10n: true }, ); screen.getByText("Page content"); @@ -112,11 +112,11 @@ describe.skip("Page", () => { , - { withL10n: true } + { withL10n: true }, ); // Sidebar is rendering it's own header, let's ignore it - const [header,] = screen.getAllByRole("banner"); + const [header] = screen.getAllByRole("banner"); const menuButton = within(header).getByRole("button", { name: "Testing menu" }); await user.click(menuButton); screen.getByRole("menuitem", { name: "Switch to advanced mode" }); @@ -130,11 +130,11 @@ describe.skip("Page", () => { Discard , - { withL10n: true } + { withL10n: true }, ); // Sidebar is rendering it's own footer, let's ignore it - const [footer,] = screen.getAllByRole("contentinfo"); + const [footer] = screen.getAllByRole("contentinfo"); within(footer).getByRole("button", { name: "Save" }); within(footer).getByRole("button", { name: "Discard" }); }); @@ -167,7 +167,7 @@ describe.skip("Page.Actions", () => { plainRender( - + , ); screen.getByRole("button", { name: "Plain action" }); @@ -185,7 +185,7 @@ describe.skip("Page.Menu", () => { <>The menu entry - + , ); screen.getByRole("button", { name: "Show page menu" }); @@ -223,7 +223,9 @@ describe.skip("Page.Action", () => { it("triggers form submission if it's a submit action and has an associated form", async () => { // NOTE: using preventDefault here to avoid a jsdom error // Error: Not implemented: HTMLFormElement.prototype.requestSubmit - const onSubmit = jest.fn((e) => { e.preventDefault() }); + const onSubmit = jest.fn((e) => { + e.preventDefault(); + }); const { user } = plainRender( <> @@ -231,7 +233,7 @@ describe.skip("Page.Action", () => { Send - + , ); const button = screen.getByRole("button", { name: "Send" }); await user.click(button); @@ -242,20 +244,17 @@ describe.skip("Page.Action", () => { const onClick = jest.fn(); // NOTE: using preventDefault here to avoid a jsdom error // Error: Not implemented: HTMLFormElement.prototype.requestSubmit - const onSubmit = jest.fn((e) => { e.preventDefault() }); + const onSubmit = jest.fn((e) => { + e.preventDefault(); + }); const { user } = plainRender( <> - + Send - + , ); const button = screen.getByRole("button", { name: "Send" }); await user.click(button); diff --git a/web/src/components/core/PasswordAndConfirmationInput.jsx b/web/src/components/core/PasswordAndConfirmationInput.jsx index ff8be137df..f23680ba0c 100644 --- a/web/src/components/core/PasswordAndConfirmationInput.jsx +++ b/web/src/components/core/PasswordAndConfirmationInput.jsx @@ -29,7 +29,14 @@ import { _ } from "~/i18n"; // TODO: improve the component to allow working only in uncontrlled mode if // needed. // TODO: improve the showErrors thingy -const PasswordAndConfirmationInput = ({ inputRef, showErrors = true, value, onChange, onValidation, isDisabled = false }) => { +const PasswordAndConfirmationInput = ({ + inputRef, + showErrors = true, + value, + onChange, + onValidation, + isDisabled = false, +}) => { const passwordInput = inputRef?.current; const [password, setPassword] = useState(value || ""); const [confirmation, setConfirmation] = useState(value || ""); @@ -80,10 +87,7 @@ const PasswordAndConfirmationInput = ({ inputRef, showErrors = true, value, onCh onBlur={() => validate(password, confirmation)} />
      - + { it("displays a warning", async () => { const password = ""; - const { user } = plainRender( - - ); + const { user } = plainRender(); const passwordInput = screen.getByLabelText("Password"); user.type(passwordInput, "123456"); @@ -38,9 +36,7 @@ describe("when the passwords do not match", () => { }); it("uses the given password value for confirmation too", async () => { - plainRender( - - ); + plainRender(); const passwordInput = screen.getByLabelText("Password"); const confirmationInput = screen.getByLabelText("Password confirmation"); @@ -51,9 +47,7 @@ it("uses the given password value for confirmation too", async () => { describe("when isDisabled", () => { it("disables both, password and confirmation", async () => { - plainRender( - - ); + plainRender(); const passwordInput = screen.getByLabelText("Password"); const confirmationInput = screen.getByLabelText("Password confirmation"); diff --git a/web/src/components/core/PasswordInput.jsx b/web/src/components/core/PasswordInput.jsx index aefb3ca898..1bb192fbed 100644 --- a/web/src/components/core/PasswordInput.jsx +++ b/web/src/components/core/PasswordInput.jsx @@ -47,17 +47,14 @@ export default function PasswordInput({ id, inputRef, ...props }) { if (!id) { const field = props.label || props["aria-label"] || props.name; - console.error(`The PasswordInput component must have an 'id' but it was not given for '${field}'`); + console.error( + `The PasswordInput component must have an 'id' but it was not given for '${field}'`, + ); } return ( - + -); +const Action = ({ children, ...buttonProps }) => ; /** * A Popup primary action @@ -77,7 +73,9 @@ const Action = ({ children, ...buttonProps }) => ( * @param {ButtonWithoutVariantProps} props */ const PrimaryAction = ({ children, ...actionProps }) => ( - {children} + + {children} + ); /** @@ -92,7 +90,9 @@ const PrimaryAction = ({ children, ...actionProps }) => ( * @param {ButtonWithoutVariantProps} props */ const Confirm = ({ children = _("Confirm"), ...actionProps }) => ( - {children} + + {children} + ); /** @@ -113,7 +113,9 @@ const Confirm = ({ children = _("Confirm"), ...actionProps }) => ( * @param {ButtonWithoutVariantProps} props */ const SecondaryAction = ({ children, ...actionProps }) => ( - {children} + + {children} + ); /** @@ -128,7 +130,9 @@ const SecondaryAction = ({ children, ...actionProps }) => ( * @param {ButtonWithoutVariantProps} props */ const Cancel = ({ children = _("Cancel"), ...actionProps }) => ( - {children} + + {children} + ); /** @@ -149,7 +153,9 @@ const Cancel = ({ children = _("Cancel"), ...actionProps }) => ( * @param {ButtonWithoutVariantProps} props */ const AncillaryAction = ({ children, ...actionsProps }) => ( - {children} + + {children} + ); /** diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index d41a8be3c3..63b0230231 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -30,7 +30,7 @@ import { ProgressStepper, Spinner, Stack, - Truncate + Truncate, } from "@patternfly/react-core"; import { _ } from "~/i18n"; @@ -58,7 +58,11 @@ const Progress = ({ steps, step, firstStep, detail }) => {
      {_("In progress")}
      - +
      ); @@ -138,7 +142,11 @@ function ProgressReport({ title, firstStep }) { - +

      {progressTitle} diff --git a/web/src/components/core/ProgressText.jsx b/web/src/components/core/ProgressText.jsx index e5117120cc..411e48ea3c 100644 --- a/web/src/components/core/ProgressText.jsx +++ b/web/src/components/core/ProgressText.jsx @@ -35,12 +35,10 @@ import { Split, Text } from "@patternfly/react-core"; * @param {number} [props.total] Number of steps */ export default function ProgressText({ message, current, total }) { - const text = (current === 0) ? message : `${message} (${current}/${total})`; + const text = current === 0 ? message : `${message} (${current}/${total})`; return ( - - {text} - + {text} ); } diff --git a/web/src/components/core/RowActions.jsx b/web/src/components/core/RowActions.jsx index d72b847887..864fcb141d 100644 --- a/web/src/components/core/RowActions.jsx +++ b/web/src/components/core/RowActions.jsx @@ -20,10 +20,10 @@ */ import React from "react"; -import { MenuToggle } from '@patternfly/react-core'; -import { ActionsColumn } from '@patternfly/react-table'; +import { MenuToggle } from "@patternfly/react-core"; +import { ActionsColumn } from "@patternfly/react-table"; -import { Icon } from '~/components/layout'; +import { Icon } from "~/components/layout"; import { _ } from "~/i18n"; /** diff --git a/web/src/components/core/Section.jsx b/web/src/components/core/Section.jsx index 0435ad602d..1d8c6c891d 100644 --- a/web/src/components/core/Section.jsx +++ b/web/src/components/core/Section.jsx @@ -24,7 +24,7 @@ import React from "react"; import { Link } from "react-router-dom"; import { PageSection, Stack } from "@patternfly/react-core"; -import { Icon } from '~/components/layout'; +import { Icon } from "~/components/layout"; /** * @typedef {import("~/components/layout/Icon").IconName} IconName */ @@ -75,7 +75,7 @@ export default function Section({ id, errors, children, - "aria-label": ariaLabel + "aria-label": ariaLabel, }) { const headerId = `${name || crypto.randomUUID()}-section-header`; @@ -93,7 +93,10 @@ export default function Section({ return (
      -

      {headerIcon}{headerText}

      +

      + {headerIcon} + {headerText} +

      {renderDescription &&

      {description}

      }
      ); @@ -102,9 +105,7 @@ export default function Section({ return (
      - - {children} - + {children} ); } diff --git a/web/src/components/core/Section.test.jsx b/web/src/components/core/Section.test.jsx index d293cccd03..5d7c5cbb11 100644 --- a/web/src/components/core/Section.test.jsx +++ b/web/src/components/core/Section.test.jsx @@ -66,14 +66,16 @@ describe.skip("Section", () => { it("does not render an icon if not valid icon name is given", () => { // @ts-expect-error: Creating the icon name dynamically is unlikely, but let's be safe. - const { container } = plainRender(
      ); + const { container } = plainRender( +
      , + ); const icon = container.querySelector("svg"); expect(icon).toBeNull(); }); it("renders given description as part of the header", () => { plainRender( -
      +
      , ); const header = screen.getByRole("banner"); within(header).getByText(/Short explanation/); @@ -149,18 +151,18 @@ describe.skip("Section", () => { it("renders given errors", () => { plainRender( -
      +
      , ); screen.getByText("Something went wrong"); }); it("renders given content", () => { - plainRender( -
      - A settings summary -
      - ); + plainRender(
      A settings summary
      ); screen.getByText("A settings summary"); }); diff --git a/web/src/components/core/SectionSkeleton.jsx b/web/src/components/core/SectionSkeleton.jsx index e34da05402..57e2e01dd5 100644 --- a/web/src/components/core/SectionSkeleton.jsx +++ b/web/src/components/core/SectionSkeleton.jsx @@ -24,24 +24,16 @@ import { Skeleton } from "@patternfly/react-core"; import { _ } from "~/i18n"; const WaitingSkeleton = ({ width }) => { - return ( - - ); + return ; }; const SectionSkeleton = ({ numRows = 2 }) => { return ( <> - { - Array.from({ length: numRows }, (_, i) => { - const width = i % 2 === 0 ? "50%" : "25%"; - return ; - }) - } + {Array.from({ length: numRows }, (_, i) => { + const width = i % 2 === 0 ? "50%" : "25%"; + return ; + })} ); }; diff --git a/web/src/components/core/ServerError.jsx b/web/src/components/core/ServerError.jsx index 3452081129..0846b5b136 100644 --- a/web/src/components/core/ServerError.jsx +++ b/web/src/components/core/ServerError.jsx @@ -20,7 +20,12 @@ */ import React from "react"; -import { EmptyState, EmptyStateIcon, EmptyStateBody, EmptyStateHeader } from "@patternfly/react-core"; +import { + EmptyState, + EmptyStateIcon, + EmptyStateBody, + EmptyStateHeader, +} from "@patternfly/react-core"; import { Center, Icon } from "~/components/layout"; import { Page } from "~/components/core"; import SimpleLayout from "~/SimpleLayout"; @@ -43,16 +48,12 @@ function ServerError() { headingLevel="h2" icon={} /> - - {_("Please, check whether it is running.")} - + {_("Please, check whether it is running.")} - - {_("Reload")} - + {_("Reload")} ); diff --git a/web/src/components/core/Tip.jsx b/web/src/components/core/Tip.jsx index 8a72e22f3b..ae31ab7023 100644 --- a/web/src/components/core/Tip.jsx +++ b/web/src/components/core/Tip.jsx @@ -37,7 +37,7 @@ import { Icon } from "~/components/layout"; * @param {React.ReactElement} props.description - Details displayed after clicking the label. * @param {React.ReactNode} props.children - The content of the label. */ -export default function Tip ({ description, children }) { +export default function Tip({ description, children }) { if (description) { return ( diff --git a/web/src/components/core/TreeTable.jsx b/web/src/components/core/TreeTable.jsx index 6072fd18d3..57c7ab4d1b 100644 --- a/web/src/components/core/TreeTable.jsx +++ b/web/src/components/core/TreeTable.jsx @@ -22,7 +22,7 @@ // @ts-check import React, { useEffect, useState } from "react"; -import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/react-table'; +import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from "@patternfly/react-table"; /** * @typedef {import("@patternfly/react-table").TableProps} TableProps @@ -73,7 +73,7 @@ export default function TreeTable({ const toggle = (item) => { if (isExpanded(item)) { - setExpanded(expanded.filter(d => d !== item)); + setExpanded(expanded.filter((d) => d !== item)); } else { setExpanded([...expanded, item]); } @@ -83,13 +83,15 @@ export default function TreeTable({ return columns.map((c, cIdx) => { const props = { dataLabel: c.name, - className: c.classNames + className: c.classNames, }; if (cIdx === 0) props.treeRow = treeRow; return ( -

      + ); }); }; @@ -97,53 +99,48 @@ export default function TreeTable({ const renderRows = (items, level, hidden = false) => { if (items?.length <= 0) return; - return ( - items.map((item, itemIdx) => { - const children = itemChildren(item); - const expanded = isExpanded(item); - - const treeRow = { - onCollapse: () => toggle(item), - props: { - isExpanded: expanded, - isDetailsExpanded: true, - isHidden: hidden, - "aria-level": level, - "aria-posinset": itemIdx + 1, - "aria-setsize": children?.length || 0 - } - }; - - const rowProps = { - row: { props: treeRow.props }, - className: rowClassNames(item) - }; - - return ( - - {renderColumns(item, treeRow)} - { renderRows(children, level + 1, !expanded)} - - ); - }) - ); + return items.map((item, itemIdx) => { + const children = itemChildren(item); + const expanded = isExpanded(item); + + const treeRow = { + onCollapse: () => toggle(item), + props: { + isExpanded: expanded, + isDetailsExpanded: true, + isHidden: hidden, + "aria-level": level, + "aria-posinset": itemIdx + 1, + "aria-setsize": children?.length || 0, + }, + }; + + const rowProps = { + row: { props: treeRow.props }, + className: rowClassNames(item), + }; + + return ( + + {renderColumns(item, treeRow)} + {renderRows(children, level + 1, !expanded)} + + ); + }); }; return ( -
      {c.value(item)} + {c.value(item)} +
      +
      - { columns.map((c, i) => ) } + {columns.map((c, i) => ( + + ))} - - { renderRows(items, 1) } - + {renderRows(items, 1)}
      {c.name} + {c.name} +
      ); } diff --git a/web/src/components/l10n/KeyboardSelection.jsx b/web/src/components/l10n/KeyboardSelection.jsx index fa58525b08..f9a45de367 100644 --- a/web/src/components/l10n/KeyboardSelection.jsx +++ b/web/src/components/l10n/KeyboardSelection.jsx @@ -25,7 +25,7 @@ import { useNavigate } from "react-router-dom"; import { ListSearch, Page } from "~/components/core"; import { _ } from "~/i18n"; import { useConfigMutation, useL10n } from "~/queries/l10n"; -import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; // TODO: Add documentation and typechecking // TODO: Evaluate if worth it extracting the selector @@ -35,7 +35,7 @@ export default function KeyboardSelection() { const { keymaps, selectedKeymap: currentKeymap } = useL10n(); const [selected, setSelected] = useState(currentKeymap.id); const [filteredKeymaps, setFilteredKeymaps] = useState( - keymaps.sort((k1, k2) => k1.name > k2.name ? 1 : -1) + keymaps.sort((k1, k2) => (k1.name > k2.name ? 1 : -1)), ); const searchHelp = _("Filter by description or keymap code"); @@ -57,7 +57,8 @@ export default function KeyboardSelection() { <> {name} - {id} + {" "} + {id} } value={id} @@ -67,9 +68,7 @@ export default function KeyboardSelection() { }); if (keymapsList.length === 0) { - keymapsList = ( - {_("None of the keymaps match the filter.")} - ); + keymapsList = {_("None of the keymaps match the filter.")}; } return ( @@ -81,9 +80,7 @@ export default function KeyboardSelection() { - - {keymapsList} - + {keymapsList} diff --git a/web/src/components/l10n/KeyboardSelection.test.jsx b/web/src/components/l10n/KeyboardSelection.test.jsx index b08daf2659..627b2cb196 100644 --- a/web/src/components/l10n/KeyboardSelection.test.jsx +++ b/web/src/components/l10n/KeyboardSelection.test.jsx @@ -27,17 +27,17 @@ import { mockNavigateFn, plainRender } from "~/test-utils"; const keymaps = [ { id: "us", name: "English" }, - { id: "es", name: "Spanish" } + { id: "es", name: "Spanish" }, ]; const mockConfigMutation = { - mutate: jest.fn() + mutate: jest.fn(), }; jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), useConfigMutation: () => mockConfigMutation, - useL10n: () => ({ keymaps, selectedKeymap: keymaps[0] }) + useL10n: () => ({ keymaps, selectedKeymap: keymaps[0] }), })); jest.mock("react-router-dom", () => ({ diff --git a/web/src/components/l10n/L10nPage.jsx b/web/src/components/l10n/L10nPage.jsx index 2eaa70babd..33e76b3fa5 100644 --- a/web/src/components/l10n/L10nPage.jsx +++ b/web/src/components/l10n/L10nPage.jsx @@ -20,22 +20,21 @@ */ import React from "react"; -import { Gallery, GalleryItem, } from "@patternfly/react-core"; +import { Gallery, GalleryItem } from "@patternfly/react-core"; import { useLoaderData } from "react-router-dom"; import { ButtonLink, CardField, Page } from "~/components/core"; -import { LOCALE_SELECTION_PATH, KEYMAP_SELECTION_PATH, TIMEZONE_SELECTION_PATH } from "~/routes/l10n"; +import { + LOCALE_SELECTION_PATH, + KEYMAP_SELECTION_PATH, + TIMEZONE_SELECTION_PATH, +} from "~/routes/l10n"; import { _ } from "~/i18n"; import { useL10n } from "~/queries/l10n"; const Section = ({ label, value, children }) => { return ( - - - {children} - + + {children} ); }; @@ -46,11 +45,7 @@ const Section = ({ label, value, children }) => { * @component */ export default function L10nPage() { - const { - selectedLocale: locale, - selectedTimezone: timezone, - selectedKeymap: keymap - } = useL10n(); + const { selectedLocale: locale, selectedTimezone: timezone, selectedKeymap: keymap } = useL10n(); return ( <> @@ -72,10 +67,7 @@ export default function L10nPage() { -
      +
      {keymap ? _("Change") : _("Select")} @@ -85,7 +77,7 @@ export default function L10nPage() {
      {timezone ? _("Change") : _("Select")} diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.jsx index 6ad324dc6e..808f5ce5aa 100644 --- a/web/src/components/l10n/L10nPage.test.jsx +++ b/web/src/components/l10n/L10nPage.test.jsx @@ -27,27 +27,27 @@ let mockLoadedData; const locales = [ { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" } + { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }, ]; const keymaps = [ { id: "us", name: "English" }, - { id: "es", name: "Spanish" } + { id: "es", name: "Spanish" }, ]; const timezones = [ { id: "Europe/Berlin", parts: ["Europe", "Berlin"] }, - { id: "Europe/Madrid", parts: ["Europe", "Madrid"] } + { id: "Europe/Madrid", parts: ["Europe", "Madrid"] }, ]; -jest.mock('react-router-dom', () => ({ +jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), // TODO: mock the link because it needs a working router. - Link: ({ children }) => + Link: ({ children }) => , })); jest.mock("~/queries/l10n", () => ({ - useL10n: () => mockLoadedData + useL10n: () => mockLoadedData, })); beforeEach(() => { diff --git a/web/src/components/l10n/LocaleSelection.jsx b/web/src/components/l10n/LocaleSelection.jsx index 543c18336a..cf6f39f00c 100644 --- a/web/src/components/l10n/LocaleSelection.jsx +++ b/web/src/components/l10n/LocaleSelection.jsx @@ -25,7 +25,7 @@ import { useNavigate } from "react-router-dom"; import { ListSearch, Page } from "~/components/core"; import { _ } from "~/i18n"; import { useConfigMutation, useL10n } from "~/queries/l10n"; -import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; // TODO: Add documentation and typechecking // TODO: Evaluate if worth it extracting the selector @@ -53,8 +53,12 @@ export default function LocaleSelection() { onChange={() => setSelected(id)} label={ - {name} - {territory} + + {name} + + + {territory} + {id} } @@ -65,9 +69,7 @@ export default function LocaleSelection() { }); if (localesList.length === 0) { - localesList = ( - {_("None of the locales match the filter.")} - ); + localesList = {_("None of the locales match the filter.")}; } return ( @@ -80,9 +82,7 @@ export default function LocaleSelection() {
      - - {localesList} - + {localesList}
      diff --git a/web/src/components/l10n/LocaleSelection.test.jsx b/web/src/components/l10n/LocaleSelection.test.jsx index a104ddcd30..857e084270 100644 --- a/web/src/components/l10n/LocaleSelection.test.jsx +++ b/web/src/components/l10n/LocaleSelection.test.jsx @@ -27,22 +27,22 @@ import { mockNavigateFn, plainRender } from "~/test-utils"; const locales = [ { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" } + { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }, ]; const mockConfigMutation = { - mutate: jest.fn() + mutate: jest.fn(), }; jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), useL10n: () => ({ locales, selectedLocale: locales[0] }), - useConfigMutation: () => mockConfigMutation + useConfigMutation: () => mockConfigMutation, })); jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), - useNavigate: () => mockNavigateFn + useNavigate: () => mockNavigateFn, })); it("allows changing the keyboard", async () => { diff --git a/web/src/components/l10n/TimezoneSelection.jsx b/web/src/components/l10n/TimezoneSelection.jsx index a67c65fc5a..385d689a70 100644 --- a/web/src/components/l10n/TimezoneSelection.jsx +++ b/web/src/components/l10n/TimezoneSelection.jsx @@ -26,7 +26,7 @@ import { ListSearch, Page } from "~/components/core"; import { _ } from "~/i18n"; import { timezoneTime } from "~/utils"; import { useConfigMutation, useL10n } from "~/queries/l10n"; -import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; let date; @@ -44,7 +44,7 @@ const timezoneWithDetails = (timezone) => { const sortedTimezones = (timezones) => { return timezones.sort((timezone1, timezone2) => { - const timezoneText = t => t.parts.join('').toLowerCase(); + const timezoneText = (t) => t.parts.join("").toLowerCase(); return timezoneText(timezone1) > timezoneText(timezone2) ? 1 : -1; }); }; @@ -79,8 +79,9 @@ export default function TimezoneSelection() { label={ <> - {parts.join('-')} - {country} + {parts.join("-")} + {" "} + {country} } description={ @@ -97,24 +98,24 @@ export default function TimezoneSelection() { }); if (timezonesList.length === 0) { - timezonesList = ( - {_("None of the time zones match the filter.")} - ); + timezonesList = {_("None of the time zones match the filter.")}; } return ( <>

      {_(" Timezone selection")}

      - +
      - - {timezonesList} - + {timezonesList}
      diff --git a/web/src/components/l10n/TimezoneSelection.test.jsx b/web/src/components/l10n/TimezoneSelection.test.jsx index 61e882fc8f..4a1b3ae061 100644 --- a/web/src/components/l10n/TimezoneSelection.test.jsx +++ b/web/src/components/l10n/TimezoneSelection.test.jsx @@ -27,22 +27,22 @@ import { mockNavigateFn, plainRender } from "~/test-utils"; const timezones = [ { id: "Europe/Berlin", parts: ["Europe", "Berlin"], country: "Germany", utcOffset: 1 }, - { id: "Europe/Madrid", parts: ["Europe", "Madrid"], country: "Spain", utfOffset: 1 } + { id: "Europe/Madrid", parts: ["Europe", "Madrid"], country: "Spain", utfOffset: 1 }, ]; const mockConfigMutation = { - mutate: jest.fn() + mutate: jest.fn(), }; jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), useConfigMutation: () => mockConfigMutation, - useL10n: () => ({ timezones, selectedTimezone: timezones[0] }) + useL10n: () => ({ timezones, selectedTimezone: timezones[0] }), })); jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), - useNavigate: () => mockNavigateFn + useNavigate: () => mockNavigateFn, })); it("allows changing the keyboard", async () => { diff --git a/web/src/components/layout/Center.jsx b/web/src/components/layout/Center.jsx index 5e692e9c95..52bd24f759 100644 --- a/web/src/components/layout/Center.jsx +++ b/web/src/components/layout/Center.jsx @@ -50,9 +50,7 @@ import React from "react"; */ const Center = ({ children, ...htmlProps }) => (
      -
      - {children} -
      +
      {children}
      ); diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index 086d6b60fe..47ad45b5ec 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React from 'react'; +import React from "react"; // NOTE: "@icons" is an alias to use a shorter path to real @material-symbols // icons location. Check the tsconfig.json file to see its value. @@ -150,12 +150,10 @@ const icons = { wifi_off: WifiOff, // brand icons linux_logo: SiLinux, - windows_logo: SiWindows + windows_logo: SiWindows, }; -const PREDEFINED_SIZES = [ - "xxxs", "xxs", "xs", "s", "m", "l", "xl", "xxl", "xxxl" -]; +const PREDEFINED_SIZES = ["xxxs", "xxs", "xs", "s", "m", "l", "xl", "xxl", "xxxl"]; /** * Agama Icon component diff --git a/web/src/components/layout/Icon.test.jsx b/web/src/components/layout/Icon.test.jsx index bf2212b33c..1836e63ba0 100644 --- a/web/src/components/layout/Icon.test.jsx +++ b/web/src/components/layout/Icon.test.jsx @@ -40,20 +40,20 @@ describe("Icon", () => { describe("mounted with a known name", () => { it("renders an aria-hidden SVG element", async () => { const { container } = plainRender(); - const icon = container.querySelector('svg'); + const icon = container.querySelector("svg"); expect(icon).toHaveAttribute("aria-hidden", "true"); }); it("includes the icon name as a data attribute of the SVG", async () => { const { container } = plainRender(); - const icon = container.querySelector('svg'); + const icon = container.querySelector("svg"); expect(icon).toHaveAttribute("data-icon-name", "wifi"); }); describe("and a predefined size", () => { it("adds a CSS class for given size", () => { const { container } = plainRender(); - const icon = container.querySelector('svg'); + const icon = container.querySelector("svg"); // Check that width and height are set to default (see .svgrrc for // production, __mocks__/svg.js for testing) expect(icon).toHaveAttribute("width", "28"); @@ -66,7 +66,7 @@ describe("Icon", () => { describe("and an arbitrary size", () => { it("change the width and height attributes to given value", () => { const { container } = plainRender(); - const icon = container.querySelector('svg'); + const icon = container.querySelector("svg"); expect(icon).toHaveAttribute("width", "1dhv"); expect(icon).toHaveAttribute("height", "1dhv"); }); @@ -77,9 +77,7 @@ describe("Icon", () => { it("outputs to console.error", () => { // @ts-expect-error: It's unlikely to happen, but let's test it anyway plainRender(); - expect(console.error).toHaveBeenCalledWith( - expect.stringContaining("'apsens' not found") - ); + expect(console.error).toHaveBeenCalledWith(expect.stringContaining("'apsens' not found")); }); it("renders nothing", async () => { @@ -93,9 +91,7 @@ describe("Icon", () => { it("outputs to console.error", () => { // @ts-expect-error: It's unlikely to happen, but let's test it anyway plainRender(); - expect(console.error).toHaveBeenCalledWith( - expect.stringContaining("not found") - ); + expect(console.error).toHaveBeenCalledWith(expect.stringContaining("not found")); }); it("renders nothing", () => { diff --git a/web/src/components/network/AddressesDataList.jsx b/web/src/components/network/AddressesDataList.jsx index 4c4ff9c3ae..15038a8d4f 100644 --- a/web/src/components/network/AddressesDataList.jsx +++ b/web/src/components/network/AddressesDataList.jsx @@ -28,9 +28,14 @@ import React from "react"; import { Button, - DataList, DataListItem, DataListItemRow, DataListItemCells, DataListCell, DataListAction, + DataList, + DataListItem, + DataListItemRow, + DataListItemCells, + DataListCell, + DataListAction, Flex, - Stack + Stack, } from "@patternfly/react-core"; import { FormLabel } from "~/components/core"; @@ -42,9 +47,9 @@ let index = 0; export default function AddressesDataList({ addresses: originalAddresses, updateAddresses, - allowEmpty = true + allowEmpty = true, }) { - const addresses = originalAddresses.map(addr => { + const addresses = originalAddresses.map((addr) => { const newAddr = addr; if (!newAddr.id) newAddr.id = index++; return newAddr; @@ -56,13 +61,13 @@ export default function AddressesDataList({ }; const updateAddress = (id, field, value) => { - const address = addresses.find(addr => addr.id === id); + const address = addresses.find((addr) => addr.id === id); address[field] = value; updateAddresses(addresses); }; - const deleteAddress = id => { - const addressIdx = addresses.findIndex(addr => addr.id === id); + const deleteAddress = (id) => { + const addressIdx = addresses.findIndex((addr) => addr.id === id); addresses.splice(addressIdx, 1); updateAddresses(addresses); }; @@ -73,7 +78,12 @@ export default function AddressesDataList({ return ( - @@ -99,7 +109,7 @@ export default function AddressesDataList({ placeholder={_("Prefix length or netmask")} aria-label={_("Prefix length or netmask")} /> - + , ]; return ( @@ -121,7 +131,7 @@ export default function AddressesDataList({ {_("Addresses")} - {addresses.map(address => renderAddress(address))} + {addresses.map((address) => renderAddress(address))} @@ -98,7 +109,7 @@ export default function DnsDataList({ servers: originalServers, updateDnsServers {_("DNS")} - {servers.map(server => renderDns(server))} + {servers.map((server) => renderDns(server))} {/* TRANSLATORS: button label */} - + ); diff --git a/web/src/components/network/WifiConnectionForm.test.jsx b/web/src/components/network/WifiConnectionForm.test.jsx index fd77bb5e92..b332ec1069 100644 --- a/web/src/components/network/WifiConnectionForm.test.jsx +++ b/web/src/components/network/WifiConnectionForm.test.jsx @@ -31,7 +31,7 @@ jest.mock("~/client"); Element.prototype.scrollIntoView = jest.fn(); const hiddenNetworkMock = { - hidden: true + hidden: true, }; const networkMock = { @@ -48,7 +48,7 @@ beforeEach(() => { return { network: { addAndConnectTo: addAndConnectToFn, - } + }, }; }); }); @@ -100,7 +100,7 @@ describe("WifiConnectionForm", () => { expect(addAndConnectToFn).toHaveBeenCalledWith( "Wi-Fi Network", - expect.objectContaining({ security: "wpa-psk", password: "wifi-password" }) + expect.objectContaining({ security: "wpa-psk", password: "wifi-password" }), ); }); diff --git a/web/src/components/network/WifiHiddenNetworkForm.jsx b/web/src/components/network/WifiHiddenNetworkForm.jsx index 3519816f4f..9d58ea7848 100644 --- a/web/src/components/network/WifiHiddenNetworkForm.jsx +++ b/web/src/components/network/WifiHiddenNetworkForm.jsx @@ -38,12 +38,13 @@ import { _ } from "~/i18n"; function WifiHiddenNetworkForm({ network, visible, beforeHiding, onSubmitCallback }) { return ( <> - {visible && + {visible && ( } + /> + )} ); } diff --git a/web/src/components/network/WifiHiddenNetworkForm.test.jsx b/web/src/components/network/WifiHiddenNetworkForm.test.jsx index 37bc6b7620..0bd628301c 100644 --- a/web/src/components/network/WifiHiddenNetworkForm.test.jsx +++ b/web/src/components/network/WifiHiddenNetworkForm.test.jsx @@ -26,7 +26,9 @@ import { plainRender } from "~/test-utils"; import { WifiHiddenNetworkForm } from "~/components/network"; -jest.mock("~/components/network/WifiConnectionForm", () => () =>
      WifiConnectionForm mock
      ); +jest.mock("~/components/network/WifiConnectionForm", () => () => ( +
      WifiConnectionForm mock
      +)); describe("WifiHiddenNetworkForm", () => { describe("when it is visible", () => { diff --git a/web/src/components/network/WifiNetworksListPage.jsx b/web/src/components/network/WifiNetworksListPage.jsx index a12f1d6a4c..b0b21f0f31 100644 --- a/web/src/components/network/WifiNetworksListPage.jsx +++ b/web/src/components/network/WifiNetworksListPage.jsx @@ -22,14 +22,26 @@ import React from "react"; import { Button, - Card, CardBody, - DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow, - Drawer, DrawerActions, DrawerCloseButton, DrawerContent, DrawerContentBody, DrawerHead, DrawerPanelBody, DrawerPanelContent, + Card, + CardBody, + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + Drawer, + DrawerActions, + DrawerCloseButton, + DrawerContent, + DrawerContentBody, + DrawerHead, + DrawerPanelBody, + DrawerPanelContent, Flex, Label, Spinner, Split, - Stack + Stack, } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { WifiConnectionForm } from "~/components/network"; @@ -70,11 +82,7 @@ const connectionAddresses = (network) => { }; const ConnectionData = ({ network }) => { - return ( - - {connectionAddresses(network)} - - ); + return {connectionAddresses(network)}; }; const WifiDrawerPanelBody = ({ network, onCancel, onForget }) => { @@ -96,9 +104,7 @@ const WifiDrawerPanelBody = ({ network, onCancel, onForget }) => { await client.network.connectTo(network.settings)}> {_("Connect")} - - {_("Edit")} - + {_("Edit")} @@ -123,7 +129,11 @@ const WifiDrawerPanelBody = ({ network, onCancel, onForget }) => { {_("Edit")} - @@ -137,11 +147,7 @@ const WifiDrawerPanelBody = ({ network, onCancel, onForget }) => { const NetworkFormName = ({ network }) => { if (!network) return; - return ( -

      - {network === HIDDEN_NETWORK ? _("Connect to a hidden network") : network.ssid} -

      - ); + return

      {network === HIDDEN_NETWORK ? _("Connect to a hidden network") : network.ssid}

      ; }; const NetworkListName = ({ network }) => { @@ -150,8 +156,16 @@ const NetworkListName = ({ network }) => { return ( {network.ssid} - {network.settings && } - {state === _("Connected") && } + {network.settings && ( + + )} + {state === _("Connected") && ( + + )} ); }; @@ -170,14 +184,14 @@ function WifiNetworksListPage({ selected, onSelectionChange, networks = [], - forceUpdateNetworksCallback = () => { } + forceUpdateNetworksCallback = () => {}, }) { const selectHiddenNetwork = () => { onSelectionChange(HIDDEN_NETWORK); }; const selectNetwork = (ssid) => { - onSelectionChange(networks.find(n => n.ssid === ssid)); + onSelectionChange(networks.find((n) => n.ssid === ssid)); }; const unselectNetwork = () => { @@ -185,7 +199,7 @@ function WifiNetworksListPage({ }; const renderElements = () => { - return networks.map(n => { + return networks.map((n) => { return ( @@ -194,12 +208,19 @@ function WifiNetworksListPage({ - -
      {n.security.join(", ")}
      -
      {n.strength}
      + +
      + {n.security.join(", ")} +
      +
      + {n.strength} +
      -
      + , ]} />
      diff --git a/web/src/components/network/WifiSelectorPage.jsx b/web/src/components/network/WifiSelectorPage.jsx index 2e81023694..1f0a82efa0 100644 --- a/web/src/components/network/WifiSelectorPage.jsx +++ b/web/src/components/network/WifiSelectorPage.jsx @@ -37,7 +37,12 @@ const baseHiddenNetwork = { ssid: undefined, hidden: true }; function WifiSelectorPage() { const { network: client } = useInstallerClient(); - const { connections: initialConnections, devices: initialDevices, accessPoints, networks: initialNetworks } = useLoaderData(); + const { + connections: initialConnections, + devices: initialDevices, + accessPoints, + networks: initialNetworks, + } = useLoaderData(); const [data, saveData] = useLocalStorage("agama-network", { selectedWifi: null }); // Reevaluate how to keep the state in the future const [selected, setSelected] = useState(data.selectedWifi); @@ -77,14 +82,14 @@ function WifiSelectorPage() { // Let's keep the selected network up to date after networks information is // updated (e.g., if the network status change); if (networks) { - setSelected(prev => { - return networksFromValues(networks).find(n => n.ssid === prev?.ssid); + setSelected((prev) => { + return networksFromValues(networks).find((n) => n.ssid === prev?.ssid); }); } }, [networks]); useEffect(() => { - setActiveNetwork(networksFromValues(networks).find(d => d.device)); + setActiveNetwork(networksFromValues(networks).find((d) => d.device)); }, [networks]); useEffect(() => { @@ -107,7 +112,7 @@ function WifiSelectorPage() { const current_device = devices.find((d) => d.name === name); if (data.state === DeviceState.FAILED) { - if (current_device && (data.stateReason === 7)) { + if (current_device && data.stateReason === 7) { console.log(`FAILED Device ${name} updated' with data`, data); setNeedAuth(current_device.connection); } diff --git a/web/src/components/network/routes.js b/web/src/components/network/routes.js index 7c4e21a819..2627c6381f 100644 --- a/web/src/components/network/routes.js +++ b/web/src/components/network/routes.js @@ -40,7 +40,7 @@ const loaders = { }, connection: async ({ params }) => { const connections = await client.network.connections(); - return connections.find(c => c.id === params.id); + return connections.find((c) => c.id === params.id); }, wifis: async () => { const connections = await client.network.connections(); @@ -57,21 +57,21 @@ const routes = { element: , handle: { name: N_("Network"), - icon: "settings_ethernet" + icon: "settings_ethernet", }, children: [ { index: true, element: , loader: loaders.all }, { path: "connections/:id/edit", element: , - loader: loaders.connection + loader: loaders.connection, }, { path: "wifis", element: , loader: loaders.wifis, - } - ] + }, + ], }; export default routes; diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.jsx index a3f29f6945..9b6b944db7 100644 --- a/web/src/components/overview/L10nSection.test.jsx +++ b/web/src/components/overview/L10nSection.test.jsx @@ -26,7 +26,7 @@ import { L10nSection } from "~/components/overview"; const locales = [ { id: "en_US.UTF-8", name: "English", territory: "United States" }, - { id: "de_DE.UTF-8", name: "German", territory: "Germany" } + { id: "de_DE.UTF-8", name: "German", territory: "Germany" }, ]; jest.mock("~/queries/l10n", () => ({ diff --git a/web/src/components/overview/OverviewPage.jsx b/web/src/components/overview/OverviewPage.jsx index 78185b647d..ebc4532ec4 100644 --- a/web/src/components/overview/OverviewPage.jsx +++ b/web/src/components/overview/OverviewPage.jsx @@ -22,8 +22,10 @@ import React, { useEffect, useState } from "react"; import { CardBody, - Grid, GridItem, - Hint, HintBody, + Grid, + GridItem, + Hint, + HintBody, NotificationDrawer, NotificationDrawerBody, NotificationDrawerList, @@ -44,7 +46,7 @@ import { _ } from "~/i18n"; const SCOPE_HEADERS = { users: _("Users"), storage: _("Storage"), - software: _("Software") + software: _("Software"), }; const ReadyForInstallation = () => ( @@ -65,7 +67,11 @@ const IssuesList = ({ issues }) => { const link = ( - + {issue.description} @@ -78,9 +84,7 @@ const IssuesList = ({ issues }) => { return ( - - {list} - + {list} ); @@ -94,13 +98,11 @@ export default function OverviewPage() { client.issues().then(setIssues); }, [client]); - const resultSectionProps = - issues.isEmpty - ? {} - : { - + const resultSectionProps = issues.isEmpty + ? {} + : { label: _("Installation"), - description: _("Before installing, please check the following problems.") + description: _("Before installing, please check the following problems."), }; return ( @@ -111,7 +113,7 @@ export default function OverviewPage() { {_( - "Take your time to check your configuration before starting the installation process." + "Take your time to check your configuration before starting the installation process.", )} @@ -120,7 +122,7 @@ export default function OverviewPage() { diff --git a/web/src/components/overview/OverviewPage.test.jsx b/web/src/components/overview/OverviewPage.test.jsx index cb0a26f2b5..a5024b9284 100644 --- a/web/src/components/overview/OverviewPage.test.jsx +++ b/web/src/components/overview/OverviewPage.test.jsx @@ -32,7 +32,7 @@ jest.mock("~/client"); jest.mock("~/queries/software", () => ({ ...jest.requireActual("~/queries/software"), useProduct: () => ({ selectedProduct: mockSelectedProduct }), - useProductChanges: () => jest.fn() + useProductChanges: () => jest.fn(), })); jest.mock("~/components/overview/L10nSection", () => () =>
      Localization Section
      ); @@ -44,9 +44,9 @@ beforeEach(() => { createClient.mockImplementation(() => { return { manager: { - startInstallation: startInstallationFn + startInstallation: startInstallationFn, }, - issues: jest.fn().mockResolvedValue({ isEmpty: true }) + issues: jest.fn().mockResolvedValue({ isEmpty: true }), }; }); }); diff --git a/web/src/components/overview/SoftwareSection.jsx b/web/src/components/overview/SoftwareSection.jsx index 69b7d9664c..3070acd1d2 100644 --- a/web/src/components/overview/SoftwareSection.jsx +++ b/web/src/components/overview/SoftwareSection.jsx @@ -46,7 +46,7 @@ export default function SoftwareSection() { if (proposal.patterns === undefined) return; const ids = Object.keys(proposal.patterns); - const selected = patterns.filter(p => ids.includes(p.name)).sort((a, b) => a.order - b.order); + const selected = patterns.filter((p) => ids.includes(p.name)).sort((a, b) => a.order - b.order); setSelectedPatterns(selected); }, [client, proposal, patterns]); @@ -73,7 +73,7 @@ export default function SoftwareSection() { {msg2} - {selectedPatterns.map(p => ( + {selectedPatterns.map((p) => ( {p.summary} ))} diff --git a/web/src/components/overview/SoftwareSection.test.jsx b/web/src/components/overview/SoftwareSection.test.jsx index 7c32f146ea..5c9d779f1f 100644 --- a/web/src/components/overview/SoftwareSection.test.jsx +++ b/web/src/components/overview/SoftwareSection.test.jsx @@ -33,7 +33,7 @@ const gnomePattern = { category: "Graphical Environments", icon: "./pattern-gnome", summary: "GNOME Desktop Environment (Wayland)", - order: 1120 + order: 1120, }; const kdePattern = { @@ -41,7 +41,7 @@ const kdePattern = { category: "Graphical Environments", icon: "./pattern-kde", summary: "KDE Applications and Plasma Desktop", - order: 1110 + order: 1110, }; beforeEach(() => { @@ -50,8 +50,8 @@ beforeEach(() => { software: { onSelectedPatternsChanged: noop, getProposal: jest.fn().mockResolvedValue({ size: "500 MiB", patterns: { kde: 1 } }), - getPatterns: jest.fn().mockResolvedValue([gnomePattern, kdePattern]) - } + getPatterns: jest.fn().mockResolvedValue([gnomePattern, kdePattern]), + }, }; }); }); diff --git a/web/src/components/overview/StorageSection.jsx b/web/src/components/overview/StorageSection.jsx index b26dc82ee9..2a579eba3b 100644 --- a/web/src/components/overview/StorageSection.jsx +++ b/web/src/components/overview/StorageSection.jsx @@ -45,27 +45,27 @@ import { IDLE } from "~/client/status"; * @param {String} policy Find space policy * @returns {String} Translated description */ -const msgLvmMultipleDisks = policy => { +const msgLvmMultipleDisks = (policy) => { switch (policy) { case "resize": // TRANSLATORS: installing on an LVM with multiple physical partitions/disks return _( - "Install in a new Logical Volume Manager (LVM) volume group shrinking existing partitions at the underlying devices as needed" + "Install in a new Logical Volume Manager (LVM) volume group shrinking existing partitions at the underlying devices as needed", ); case "keep": // TRANSLATORS: installing on an LVM with multiple physical partitions/disks return _( - "Install in a new Logical Volume Manager (LVM) volume group without modifying the partitions at the underlying devices" + "Install in a new Logical Volume Manager (LVM) volume group without modifying the partitions at the underlying devices", ); case "delete": // TRANSLATORS: installing on an LVM with multiple physical partitions/disks return _( - "Install in a new Logical Volume Manager (LVM) volume group deleting all the content of the underlying devices" + "Install in a new Logical Volume Manager (LVM) volume group deleting all the content of the underlying devices", ); case "custom": // TRANSLATORS: installing on an LVM with multiple physical partitions/disks return _( - "Install in a new Logical Volume Manager (LVM) volume group using a custom strategy to find the needed space at the underlying devices" + "Install in a new Logical Volume Manager (LVM) volume group using a custom strategy to find the needed space at the underlying devices", ); } }; @@ -77,31 +77,31 @@ const msgLvmMultipleDisks = policy => { * @returns {String} Translated description with %s placeholder for the device * name */ -const msgLvmSingleDisk = policy => { +const msgLvmSingleDisk = (policy) => { switch (policy) { case "resize": // TRANSLATORS: installing on an LVM with a single physical partition/disk, // %s will be replaced by a device name and its size (eg. "/dev/sda, 20 GiB") return _( - "Install in a new Logical Volume Manager (LVM) volume group on %s shrinking existing partitions as needed" + "Install in a new Logical Volume Manager (LVM) volume group on %s shrinking existing partitions as needed", ); case "keep": // TRANSLATORS: installing on an LVM with a single physical partition/disk, // %s will be replaced by a device name and its size (eg. "/dev/sda, 20 GiB") return _( - "Install in a new Logical Volume Manager (LVM) volume group on %s without modifying existing partitions" + "Install in a new Logical Volume Manager (LVM) volume group on %s without modifying existing partitions", ); case "delete": // TRANSLATORS: installing on an LVM with a single physical partition/disk, // %s will be replaced by a device name and its size (eg. "/dev/sda, 20 GiB") return _( - "Install in a new Logical Volume Manager (LVM) volume group on %s deleting all its content" + "Install in a new Logical Volume Manager (LVM) volume group on %s deleting all its content", ); case "custom": // TRANSLATORS: installing on an LVM with a single physical partition/disk, // %s will be replaced by a device name and its size (eg. "/dev/sda, 20 GiB") return _( - "Install in a new Logical Volume Manager (LVM) volume group on %s using a custom strategy to find the needed space" + "Install in a new Logical Volume Manager (LVM) volume group on %s using a custom strategy to find the needed space", ); } }; @@ -137,7 +137,7 @@ export default function StorageSection() { useEffect(loadProposal, [loadProposal]); useEffect(() => { - return client.storage.onStatusChange(status => { + return client.storage.onStatusChange((status) => { if (status === IDLE) { loadProposal(); } @@ -146,8 +146,8 @@ export default function StorageSection() { if (result === undefined) return; - const label = deviceName => { - const device = availableDevices.find(d => d.name === deviceName); + const label = (deviceName) => { + const device = availableDevices.find((d) => d.name === deviceName); return device ? deviceLabel(device) : deviceName; }; @@ -155,7 +155,11 @@ export default function StorageSection() { const pvDevices = result.settings.targetPVDevices; if (pvDevices.length > 1) { - return {msgLvmMultipleDisks(result.settings.spacePolicy)}; + return ( + + {msgLvmMultipleDisks(result.settings.spacePolicy)} + + ); } else { const [msg1, msg2] = msgLvmSingleDisk(result.settings.spacePolicy).split("%s"); @@ -174,7 +178,7 @@ export default function StorageSection() { const targetDevice = result.settings.targetDevice; if (!targetDevice) return {_("No device selected yet")}; - const fullMsg = policy => { + const fullMsg = (policy) => { switch (policy) { case "resize": // TRANSLATORS: %s will be replaced by the device name and its size, diff --git a/web/src/components/overview/StorageSection.test.jsx b/web/src/components/overview/StorageSection.test.jsx index 26de852153..5a3ba9a431 100644 --- a/web/src/components/overview/StorageSection.test.jsx +++ b/web/src/components/overview/StorageSection.test.jsx @@ -29,25 +29,25 @@ jest.mock("~/client"); const availableDevices = [ { name: "/dev/sda", size: 536870912000 }, - { name: "/dev/sdb", size: 697932185600 } + { name: "/dev/sdb", size: 697932185600 }, ]; const proposalResult = { settings: { target: "disk", targetDevice: "/dev/sda", - spacePolicy: "delete" + spacePolicy: "delete", }, - actions: [] + actions: [], }; const storageMock = { probe: jest.fn().mockResolvedValue(0), proposal: { getAvailableDevices: jest.fn().mockResolvedValue(availableDevices), - getResult: jest.fn().mockResolvedValue(proposalResult) + getResult: jest.fn().mockResolvedValue(proposalResult), }, - onStatusChange: jest.fn() + onStatusChange: jest.fn(), }; let storage; diff --git a/web/src/components/overview/routes.js b/web/src/components/overview/routes.js index ec0a565658..f9f98041a5 100644 --- a/web/src/components/overview/routes.js +++ b/web/src/components/overview/routes.js @@ -28,8 +28,8 @@ const routes = { element: , handle: { name: N_("Overview"), - icon: "list_alt" - } + icon: "list_alt", + }, }; export default routes; diff --git a/web/src/components/product/ProductRegistrationForm.test.jsx b/web/src/components/product/ProductRegistrationForm.test.jsx index 645285f26b..2c4a9ea44e 100644 --- a/web/src/components/product/ProductRegistrationForm.test.jsx +++ b/web/src/components/product/ProductRegistrationForm.test.jsx @@ -25,12 +25,12 @@ import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProductRegistrationForm } from "~/components/product"; -it.skip("renders a field for entering the registration code", async() => { +it.skip("renders a field for entering the registration code", async () => { plainRender(); await screen.findByLabelText(/Registration code/); }); -it.skip("renders a field for entering an email", async() => { +it.skip("renders a field for entering an email", async () => { plainRender(); await screen.findByLabelText("Email"); }); @@ -42,7 +42,9 @@ const ProductRegistrationFormTest = () => { return ( <> - + {isSubmitted &&

      Form is submitted!

      } {isValid === false &&

      Form is not valid!

      } diff --git a/web/src/components/product/ProductRegistrationPage.jsx b/web/src/components/product/ProductRegistrationPage.jsx index 9b74b7beb2..df3672003b 100644 --- a/web/src/components/product/ProductRegistrationPage.jsx +++ b/web/src/components/product/ProductRegistrationPage.jsx @@ -64,29 +64,26 @@ export default function ProductRegistrationPage() { <>

      {sprintf(_("Register %s"), selectedProduct.name)}

      - { - error && + {error && (

      {error}

      - } + )}
      setCode(v)} /> - setEmail(v)} - /> + setEmail(v)} />
      - {_("Accept")} + + {_("Accept")} + ); diff --git a/web/src/components/product/ProductSelectionPage.jsx b/web/src/components/product/ProductSelectionPage.jsx index 1ba59c83cd..bd5f2a8403 100644 --- a/web/src/components/product/ProductSelectionPage.jsx +++ b/web/src/components/product/ProductSelectionPage.jsx @@ -20,14 +20,8 @@ */ import React, { useState } from "react"; -import { - Card, CardBody, - Flex, - Form, - Grid, GridItem, - Radio -} from "@patternfly/react-core"; -import styles from '@patternfly/react-styles/css/utilities/Text/text'; +import { Card, CardBody, Flex, Form, Grid, GridItem, Radio } from "@patternfly/react-core"; +import styles from "@patternfly/react-styles/css/utilities/Text/text"; import { _ } from "~/i18n"; import { Page } from "~/components/core"; @@ -35,9 +29,7 @@ import { Loading, Center } from "~/components/layout"; import { useConfigMutation, useProduct } from "~/queries/software"; const Label = ({ children }) => ( - - {children} - + {children} ); function ProductSelectionPage() { @@ -63,7 +55,7 @@ function ProductSelectionPage() { ); }; - const isSelectionDisabled = !nextProduct || (nextProduct === selectedProduct); + const isSelectionDisabled = !nextProduct || nextProduct === selectedProduct; return (
      diff --git a/web/src/components/product/ProductSelectionPage.test.jsx b/web/src/components/product/ProductSelectionPage.test.jsx index 4a9db4db8a..f742352726 100644 --- a/web/src/components/product/ProductSelectionPage.test.jsx +++ b/web/src/components/product/ProductSelectionPage.test.jsx @@ -29,13 +29,13 @@ const products = [ { id: "Tumbleweed", name: "openSUSE Tumbleweed", - description: "Tumbleweed description..." + description: "Tumbleweed description...", }, { id: "MicroOS", name: "openSUSE MicroOS", - description: "MicroOS description" - } + description: "MicroOS description", + }, ]; jest.mock("~/client"); @@ -44,21 +44,21 @@ jest.mock("~/queries/software", () => ({ useProduct: () => { return { products, - selectedProduct: products[0] + selectedProduct: products[0], }; }, - useProductChanges: () => jest.fn() + useProductChanges: () => jest.fn(), })); const managerMock = { - startProbing: jest.fn() + startProbing: jest.fn(), }; const productMock = { getAll: () => Promise.resolve(products), getSelected: jest.fn(() => Promise.resolve(products[0])), select: jest.fn().mockResolvedValue(), - onChange: jest.fn() + onChange: jest.fn(), }; beforeEach(() => { diff --git a/web/src/components/questions/GenericQuestion.jsx b/web/src/components/questions/GenericQuestion.jsx index f0f3802cff..8028b4be97 100644 --- a/web/src/components/questions/GenericQuestion.jsx +++ b/web/src/components/questions/GenericQuestion.jsx @@ -33,9 +33,7 @@ export default function GenericQuestion({ question, answerCallback }) { return ( - - { question.text } - + {question.text} ( - installerRender() -); +const renderQuestion = () => + installerRender(); describe("GenericQuestion", () => { it("renders the question text", async () => { @@ -44,7 +43,7 @@ describe("GenericQuestion", () => { await screen.findByText(question.text); }); - it("sets chosen option and calls the callback after user clicking an action", async() => { + it("sets chosen option and calls the callback after user clicking an action", async () => { const { user } = renderQuestion(); let button = await screen.findByRole("button", { name: /Sometimes/ }); diff --git a/web/src/components/questions/LuksActivationQuestion.jsx b/web/src/components/questions/LuksActivationQuestion.jsx index e42b0eb969..8e31b915fc 100644 --- a/web/src/components/questions/LuksActivationQuestion.jsx +++ b/web/src/components/questions/LuksActivationQuestion.jsx @@ -60,12 +60,10 @@ export default function LuksActivationQuestion({ question, answerCallback }) { aria-label={_("Question")} titleIconVariant={() => } > - { renderAlert(parseInt(question.data.attempt)) } - - { question.text } - + {renderAlert(parseInt(question.data.attempt))} + {question.text}
      - { /* TRANSLATORS: field label */ } + {/* TRANSLATORS: field label */} ( - installerRender() -); +const renderQuestion = () => + installerRender(); describe("LuksActivationQuestion", () => { beforeEach(() => { @@ -97,7 +96,7 @@ describe("LuksActivationQuestion", () => { }); describe("by clicking on 'Skip'", () => { - it("calls the callback after setting both, answer and password", async() => { + it("calls the callback after setting both, answer and password", async () => { const { user } = renderQuestion(); const passwordInput = await screen.findByLabelText("Encryption Password"); @@ -105,13 +104,15 @@ describe("LuksActivationQuestion", () => { const skipButton = await screen.findByRole("button", { name: /Skip/ }); await user.click(skipButton); - expect(question).toEqual(expect.objectContaining({ password: "notSecret", answer: "skip" })); + expect(question).toEqual( + expect.objectContaining({ password: "notSecret", answer: "skip" }), + ); expect(answerFn).toHaveBeenCalledWith(question); }); }); describe("by clicking on 'Decrypt'", () => { - it("calls the callback after setting both, answer and password", async() => { + it("calls the callback after setting both, answer and password", async () => { const { user } = renderQuestion(); const passwordInput = await screen.findByLabelText("Encryption Password"); @@ -119,19 +120,23 @@ describe("LuksActivationQuestion", () => { const decryptButton = await screen.findByRole("button", { name: /Decrypt/ }); await user.click(decryptButton); - expect(question).toEqual(expect.objectContaining({ password: "notSecret", answer: "decrypt" })); + expect(question).toEqual( + expect.objectContaining({ password: "notSecret", answer: "decrypt" }), + ); expect(answerFn).toHaveBeenCalledWith(question); }); }); describe("submitting the form by pressing 'enter'", () => { - it("calls the callback after setting both, answer and password", async() => { + it("calls the callback after setting both, answer and password", async () => { const { user } = renderQuestion(); const passwordInput = await screen.findByLabelText("Encryption Password"); await user.type(passwordInput, "notSecret{enter}"); - expect(question).toEqual(expect.objectContaining({ password: "notSecret", answer: "decrypt" })); + expect(question).toEqual( + expect.objectContaining({ password: "notSecret", answer: "decrypt" }), + ); expect(answerFn).toHaveBeenCalledWith(question); }); }); diff --git a/web/src/components/questions/QuestionActions.jsx b/web/src/components/questions/QuestionActions.jsx index 021f1ca1cc..f0ececc376 100644 --- a/web/src/components/questions/QuestionActions.jsx +++ b/web/src/components/questions/QuestionActions.jsx @@ -31,7 +31,7 @@ import { Popup } from "~/components/core"; * @param {String} text - string to be capitalized * @return {String} capitalized text */ -const label = text => `${text[0].toUpperCase()}${text.slice(1)}`; +const label = (text) => `${text[0].toUpperCase()}${text.slice(1)}`; /** * A component for building a Question actions, using the defaultAction @@ -48,7 +48,7 @@ const label = text => `${text[0].toUpperCase()}${text.slice(1)}`; * @param {Object} [props.conditions={}] - an object holding conditions, like when an action is disabled */ export default function QuestionActions({ actions, defaultAction, actionCallback, conditions }) { - let [[primaryAction], secondaryActions] = partition(actions, a => a === defaultAction); + let [[primaryAction], secondaryActions] = partition(actions, (a) => a === defaultAction); // Ensure there is always a primary action if (!primaryAction) [primaryAction, ...secondaryActions] = secondaryActions; @@ -62,17 +62,15 @@ export default function QuestionActions({ actions, defaultAction, actionCallback > {label(primaryAction)} - { - secondaryActions.map(action => - actionCallback(action)} - isDisabled={conditions?.disable?.[action]} - > - {label(action)} - - ) - } + {secondaryActions.map((action) => ( + actionCallback(action)} + isDisabled={conditions?.disable?.[action]} + > + {label(action)} + + ))} ); } diff --git a/web/src/components/questions/QuestionActions.test.jsx b/web/src/components/questions/QuestionActions.test.jsx index c6b7475866..f390d1d2d2 100644 --- a/web/src/components/questions/QuestionActions.test.jsx +++ b/web/src/components/questions/QuestionActions.test.jsx @@ -30,21 +30,20 @@ let question = { id: 1, text: "Should we use a component for rendering actions?", options: ["no", "maybe", "sure"], - defaultOption + defaultOption, }; const actionCallback = jest.fn(); -const renderQuestionActions = () => ( +const renderQuestionActions = () => installerRender( - ) -); + conditions={{ disable: { no: true } }} + />, + ); describe("QuestionActions", () => { describe("when question has a default option", () => { @@ -95,10 +94,10 @@ describe("QuestionActions", () => { renderQuestionActions(); let button = await screen.findByRole("button", { name: "No" }); - expect(button).toHaveAttribute('disabled'); + expect(button).toHaveAttribute("disabled"); button = await screen.findByRole("button", { name: "Maybe" }); - expect(button).not.toHaveAttribute('disabled'); + expect(button).not.toHaveAttribute("disabled"); }); it("calls the actionCallback when user clicks on action", async () => { diff --git a/web/src/components/questions/Questions.jsx b/web/src/components/questions/Questions.jsx index 2b5e48e505..14be33362d 100644 --- a/web/src/components/questions/Questions.jsx +++ b/web/src/components/questions/Questions.jsx @@ -40,10 +40,13 @@ export default function Questions() { [], ); - const answerQuestion = useCallback((question) => { - client.questions.answer(question); - removeQuestion(question.id); - }, [client.questions, removeQuestion]); + const answerQuestion = useCallback( + (question) => { + client.questions.answer(question); + removeQuestion(question.id); + }, + [client.questions, removeQuestion], + ); useEffect(() => { client.questions.listenQuestions(); diff --git a/web/src/components/questions/Questions.test.jsx b/web/src/components/questions/Questions.test.jsx index 6c4d6e625b..e951a853f9 100644 --- a/web/src/components/questions/Questions.test.jsx +++ b/web/src/components/questions/Questions.test.jsx @@ -29,10 +29,9 @@ import { Questions } from "~/components/questions"; jest.mock("~/client"); jest.mock("~/components/questions/GenericQuestion", () => () =>
      A Generic question mock
      ); -jest.mock( - "~/components/questions/LuksActivationQuestion", - () => () =>
      A LUKS activation question mock
      , -); +jest.mock("~/components/questions/LuksActivationQuestion", () => () => ( +
      A LUKS activation question mock
      +)); const handlers = {}; const genericQuestion = { id: 1, type: "generic" }; diff --git a/web/src/components/software/SoftwarePage.jsx b/web/src/components/software/SoftwarePage.jsx index a758559b4f..49c55ee89e 100644 --- a/web/src/components/software/SoftwarePage.jsx +++ b/web/src/components/software/SoftwarePage.jsx @@ -39,7 +39,7 @@ import { DescriptionListTerm, Grid, GridItem, - Stack + Stack, } from "@patternfly/react-core"; /** @@ -61,11 +61,11 @@ import { */ function buildPatterns(patterns, selection) { return patterns - .map(pattern => { + .map((pattern) => { const selectedBy = selection[pattern.name] !== undefined ? selection[pattern.name] : 2; return { ...pattern, - selectedBy + selectedBy, }; }) .sort((a, b) => a.order - b.order); @@ -79,7 +79,7 @@ function buildPatterns(patterns, selection) { * @return {JSX.Element} */ const SelectedPatternsList = ({ patterns }) => { - const selected = patterns.filter(p => p.selectedBy !== SelectedBy.NONE); + const selected = patterns.filter((p) => p.selectedBy !== SelectedBy.NONE); if (selected.length === 0) { return <>{_("No additional software was selected.")}; @@ -89,7 +89,7 @@ const SelectedPatternsList = ({ patterns }) => {

      {_("The following software patterns are selected for installation:")}

      - {selected.map(pattern => ( + {selected.map((pattern) => ( {pattern.summary} {pattern.description} @@ -125,8 +125,8 @@ function SoftwarePage() { useEffect(() => { if (!patterns) return; - return client.software.onSelectedPatternsChanged(selection => { - client.software.getProposal().then(proposal => setProposal(proposal)); + return client.software.onSelectedPatternsChanged((selection) => { + client.software.getProposal().then((proposal) => setProposal(proposal)); setPatterns(buildPatterns(patterns, selection)); }); }, [client.software, patterns]); @@ -162,12 +162,12 @@ function SoftwarePage() { - {_("Change selection")} - - } + label={_("Selected patterns")} + actions={ + + {_("Change selection")} + + } > diff --git a/web/src/components/software/SoftwarePatternsSelection.jsx b/web/src/components/software/SoftwarePatternsSelection.jsx index c431d888b5..5f0acdf647 100644 --- a/web/src/components/software/SoftwarePatternsSelection.jsx +++ b/web/src/components/software/SoftwarePatternsSelection.jsx @@ -31,7 +31,7 @@ import { DataListItemCells, DataListItemRow, SearchInput, - Stack + Stack, } from "@patternfly/react-core"; import { Section, Page } from "~/components/core"; @@ -109,13 +109,15 @@ function sortGroups(groups) { * @return {Pattern[]} List of patterns including its selection status */ function buildPatterns(patterns, selection) { - return patterns.map((pattern) => { - const selectedBy = (selection[pattern.name] !== undefined) ? selection[pattern.name] : 2; - return { - ...pattern, - selectedBy, - }; - }).sort((a, b) => a.order - b.order); + return patterns + .map((pattern) => { + const selectedBy = selection[pattern.name] !== undefined ? selection[pattern.name] : 2; + return { + ...pattern, + selectedBy, + }; + }) + .sort((a, b) => a.order - b.order); } /** @@ -151,9 +153,10 @@ function SoftwarePatternsSelection() { if (searchValue !== "") { // case insensitive search const searchData = searchValue.toUpperCase(); - const filtered = patterns.filter((p) => - p.name.toUpperCase().indexOf(searchData) !== -1 || - p.description.toUpperCase().indexOf(searchData) !== -1 + const filtered = patterns.filter( + (p) => + p.name.toUpperCase().indexOf(searchData) !== -1 || + p.description.toUpperCase().indexOf(searchData) !== -1, ); setVisiblePatterns(filtered); } else { @@ -166,17 +169,21 @@ function SoftwarePatternsSelection() { }); }, [patterns, searchValue, client.software]); - const onToggle = useCallback((name) => { - const selected = patterns.filter((p) => p.selectedBy === SelectedBy.USER) - .reduce((all, p) => { - all[p.name] = true; - return all; - }, {}); - const pattern = patterns.find((p) => p.name === name); - selected[name] = pattern.selectedBy === SelectedBy.NONE; + const onToggle = useCallback( + (name) => { + const selected = patterns + .filter((p) => p.selectedBy === SelectedBy.USER) + .reduce((all, p) => { + all[p.name] = true; + return all; + }, {}); + const pattern = patterns.find((p) => p.name === name); + selected[name] = pattern.selectedBy === SelectedBy.NONE; - client.software.selectPatterns(selected); - }, [patterns, client.software]); + client.software.selectPatterns(selected); + }, + [patterns, client.software], + ); // FIXME: use loading indicator when busy, we cannot know if it will be // quickly or not in advance. @@ -190,45 +197,48 @@ function SoftwarePatternsSelection() { // be selected/deselected immediately. // TODO: extract to a DataListSelector component or so. let selector = sortGroups(groups).map((groupName) => { - const selectedIds = groups[groupName].filter((p) => p.selectedBy !== SelectedBy.NONE).map((p) => - p.name - ); + const selectedIds = groups[groupName] + .filter((p) => p.selectedBy !== SelectedBy.NONE) + .map((p) => p.name); return ( -
      +
      - { - groups[groupName].map(option => ( - - - onToggle(option.name)} aria-labelledby="check-action-item1" name="check-action-check1" isChecked={selectedIds.includes(option.name)} /> - - -
      - {option.summary} {option.selectedBy === SelectedBy.AUTO && } -
      -
      {option.description}
      -
      - , - ]} - /> -
      -
      - )) - } + {groups[groupName].map((option) => ( + + + onToggle(option.name)} + aria-labelledby="check-action-item1" + name="check-action-check1" + isChecked={selectedIds.includes(option.name)} + /> + + +
      + {option.summary}{" "} + {option.selectedBy === SelectedBy.AUTO && ( + + )} +
      +
      {option.description}
      +
      + , + ]} + /> +
      +
      + ))}
      ); }); if (selector.length === 0) { - selector = ( - {_("None of the patterns match the filter.")} - ); + selector = {_("None of the patterns match the filter.")}; } return ( @@ -250,9 +260,7 @@ function SoftwarePatternsSelection() { - - {selector} - + {selector} diff --git a/web/src/components/software/SoftwarePatternsSelection.test.jsx b/web/src/components/software/SoftwarePatternsSelection.test.jsx index 4d4633d640..1faf1225da 100644 --- a/web/src/components/software/SoftwarePatternsSelection.test.jsx +++ b/web/src/components/software/SoftwarePatternsSelection.test.jsx @@ -42,9 +42,7 @@ describe.skip("SoftwarePatternsSelection", () => { }); it("displays the patterns in a group in correct order", async () => { - plainRender( - , - ); + plainRender(); // the "Base Technologies" pattern group const baseGroup = await screen.findByRole("region", { name: "Base Technologies" }); @@ -58,9 +56,7 @@ describe.skip("SoftwarePatternsSelection", () => { }); it("displays only the matching patterns when using the search filter", async () => { - const { user } = plainRender( - , - ); + const { user } = plainRender(); // enter "multimedia" into the search filter const searchFilter = await screen.findByRole("textbox", { name: "Search" }); @@ -72,17 +68,16 @@ describe.skip("SoftwarePatternsSelection", () => { const desktopGroup = screen.getByRole("region", { name: "Desktop Functions" }); expect(within(desktopGroup).queryByRole("row", { name: /Multimedia/ })).toBeInTheDocument(); - expect(within(desktopGroup).queryByRole("row", { name: /Office Software/ })).not - .toBeInTheDocument(); + expect( + within(desktopGroup).queryByRole("row", { name: /Office Software/ }), + ).not.toBeInTheDocument(); }); it("displays the checkbox depending whether the patter is selected", async () => { const pattern = patterns.find((p) => p.name === "yast2_basis"); pattern.selectedBy = SelectedBy.USER; - plainRender( - , - ); + plainRender(); // the "Base Technologies" pattern group const baseGroup = await screen.findByRole("region", { name: "Base Technologies" }); diff --git a/web/src/components/software/UsedSize.jsx b/web/src/components/software/UsedSize.jsx index ce7467726a..499ec1369c 100644 --- a/web/src/components/software/UsedSize.jsx +++ b/web/src/components/software/UsedSize.jsx @@ -34,9 +34,7 @@ export default function UsedSize({ size }) { return ( -

      - {_("This space includes the base system and the selected software patterns.")} -

      +

      {_("This space includes the base system and the selected software patterns.")}

      ); } diff --git a/web/src/components/software/icons/README.md b/web/src/components/software/icons/README.md index c945854d48..99db442b53 100644 --- a/web/src/components/software/icons/README.md +++ b/web/src/components/software/icons/README.md @@ -1,4 +1,3 @@ # Status Icons -The package status icons were copied from the [libyui-qt-pkg]( -https://github.com/libyui/libyui/tree/master/libyui-qt-pkg/src/icons) package. +The package status icons were copied from the [libyui-qt-pkg](https://github.com/libyui/libyui/tree/master/libyui-qt-pkg/src/icons) package. diff --git a/web/src/components/software/routes.js b/web/src/components/software/routes.js index 0f821f1646..e6214c434f 100644 --- a/web/src/components/software/routes.js +++ b/web/src/components/software/routes.js @@ -30,18 +30,18 @@ const routes = { element: , handle: { name: N_("Software"), - icon: "apps" + icon: "apps", }, children: [ { index: true, - element: + element: , }, { path: "patterns/select", element: , - } - ] + }, + ], }; export default routes; diff --git a/web/src/components/storage/BootConfigField.jsx b/web/src/components/storage/BootConfigField.jsx index 669feb30db..eb509be202 100644 --- a/web/src/components/storage/BootConfigField.jsx +++ b/web/src/components/storage/BootConfigField.jsx @@ -42,11 +42,7 @@ import { Icon } from "~/components/layout"; const Link = ({ isBold = false }) => { const text = _("Change boot options"); - return ( - - {isBold ? {text} : text} - - ); + return {isBold ? {text} : text}; }; /** @@ -67,12 +63,7 @@ const Link = ({ isBold = false }) => { * * @param {BootConfigFieldProps} props */ -export default function BootConfigField({ - configureBoot, - bootDevice, - isLoading, - onChange -}) { +export default function BootConfigField({ configureBoot, bootDevice, isLoading, onChange }) { const onAccept = ({ configureBoot, bootDevice }) => { onChange({ configureBoot, bootDevice }); }; @@ -84,12 +75,20 @@ export default function BootConfigField({ let value; if (!configureBoot) { - value = <> {_("Installation will not configure partitions for booting.")}; + value = ( + <> + {" "} + {_("Installation will not configure partitions for booting.")} + + ); } else if (!bootDevice) { value = _("Installation will configure partitions for booting at the installation disk."); } else { // TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) - value = sprintf(_("Installation will configure partitions for booting at %s."), deviceLabel(bootDevice)); + value = sprintf( + _("Installation will configure partitions for booting at %s."), + deviceLabel(bootDevice), + ); } return ( diff --git a/web/src/components/storage/BootConfigField.test.jsx b/web/src/components/storage/BootConfigField.test.jsx index e478b95651..f1e66f0a5f 100644 --- a/web/src/components/storage/BootConfigField.test.jsx +++ b/web/src/components/storage/BootConfigField.test.jsx @@ -49,7 +49,7 @@ const sda = { name: "/dev/sda", size: 1024, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }; @@ -64,7 +64,7 @@ beforeEach(() => { defaultBootDevice: undefined, availableDevices: [sda], isLoading: false, - onChange: jest.fn() + onChange: jest.fn(), }; }); diff --git a/web/src/components/storage/BootSelection.jsx b/web/src/components/storage/BootSelection.jsx index 0dbb3383ad..0241f51a32 100644 --- a/web/src/components/storage/BootSelection.jsx +++ b/web/src/components/storage/BootSelection.jsx @@ -23,12 +23,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { - Card, CardBody, - Form, FormGroup, - Radio, - Stack -} from "@patternfly/react-core"; +import { Card, CardBody, Form, FormGroup, Radio, Stack } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { DevicesFormSelect } from "~/components/storage"; import { Page } from "~/components/core"; @@ -37,7 +32,7 @@ import { deviceLabel } from "~/components/storage/utils"; import { sprintf } from "sprintf-js"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; -import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; // FIXME: improve classNames // FIXME: improve and rename to BootSelectionDialog @@ -86,7 +81,7 @@ export default function BootSelectionDialog() { selectedOption = BOOT_MANUAL_ID; } - const findDevice = (name) => availableDevices.find(d => d.name === name); + const findDevice = (name) => availableDevices.find((d) => d.name === name); setState({ load: true, @@ -94,7 +89,7 @@ export default function BootSelectionDialog() { configureBoot, defaultBootDevice: findDevice(defaultBootDevice), availableDevices, - selectedOption + selectedOption, }); }; @@ -125,7 +120,7 @@ export default function BootSelectionDialog() { const description = _( "To ensure the new system is able to boot, the installer may need to create or configure some \ -partitions in the appropriate disk." +partitions in the appropriate disk.", ); const automaticText = () => { @@ -136,7 +131,7 @@ partitions in the appropriate disk." return sprintf( // TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") _("Partitions to boot will be allocated at the installation disk (%s)."), - deviceLabel(state.defaultBootDevice) + deviceLabel(state.defaultBootDevice), ); }; @@ -166,7 +161,12 @@ partitions in the appropriate disk." defaultChecked={state.selectedOption === BOOT_AUTO_ID} onChange={updateSelectedOption} label={ - + {_("Automatic")} } @@ -179,7 +179,12 @@ partitions in the appropriate disk." defaultChecked={state.selectedOption === BOOT_MANUAL_ID} onChange={updateSelectedOption} label={ - + {_("Select a disk")} } @@ -206,13 +211,20 @@ partitions in the appropriate disk." defaultChecked={state.selectedOption === BOOT_DISABLED_ID} onChange={updateSelectedOption} label={ - + {_("Do not configure")} } body={
      - {_("No partitions will be automatically configured for booting. Use with caution.")} + {_( + "No partitions will be automatically configured for booting. Use with caution.", + )}
      } /> diff --git a/web/src/components/storage/BootSelection.test.jsx b/web/src/components/storage/BootSelection.test.jsx index df352f933b..9b65e98c75 100644 --- a/web/src/components/storage/BootSelection.test.jsx +++ b/web/src/components/storage/BootSelection.test.jsx @@ -73,7 +73,7 @@ const sdb = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }; /** @type {StorageDevice} */ @@ -96,7 +96,7 @@ const sdc = { shrinking: { unsupported: ["Resizing is not supported"] }, systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }; let props; @@ -110,7 +110,7 @@ describe.skip("BootSelection", () => { bootDevice: undefined, defaultBootDevice: undefined, onCancel: jest.fn(), - onAccept: jest.fn() + onAccept: jest.fn(), }; }); @@ -205,7 +205,7 @@ describe.skip("BootSelection", () => { expect(props.onAccept).toHaveBeenCalledWith({ configureBoot: true, - bootDevice: undefined + bootDevice: undefined, }); }); }); @@ -229,7 +229,7 @@ describe.skip("BootSelection", () => { expect(props.onAccept).toHaveBeenCalledWith({ configureBoot: true, - bootDevice: sdb + bootDevice: sdb, }); }); }); @@ -250,7 +250,7 @@ describe.skip("BootSelection", () => { expect(props.onAccept).toHaveBeenCalledWith({ configureBoot: false, - bootDevice: undefined + bootDevice: undefined, }); }); }); diff --git a/web/src/components/storage/DASDFormatProgress.jsx b/web/src/components/storage/DASDFormatProgress.jsx index 74c9047523..5caac19079 100644 --- a/web/src/components/storage/DASDFormatProgress.jsx +++ b/web/src/components/storage/DASDFormatProgress.jsx @@ -20,7 +20,7 @@ */ import React, { useEffect, useState } from "react"; -import { Progress, Skeleton, Stack } from '@patternfly/react-core'; +import { Progress, Skeleton, Stack } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { _ } from "~/i18n"; import { useInstallerClient } from "~/context/installer"; @@ -30,14 +30,14 @@ export default function DASDFormatProgress({ job, devices, isOpen = true }) { const [progress, setProgress] = useState(undefined); useEffect(() => { - client.dasd.onFormatProgress(job.path, p => setProgress(p)); + client.dasd.onFormatProgress(job.path, (p) => setProgress(p)); }, [client.dasd, job.path]); const ProgressContent = ({ progress }) => { return ( {Object.entries(progress).map(([path, [total, step, done]]) => { - const device = devices.find(d => d.id === path.split("/").slice(-1)[0]); + const device = devices.find((d) => d.id === path.split("/").slice(-1)[0]); return ( + {progress ? : } ); diff --git a/web/src/components/storage/DASDPage.jsx b/web/src/components/storage/DASDPage.jsx index ec6e4a329e..2a11d067ed 100644 --- a/web/src/components/storage/DASDPage.jsx +++ b/web/src/components/storage/DASDPage.jsx @@ -36,19 +36,19 @@ const reducer = (state, action) => { case "ADD_DEVICE": { const { device } = payload; - if (state.devices.find(d => d.id === device.id)) return state; + if (state.devices.find((d) => d.id === device.id)) return state; return { ...state, devices: [...state.devices, device] }; } case "UPDATE_DEVICE": { const { device } = payload; - const index = state.devices.findIndex(d => d.id === device.id); + const index = state.devices.findIndex((d) => d.id === device.id); const devices = [...state.devices]; - index !== -1 ? devices[index] = device : devices.push(device); + index !== -1 ? (devices[index] = device) : devices.push(device); - const selectedDevicesIds = state.selectedDevices.map(d => d.id); - const selectedDevices = devices.filter(d => selectedDevicesIds.includes(d.id)); + const selectedDevicesIds = state.selectedDevices.map((d) => d.id); + const selectedDevices = devices.filter((d) => selectedDevicesIds.includes(d.id)); return { ...state, devices, selectedDevices }; } @@ -56,7 +56,7 @@ const reducer = (state, action) => { case "REMOVE_DEVICE": { const { device } = payload; - return { ...state, devices: state.devices.filter(d => d.id !== device.id) }; + return { ...state, devices: state.devices.filter((d) => d.id !== device.id) }; } case "SET_MIN_CHANNEL": { @@ -76,7 +76,7 @@ const reducer = (state, action) => { case "UNSELECT_DEVICE": { const { device } = payload; - return { ...state, selectedDevices: state.selectedDevices.filter(d => d.id !== device.id) }; + return { ...state, selectedDevices: state.selectedDevices.filter((d) => d.id !== device.id) }; } case "SELECT_ALL_DEVICES": { @@ -158,17 +158,21 @@ export default function DASDPage() { const action = (type, device) => dispatch({ type, payload: { device } }); subscriptions.push( - await client.dasd.deviceEventListener("added", d => action("ADD_DEVICE", d)), - await client.dasd.deviceEventListener("removed", d => action("REMOVE_DEVICE", d)), - await client.dasd.deviceEventListener("changed", d => action("UPDATE_DEVICE", d)) + await client.dasd.deviceEventListener("added", (d) => action("ADD_DEVICE", d)), + await client.dasd.deviceEventListener("removed", (d) => action("REMOVE_DEVICE", d)), + await client.dasd.deviceEventListener("changed", (d) => action("UPDATE_DEVICE", d)), ); - await client.dasd.onJobAdded((data) => dispatch({ type: "START_FORMAT_JOB", payload: { data } })); - await client.dasd.onJobChanged((data) => dispatch({ type: "UPDATE_FORMAT_JOB", payload: { data } })); + await client.dasd.onJobAdded((data) => + dispatch({ type: "START_FORMAT_JOB", payload: { data } }), + ); + await client.dasd.onJobChanged((data) => + dispatch({ type: "UPDATE_FORMAT_JOB", payload: { data } }), + ); }; const unsubscribe = () => { - subscriptions.forEach(fn => fn()); + subscriptions.forEach((fn) => fn()); }; subscribe(); @@ -178,7 +182,9 @@ export default function DASDPage() { return ( <> - {state.formatJob.running && } + {state.formatJob.running && ( + + )} ); } diff --git a/web/src/components/storage/DASDTable.jsx b/web/src/components/storage/DASDTable.jsx index 9f4032e04c..9937b1c4c9 100644 --- a/web/src/components/storage/DASDTable.jsx +++ b/web/src/components/storage/DASDTable.jsx @@ -23,17 +23,24 @@ import React, { useState } from "react"; import { Button, Divider, - Dropdown, DropdownItem, DropdownList, + Dropdown, + DropdownItem, + DropdownList, MenuToggle, - TextInputGroup, TextInputGroupMain, TextInputGroupUtilities, - Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem -} from '@patternfly/react-core'; -import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from "@patternfly/react-core"; +import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { Icon } from "~/components/layout"; import { SectionSkeleton } from "~/components/core"; import { _ } from "~/i18n"; import { hex } from "~/utils"; -import { sort } from 'fast-sort'; +import { sort } from "fast-sort"; import { useInstallerClient } from "~/context/installer"; // FIXME: please, note that this file still requiring refinements until reach a @@ -42,13 +49,12 @@ const columnData = (device, column) => { let data = device[column.id]; switch (column.id) { - case 'formatted': - case 'diag': - if (!device.enabled) - data = ""; + case "formatted": + case "diag": + if (!device.enabled) data = ""; break; - case 'partitionInfo': - data = data.split(",").map(d =>
      {d}
      ); + case "partitionInfo": + data = data.split(",").map((d) =>
      {d}
      ); break; } @@ -69,7 +75,7 @@ const columns = [ // usually keep untranslated { id: "diag", label: _("DIAG") }, { id: "formatted", label: _("Formatted") }, - { id: "partitionInfo", label: _("Partition Info") } + { id: "partitionInfo", label: _("Partition Info") }, ]; const Actions = ({ devices, isDisabled }) => { @@ -84,7 +90,7 @@ const Actions = ({ devices, isDisabled }) => { const setDiagOn = () => client.dasd.setDIAG(devices, true); const setDiagOff = () => client.dasd.setDIAG(devices, false); const format = () => { - const offline = devices.filter(d => !d.enabled); + const offline = devices.filter((d) => !d.enabled); if (offline.length > 0) { return false; @@ -94,7 +100,9 @@ const Actions = ({ devices, isDisabled }) => { }; const Action = ({ children, ...props }) => ( - {children} + + {children} + ); return ( @@ -109,38 +117,48 @@ const Actions = ({ devices, isDisabled }) => { )} > - { /** TRANSLATORS: drop down menu action, activate the device */} - {_("Activate")} - { /** TRANSLATORS: drop down menu action, deactivate the device */} - {_("Deactivate")} + {/** TRANSLATORS: drop down menu action, activate the device */} + + {_("Activate")} + + {/** TRANSLATORS: drop down menu action, deactivate the device */} + + {_("Deactivate")} + - { /** TRANSLATORS: drop down menu action, enable DIAG access method */} - {_("Set DIAG On")} - { /** TRANSLATORS: drop down menu action, disable DIAG access method */} - {_("Set DIAG Off")} + {/** TRANSLATORS: drop down menu action, enable DIAG access method */} + + {_("Set DIAG On")} + + {/** TRANSLATORS: drop down menu action, disable DIAG access method */} + + {_("Set DIAG Off")} + - { /** TRANSLATORS: drop down menu action, format the disk */} - {_("Format")} + {/** TRANSLATORS: drop down menu action, format the disk */} + + {_("Format")} + ); }; const filterDevices = (devices, from, to) => { - const allChannels = devices.map(d => d.hexId); + const allChannels = devices.map((d) => d.hexId); const min = hex(from) || Math.min(...allChannels); const max = hex(to) || Math.max(...allChannels); - return devices.filter(d => d.hexId >= min && d.hexId <= max); + return devices.filter((d) => d.hexId >= min && d.hexId <= max); }; export default function DASDTable({ state, dispatch }) { const [sortingColumn, setSortingColumn] = useState(columns[0]); - const [sortDirection, setSortDirection] = useState('asc'); + const [sortDirection, setSortDirection] = useState("asc"); - const sortColumnIndex = () => columns.findIndex(c => c.id === sortingColumn.id); + const sortColumnIndex = () => columns.findIndex((c) => c.id === sortingColumn.id); const filteredDevices = filterDevices(state.devices, state.minChannel, state.maxChannel); - const selectedDevicesIds = state.selectedDevices.map(d => d.id); + const selectedDevicesIds = state.selectedDevices.map((d) => d.id); // Selecting const selectAll = (isSelecting = true) => { @@ -156,7 +174,7 @@ export default function DASDTable({ state, dispatch }) { // Sorting // See https://github.com/snovakovic/fast-sort const sortBy = sortingColumn.sortBy || sortingColumn.id; - const sortedDevices = sort(filteredDevices)[sortDirection](d => d[sortBy]); + const sortedDevices = sort(filteredDevices)[sortDirection]((d) => d[sortBy]); // FIXME: this can be improved and even extracted to be used with other tables. const getSortParams = (columnIndex) => { @@ -166,7 +184,7 @@ export default function DASDTable({ state, dispatch }) { setSortingColumn(columns[index]); setSortDirection(direction); }, - columnIndex + columnIndex, }; }; @@ -194,15 +212,35 @@ export default function DASDTable({ state, dispatch }) { - )} + + ))} {sortedDevices.map((device, rowIndex) => ( - )} + + ))} ))} @@ -224,7 +262,7 @@ export default function DASDTable({ state, dispatch }) { placeholder={_("Filter by min channel")} onChange={onMinChannelFilterChange} /> - {state.minChannel !== "" && + {state.minChannel !== "" && ( - } + + )} @@ -245,7 +284,7 @@ export default function DASDTable({ state, dispatch }) { placeholder={_("Filter by max channel")} onChange={onMaxChannelFilterChange} /> - {state.maxChannel !== "" && + {state.maxChannel !== "" && ( - } + + )} - + diff --git a/web/src/components/storage/DeviceSelection.jsx b/web/src/components/storage/DeviceSelection.jsx index 12e5b28e24..653ef377b7 100644 --- a/web/src/components/storage/DeviceSelection.jsx +++ b/web/src/components/storage/DeviceSelection.jsx @@ -29,12 +29,13 @@ import { Card, CardBody, Flex, - Form, FormGroup, + Form, + FormGroup, PageSection, Radio, - Stack + Stack, } from "@patternfly/react-core"; -import a11y from '@patternfly/react-styles/css/utilities/Accessibility/accessibility'; +import a11y from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; import { _ } from "~/i18n"; import { deviceChildren } from "~/components/storage/utils"; @@ -88,8 +89,8 @@ export default function DeviceSelection() { load: true, availableDevices, target: settings.target, - targetDevice: availableDevices.find(d => d.name === settings.targetDevice), - targetPVDevices: availableDevices.filter(d => settings.targetPVDevices?.includes(d.name)), + targetDevice: availableDevices.find((d) => d.name === settings.targetDevice), + targetPVDevices: availableDevices.filter((d) => settings.targetPVDevices?.includes(d.name)), }); }; @@ -114,7 +115,7 @@ export default function DeviceSelection() { const newSettings = { target: state.target, targetDevice: isTargetDisk ? state.targetDevice?.name : "", - targetPVDevices: isTargetNewLvmVg ? state.targetPVDevices.map(d => d.name) : [] + targetPVDevices: isTargetNewLvmVg ? state.targetPVDevices.map((d) => d.name) : [], }; await client.proposal.calculate({ ...settings, ...newSettings }); @@ -133,15 +134,19 @@ export default function DeviceSelection() { // TRANSLATORS: description for using plain partitions for installing the // system, the text in the square brackets [] is displayed in bold, use only // one pair in the translation - const [msgStart1, msgBold1, msgEnd1] = _("The file systems will be allocated \ -by default as [new partitions in the selected device].").split(/[[\]]/); + const [msgStart1, msgBold1, msgEnd1] = _( + "The file systems will be allocated \ +by default as [new partitions in the selected device].", + ).split(/[[\]]/); // TRANSLATORS: description for using logical volumes for installing the // system, the text in the square brackets [] is displayed in bold, use only // one pair in the translation - const [msgStart2, msgBold2, msgEnd2] = _("The file systems will be allocated \ + const [msgStart2, msgBold2, msgEnd2] = _( + "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.").split(/[[\]]/); +devices.", + ).split(/[[\]]/); return ( <> @@ -224,7 +229,10 @@ devices.").split(/[[\]]/); - + {_("Prepare more devices by configuring advanced")} diff --git a/web/src/components/storage/DeviceSelectorTable.jsx b/web/src/components/storage/DeviceSelectorTable.jsx index 8554e26eb2..bafba38bf8 100644 --- a/web/src/components/storage/DeviceSelectorTable.jsx +++ b/web/src/components/storage/DeviceSelectorTable.jsx @@ -23,7 +23,11 @@ import React from "react"; import { - DeviceName, DeviceDetails, DeviceSize, FilesystemLabel, toStorageDevice + DeviceName, + DeviceDetails, + DeviceSize, + FilesystemLabel, + toStorageDevice, } from "~/components/storage/device-utils"; import { ExpandableSelector } from "~/components/core"; import { Icon } from "~/components/layout"; @@ -73,8 +77,8 @@ const DeviceInfo = ({ item }) => { } else { const technology = device.transport || device.bus; type = technology - // TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" - ? sprintf(_("%s disk"), technology) + ? // TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" + sprintf(_("%s disk"), technology) : _("Disk"); } } @@ -137,8 +141,7 @@ const DeviceInfo = ({ item }) => { const DeviceExtendedDetails = ({ item }) => { const device = toStorageDevice(item); - if (!device || ["partition", "lvmLv"].includes(device.type)) - return ; + if (!device || ["partition", "lvmLv"].includes(device.type)) return ; // TODO: there is a lot of room for improvement here, but first we would need // device.description (comes from YaST) to be way more granular @@ -158,7 +161,11 @@ const DeviceExtendedDetails = ({ item }) => { return _("No content found"); } - return
      {device.description}
      ; + return ( +
      + {device.description} +
      + ); }; const Systems = () => { @@ -167,7 +174,11 @@ const DeviceExtendedDetails = ({ item }) => { const System = ({ system }) => { const logo = /windows/i.test(system) ? "windows_logo" : "linux_logo"; - return
      {system}
      ; + return ( +
      + {system} +
      + ); }; return device.systems.map((s, i) => ); @@ -185,7 +196,7 @@ const DeviceExtendedDetails = ({ item }) => { const columns = [ { name: _("Device"), value: (item) => }, { name: _("Details"), value: (item) => }, - { name: _("Size"), value: (item) => , classNames: "sizes-column" } + { name: _("Size"), value: (item) => , classNames: "sizes-column" }, ]; /** diff --git a/web/src/components/storage/DevicesFormSelect.jsx b/web/src/components/storage/DevicesFormSelect.jsx index baf73b0098..0ffab68e2c 100644 --- a/web/src/components/storage/DevicesFormSelect.jsx +++ b/web/src/components/storage/DevicesFormSelect.jsx @@ -22,8 +22,8 @@ // @ts-check import React from "react"; -import { FormSelect, FormSelectOption } from '@patternfly/react-core'; -import { deviceSize } from '~/components/storage/utils'; +import { FormSelect, FormSelectOption } from "@patternfly/react-core"; +import { deviceSize } from "~/components/storage/utils"; /** * @typedef {import ("@patternfly/react-core").FormSelectProps} PFFormSelectProps @@ -52,9 +52,9 @@ export default function DevicesFormSelect({ devices, selectedDevice, onChange, . onChange(devices.find(d => d.sid === Number(value)))} + onChange={(_, value) => onChange(devices.find((d) => d.sid === Number(value)))} > - { devices.map(device => ( + {devices.map((device) => ( this.#isUsed(d)) - .map(d => d.sid); + const sids = targetSystem + .concat(targetStaging) + .filter((d) => this.#isUsed(d)) + .map((d) => d.sid); - return compact(uniq(sids).map(sid => this.stagingDevice(sid))); + return compact(uniq(sids).map((sid) => this.stagingDevice(sid))); } /** @@ -164,7 +165,7 @@ export default class DevicesManager { * @returns {StorageDevice[]} */ deletedDevices() { - return this.#deleteActionsDevice().filter(d => !d.isDrive); + return this.#deleteActionsDevice().filter((d) => !d.isDrive); } /** @@ -176,7 +177,7 @@ export default class DevicesManager { * @returns {StorageDevice[]} */ resizedDevices() { - return this.#resizeActionsDevice().filter(d => !d.isDrive); + return this.#resizeActionsDevice().filter((d) => !d.isDrive); } /** @@ -187,8 +188,8 @@ export default class DevicesManager { */ deletedSystems() { const systems = this.#deleteActionsDevice() - .filter(d => !d.partitionTable) - .map(d => d.systems) + .filter((d) => !d.partitionTable) + .map((d) => d.systems) .flat(); return compact(systems); } @@ -201,8 +202,8 @@ export default class DevicesManager { */ resizedSystems() { const systems = this.#resizeActionsDevice() - .filter(d => !d.partitionTable) - .map(d => d.systems) + .filter((d) => !d.partitionTable) + .map((d) => d.systems) .flat(); return compact(systems); } @@ -213,7 +214,7 @@ export default class DevicesManager { * @returns {StorageDevice|undefined} */ #device(sid, source) { - return source.find(d => d.sid === sid); + return source.find((d) => d.sid === sid); } /** @@ -230,24 +231,24 @@ export default class DevicesManager { * @returns {boolean} */ #isUsed(device) { - const sids = uniq(compact(this.actions.map(a => a.device))); + const sids = uniq(compact(this.actions.map((a) => a.device))); const partitions = device.partitionTable?.partitions || []; const lvmLvs = device.logicalVolumes || []; - return sids.includes(device.sid) || - partitions.find(p => this.#isUsed(p)) !== undefined || - lvmLvs.find(l => this.#isUsed(l)) !== undefined; + return ( + sids.includes(device.sid) || + partitions.find((p) => this.#isUsed(p)) !== undefined || + lvmLvs.find((l) => this.#isUsed(l)) !== undefined + ); } /** * @returns {StorageDevice[]} */ #deleteActionsDevice() { - const sids = this.actions - .filter(a => a.delete) - .map(a => a.device); - const devices = sids.map(sid => this.systemDevice(sid)); + const sids = this.actions.filter((a) => a.delete).map((a) => a.device); + const devices = sids.map((sid) => this.systemDevice(sid)); return compact(devices); } @@ -255,10 +256,8 @@ export default class DevicesManager { * @returns {StorageDevice[]} */ #resizeActionsDevice() { - const sids = this.actions - .filter(a => a.resize) - .map(a => a.device); - const devices = sids.map(sid => this.systemDevice(sid)); + const sids = this.actions.filter((a) => a.resize).map((a) => a.device); + const devices = sids.map((sid) => this.systemDevice(sid)); return compact(devices); } } diff --git a/web/src/components/storage/DevicesManager.test.js b/web/src/components/storage/DevicesManager.test.js index cab7d9dad2..4edc74fc89 100644 --- a/web/src/components/storage/DevicesManager.test.js +++ b/web/src/components/storage/DevicesManager.test.js @@ -305,7 +305,7 @@ describe("usedDevices", () => { { sid: 63, isDrive: true, partitionTable: { partitions: [] } }, { sid: 64, isDrive: false, type: "lvmVg", logicalVolumes: [] }, { sid: 65, isDrive: false, type: "lvmVg", logicalVolumes: [] }, - { sid: 66, isDrive: false, type: "lvmVg", logicalVolumes: [{ sid: 68 }] } + { sid: 66, isDrive: false, type: "lvmVg", logicalVolumes: [{ sid: 68 }] }, ]; staging = [ { sid: 60, isDrive: false }, @@ -317,7 +317,7 @@ describe("usedDevices", () => { // Logical volume added { sid: 65, isDrive: false, type: "lvmVg", logicalVolumes: [{ sid: 70 }, { sid: 71 }] }, // Logical volume removed - { sid: 66, isDrive: false, type: "lvmVg", logicalVolumes: [] } + { sid: 66, isDrive: false, type: "lvmVg", logicalVolumes: [] }, ]; }); @@ -348,20 +348,24 @@ describe("usedDevices", () => { // This logical volume was added (belongs to device 65). { device: 70 }, // This logical volume was added (belongs to device 65). - { device: 71 } + { device: 71 }, ]; }); it("does not include removed disk devices or LVM volume groups", () => { const manager = new DevicesManager(system, staging, actions); - const sids = manager.usedDevices().map(d => d.sid) + const sids = manager + .usedDevices() + .map((d) => d.sid) .sort(); expect(sids).not.toContain(61); }); it("includes all disk devices and LVM volume groups affected by the actions", () => { const manager = new DevicesManager(system, staging, actions); - const sids = manager.usedDevices().map(d => d.sid) + const sids = manager + .usedDevices() + .map((d) => d.sid) .sort(); expect(sids).toEqual([62, 63, 65, 66]); }); @@ -370,26 +374,22 @@ describe("usedDevices", () => { describe("resizedDevices", () => { beforeEach(() => { - system = [ - { sid: 60 }, - { sid: 62 }, - { sid: 63 }, - { sid: 64 }, - { sid: 65, isDrive: true } - ]; + system = [{ sid: 60 }, { sid: 62 }, { sid: 63 }, { sid: 64 }, { sid: 65, isDrive: true }]; actions = [ { device: 60, delete: true }, // This device does not exist in system. { device: 61, delete: true }, { device: 62, delete: false, resize: true }, { device: 63, delete: false, resize: true }, - { device: 65, delete: true } + { device: 65, delete: true }, ]; }); it("includes all resized devices", () => { const manager = new DevicesManager(system, staging, actions); - const sids = manager.resizedDevices().map(d => d.sid) + const sids = manager + .resizedDevices() + .map((d) => d.sid) .sort(); expect(sids).toEqual([62, 63]); }); @@ -404,12 +404,12 @@ describe("resizedSystems", () => { sid: 63, systems: ["openSUSE Leap", "openSUSE Tumbleweed"], partitionTable: { - partitions: [{ sid: 65 }, { sid: 66 }] - } + partitions: [{ sid: 65 }, { sid: 66 }], + }, }, { sid: 64 }, { sid: 65, systems: ["openSUSE Leap"] }, - { sid: 66, systems: ["openSUSE Tumbleweed"] } + { sid: 66, systems: ["openSUSE Tumbleweed"] }, ]; actions = [ { device: 60, delete: false, resize: true }, @@ -418,7 +418,7 @@ describe("resizedSystems", () => { { device: 62, delete: false }, { device: 63, delete: false, resize: true }, { device: 65, delete: true, resize: true }, - { device: 66, delete: false, resize: true } + { device: 66, delete: false, resize: true }, ]; }); @@ -434,26 +434,22 @@ describe("resizedSystems", () => { describe("deletedDevices", () => { beforeEach(() => { - system = [ - { sid: 60 }, - { sid: 62 }, - { sid: 63 }, - { sid: 64 }, - { sid: 65, isDrive: true } - ]; + system = [{ sid: 60 }, { sid: 62 }, { sid: 63 }, { sid: 64 }, { sid: 65, isDrive: true }]; actions = [ { device: 60, delete: true }, // This device does not exist in system. { device: 61, delete: true }, { device: 62, delete: false }, { device: 63, delete: true }, - { device: 65, delete: true } + { device: 65, delete: true }, ]; }); it("includes all deleted devices", () => { const manager = new DevicesManager(system, staging, actions); - const sids = manager.deletedDevices().map(d => d.sid) + const sids = manager + .deletedDevices() + .map((d) => d.sid) .sort(); expect(sids).toEqual([60, 63]); }); @@ -468,12 +464,12 @@ describe("deletedSystems", () => { sid: 63, systems: ["openSUSE Leap", "openSUSE Tumbleweed"], partitionTable: { - partitions: [{ sid: 65 }, { sid: 66 }] - } + partitions: [{ sid: 65 }, { sid: 66 }], + }, }, { sid: 64 }, { sid: 65, systems: ["openSUSE Leap"] }, - { sid: 66, systems: ["openSUSE Tumbleweed"] } + { sid: 66, systems: ["openSUSE Tumbleweed"] }, ]; actions = [ { device: 60, delete: true }, @@ -482,7 +478,7 @@ describe("deletedSystems", () => { { device: 62, delete: false }, { device: 63, delete: true }, { device: 65, delete: true }, - { device: 66, delete: true } + { device: 66, delete: true }, ]; }); diff --git a/web/src/components/storage/DevicesTechMenu.jsx b/web/src/components/storage/DevicesTechMenu.jsx index ffdb42aea7..dc0a47a659 100644 --- a/web/src/components/storage/DevicesTechMenu.jsx +++ b/web/src/components/storage/DevicesTechMenu.jsx @@ -23,10 +23,7 @@ import React, { useEffect, useState } from "react"; import { useHref } from "react-router-dom"; -import { - MenuToggle, - Select, SelectList, SelectOption -} from "@patternfly/react-core"; +import { MenuToggle, Select, SelectList, SelectOption } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { useInstallerClient } from "~/context/installer"; @@ -38,11 +35,7 @@ const DASDLink = () => { const href = useHref("/storage/dasd"); return ( - + DASD ); @@ -56,11 +49,7 @@ const ZFCPLink = () => { const href = useHref("/storage/zfcp"); return ( - + {_("zFCP")} ); @@ -74,11 +63,7 @@ const ISCSILink = () => { const href = useHref("/storage/iscsi"); return ( - + {_("iSCSI")} ); @@ -104,7 +89,7 @@ export default function DevicesTechMenu({ label }) { client.zfcp.isSupported().then(setShowZFCPLink); }, [client.dasd, client.zfcp]); - const toggle = toggleRef => ( + const toggle = (toggleRef) => ( setIsOpen(!isOpen)} isExpanded={isOpen}> {label} diff --git a/web/src/components/storage/DevicesTechMenu.test.jsx b/web/src/components/storage/DevicesTechMenu.test.jsx index ec7e7fb34c..6cd7917a1b 100644 --- a/web/src/components/storage/DevicesTechMenu.test.jsx +++ b/web/src/components/storage/DevicesTechMenu.test.jsx @@ -30,13 +30,13 @@ jest.mock("~/client"); const isDASDSupportedFn = jest.fn(); const dasd = { - isSupported: isDASDSupportedFn + isSupported: isDASDSupportedFn, }; const isZFCPSupportedFn = jest.fn(); const zfcp = { - isSupported: isZFCPSupportedFn + isSupported: isZFCPSupportedFn, }; beforeEach(() => { @@ -45,7 +45,7 @@ beforeEach(() => { createClient.mockImplementation(() => { return { - storage: { dasd, zfcp } + storage: { dasd, zfcp }, }; }); }); diff --git a/web/src/components/storage/EncryptionField.jsx b/web/src/components/storage/EncryptionField.jsx index 93a5fab08b..017ea2dc95 100644 --- a/web/src/components/storage/EncryptionField.jsx +++ b/web/src/components/storage/EncryptionField.jsx @@ -36,12 +36,14 @@ import { noop } from "~/utils"; // Field texts at root level to avoid redefinitions every time the component // is rendered. const LABEL = _("Encryption"); -const DESCRIPTION = _("Protection for the information stored at \ -the device, including data, programs, and system files."); +const DESCRIPTION = _( + "Protection for the information stored at \ +the device, including data, programs, and system files.", +); const VALUES = { disabled: _("disabled"), [EncryptionMethods.LUKS2]: _("enabled"), - [EncryptionMethods.TPM]: _("using TPM unlocking") + [EncryptionMethods.TPM]: _("using TPM unlocking"), }; const Value = ({ isLoading, isEnabled, method }) => { @@ -87,7 +89,7 @@ export default function EncryptionField({ // FIXME: should be available methods actually a prop? methods = [], isLoading = false, - onChange = noop + onChange = noop, }) { const validPassword = useCallback(() => password?.length > 0, [password]); const [isEnabled, setIsEnabled] = useState(validPassword()); @@ -117,7 +119,7 @@ export default function EncryptionField({ cardDescriptionProps={{ isFilled: true }} actions={} > - {isDialogOpen && + {isDialogOpen && ( } + /> + )} ); } diff --git a/web/src/components/storage/EncryptionSettingsDialog.jsx b/web/src/components/storage/EncryptionSettingsDialog.jsx index da5740124b..0801c45298 100644 --- a/web/src/components/storage/EncryptionSettingsDialog.jsx +++ b/web/src/components/storage/EncryptionSettingsDialog.jsx @@ -34,15 +34,19 @@ import { EncryptionMethods } from "~/client/storage"; */ const DIALOG_TITLE = _("Encryption"); -const DIALOG_DESCRIPTION = _("Full Disk Encryption (FDE) allows to protect the information stored \ -at the device, including data, programs, and system files."); +const DIALOG_DESCRIPTION = _( + "Full Disk Encryption (FDE) allows to protect the information stored \ +at the device, including data, programs, and system files.", +); // TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation const TPM_LABEL = _("Use the Trusted Platform Module (TPM) to decrypt automatically on each boot"); // TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing // 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. -const TPM_EXPLANATION = _("The password will not be needed to boot and access the data if the \ +const TPM_EXPLANATION = _( + "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."); +directly on its first run.", +); /** * Renders a dialog that allows the user change encryption settings @@ -66,7 +70,7 @@ export default function EncryptionSettingsDialog({ isOpen = false, isLoading = false, onCancel, - onAccept + onAccept, }) { const [isEnabled, setIsEnabled] = useState(passwordProp?.length > 0); const [password, setPassword] = useState(passwordProp); @@ -77,11 +81,15 @@ export default function EncryptionSettingsDialog({ const formId = "encryptionSettingsForm"; // reset the settings only after loading is finished - if (isLoading && !wasLoading) { setWasLoading(true) } + if (isLoading && !wasLoading) { + setWasLoading(true); + } if (!isLoading && wasLoading) { setWasLoading(false); // refresh the state when the real values are loaded - if (method !== methodProp) { setMethod(methodProp) } + if (method !== methodProp) { + setMethod(methodProp); + } if (password !== passwordProp) { setPassword(passwordProp); setIsEnabled(passwordProp?.length > 0); @@ -93,7 +101,8 @@ export default function EncryptionSettingsDialog({ }, [isEnabled, password, passwordsMatch]); const changePassword = (_, v) => setPassword(v); - const changeMethod = (_, useTPM) => setMethod(useTPM ? EncryptionMethods.TPM : EncryptionMethods.LUKS2); + const changeMethod = (_, useTPM) => + setMethod(useTPM ? EncryptionMethods.TPM : EncryptionMethods.LUKS2); const submitSettings = (e) => { e.preventDefault(); @@ -108,7 +117,12 @@ export default function EncryptionSettingsDialog({ const tpmAvailable = methods.includes(EncryptionMethods.TPM); return ( - + - {tpmAvailable && + {tpmAvailable && ( } + /> + )} diff --git a/web/src/components/storage/EncryptionSettingsDialog.test.jsx b/web/src/components/storage/EncryptionSettingsDialog.test.jsx index 512dbe042c..bfaf0814a9 100644 --- a/web/src/components/storage/EncryptionSettingsDialog.test.jsx +++ b/web/src/components/storage/EncryptionSettingsDialog.test.jsx @@ -40,7 +40,7 @@ describe.skip("EncryptionSettingsDialog", () => { methods: Object.values(EncryptionMethods), isOpen: true, onCancel: onCancelFn, - onAccept: onAcceptFn + onAccept: onAcceptFn, }; }); @@ -73,9 +73,7 @@ describe.skip("EncryptionSettingsDialog", () => { await user.type(confirmationInput, "2345"); await user.click(acceptButton); - expect(props.onAccept).toHaveBeenCalledWith( - expect.objectContaining({ password: "2345" }) - ); + expect(props.onAccept).toHaveBeenCalledWith(expect.objectContaining({ password: "2345" })); }); }); @@ -94,9 +92,10 @@ describe.skip("EncryptionSettingsDialog", () => { await user.click(tpmCheckbox); await user.click(acceptButton); - expect(props.onAccept).toHaveBeenCalledWith( - { password: "9876", method: EncryptionMethods.TPM } - ); + expect(props.onAccept).toHaveBeenCalledWith({ + password: "9876", + method: EncryptionMethods.TPM, + }); }); it("allows unsetting the encryption", async () => { @@ -125,7 +124,7 @@ describe.skip("EncryptionSettingsDialog", () => { expect(tpmCheckbox).not.toBeChecked(); await user.click(acceptButton); expect(props.onAccept).toHaveBeenCalledWith( - expect.not.objectContaining({ method: EncryptionMethods.TPM }) + expect.not.objectContaining({ method: EncryptionMethods.TPM }), ); }); }); diff --git a/web/src/components/storage/InstallationDeviceField.jsx b/web/src/components/storage/InstallationDeviceField.jsx index 002decc3cd..3a817fb9a5 100644 --- a/web/src/components/storage/InstallationDeviceField.jsx +++ b/web/src/components/storage/InstallationDeviceField.jsx @@ -24,7 +24,7 @@ import React from "react"; import { Skeleton } from "@patternfly/react-core"; import { ButtonLink, CardField } from "~/components/core"; -import { deviceLabel } from '~/components/storage/utils'; +import { deviceLabel } from "~/components/storage/utils"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; @@ -56,7 +56,10 @@ const targetValue = (target, targetDevice, targetPVDevices) => { if (targetPVDevices.length === 1) { // TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) - return sprintf(_("File systems created at a new LVM volume group on %s"), deviceLabel(targetPVDevices[0])); + return sprintf( + _("File systems created at a new LVM volume group on %s"), + deviceLabel(targetPVDevices[0]), + ); } } @@ -90,19 +93,21 @@ export default function InstallationDeviceField({ isLoading, }) { let value; - if (isLoading || !target) - value = ; - else - value = targetValue(target, targetDevice, targetPVDevices); + if (isLoading || !target) value = ; + else value = targetValue(target, targetDevice, targetPVDevices); return ( - : {_("Change")} + isLoading ? ( + + ) : ( + + {_("Change")} + + ) } > {value} diff --git a/web/src/components/storage/InstallationDeviceField.test.jsx b/web/src/components/storage/InstallationDeviceField.test.jsx index a402188b3b..b2ab5cd248 100644 --- a/web/src/components/storage/InstallationDeviceField.test.jsx +++ b/web/src/components/storage/InstallationDeviceField.test.jsx @@ -31,7 +31,7 @@ jest.mock("@patternfly/react-core", () => { return { ...original, - Skeleton: () =>
      PF-Skeleton
      + Skeleton: () =>
      PF-Skeleton
      , }; }); @@ -58,7 +58,7 @@ const sda = { name: "/dev/sda", size: 1024, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }; @@ -81,9 +81,9 @@ const sdb = { name: "/dev/sdb", size: 2048, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }; /** @type {InstallationDeviceFieldProps} */ @@ -96,7 +96,7 @@ beforeEach(() => { targetPVDevices: [], devices: [sda, sdb], isLoading: false, - onChange: jest.fn() + onChange: jest.fn(), }; }); @@ -199,7 +199,7 @@ it.skip("allows changing the selected device", async () => { expect(props.onChange).toHaveBeenCalledWith({ target: "DISK", targetDevice: sdb, - targetPVDevices: [] + targetPVDevices: [], }); }); diff --git a/web/src/components/storage/PartitionsField.jsx b/web/src/components/storage/PartitionsField.jsx index 96394f77f7..88faf9d78c 100644 --- a/web/src/components/storage/PartitionsField.jsx +++ b/web/src/components/storage/PartitionsField.jsx @@ -24,28 +24,36 @@ import React, { useState } from "react"; import { Button, - CardBody, CardExpandableContent, + CardBody, + CardExpandableContent, Divider, - Dropdown, DropdownList, DropdownItem, + Dropdown, + DropdownList, + DropdownItem, Flex, - List, ListItem, + List, + ListItem, MenuToggle, Skeleton, Split, - Stack -} from '@patternfly/react-core'; -import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; -import { CardField, RowActions, Tip } from '~/components/core'; + Stack, +} from "@patternfly/react-core"; +import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; +import { CardField, RowActions, Tip } from "~/components/core"; import { noop } from "~/utils"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { - deviceSize, hasSnapshots, isTransactionalRoot, isTransactionalSystem, reuseDevice -} from '~/components/storage/utils'; + deviceSize, + hasSnapshots, + isTransactionalRoot, + isTransactionalSystem, + reuseDevice, +} from "~/components/storage/utils"; import BootConfigField from "~/components/storage/BootConfigField"; import SnapshotsField from "~/components/storage/SnapshotsField"; -import VolumeDialog from '~/components/storage/VolumeDialog'; -import VolumeLocationDialog from '~/components/storage/VolumeLocationDialog'; +import VolumeDialog from "~/components/storage/VolumeDialog"; +import VolumeLocationDialog from "~/components/storage/VolumeLocationDialog"; /** * @typedef {import ("~/client/storage").ProposalTarget} ProposalTarget @@ -62,11 +70,14 @@ import VolumeLocationDialog from '~/components/storage/VolumeLocationDialog'; */ const SizeText = ({ volume }) => { let targetSize; - if (reuseDevice(volume)) - targetSize = volume.targetDevice.size; + if (reuseDevice(volume)) targetSize = volume.targetDevice.size; const minSize = deviceSize(targetSize || volume.minSize); - const maxSize = targetSize ? deviceSize(targetSize) : volume.maxSize ? deviceSize(volume.maxSize) : undefined; + const maxSize = targetSize + ? deviceSize(targetSize) + : volume.maxSize + ? deviceSize(volume.maxSize) + : undefined; if (minSize && maxSize && minSize !== maxSize) return `${minSize} - ${maxSize}`; // TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" @@ -86,24 +97,24 @@ const BasicVolumeText = ({ volume, target }) => { const snapshots = hasSnapshots(volume); const transactional = isTransactionalRoot(volume); const size = SizeText({ volume }); - const lvm = (target === "NEW_LVM_VG"); + const lvm = target === "NEW_LVM_VG"; // When target is "filesystem" or "device" this is irrelevant since the type of device // is not mentioned const lv = volume.target === "NEW_VG" || (volume.target === "DEFAULT" && lvm); if (transactional) - return (lv) - // TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" - ? sprintf(_("Transactional Btrfs root volume (%s)"), size) - // TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" - : sprintf(_("Transactional Btrfs root partition (%s)"), size); + return lv + ? // TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" + sprintf(_("Transactional Btrfs root volume (%s)"), size) + : // TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" + sprintf(_("Transactional Btrfs root partition (%s)"), size); if (snapshots) - return (lv) - // TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" - ? sprintf(_("Btrfs root volume with snapshots (%s)"), size) - // TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" - : sprintf(_("Btrfs root partition with snapshots (%s)"), size); + return lv + ? // TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" + sprintf(_("Btrfs root volume with snapshots (%s)"), size) + : // TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" + sprintf(_("Btrfs root partition with snapshots (%s)"), size); const volTarget = volume.target; const mount = volume.mountPath; @@ -120,11 +131,11 @@ const BasicVolumeText = ({ volume, target }) => { // %1$s is replaced by the device name, and %2$s by the size return sprintf(_("Swap at %1$s (%2$s)"), device, size); - return (lv) - // TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" - ? sprintf(_("Swap volume (%s)"), size) - // TRANSLATORS: %s replaced by size string, e.g. "8 GiB" - : sprintf(_("Swap partition (%s)"), size); + return lv + ? // TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" + sprintf(_("Swap volume (%s)"), size) + : // TRANSLATORS: %s replaced by size string, e.g. "8 GiB" + sprintf(_("Swap partition (%s)"), size); } const type = volume.fsType; @@ -135,14 +146,14 @@ const BasicVolumeText = ({ volume, target }) => { // %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size return sprintf(_("%1$s root at %2$s (%3$s)"), type, device, size); - return (lv) - // TRANSLATORS: "/" is in an LVM logical volume. - // Results in something like "Btrfs root volume (at least 20 GiB)" since - // $1$s is replaced by filesystem type and %2$s by size description - ? sprintf(_("%1$s root volume (%2$s)"), type, size) - // TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since - // $1$s is replaced by filesystem type and %2$s by size description - : sprintf(_("%1$s root partition (%2$s)"), type, size); + return lv + ? // TRANSLATORS: "/" is in an LVM logical volume. + // Results in something like "Btrfs root volume (at least 20 GiB)" since + // $1$s is replaced by filesystem type and %2$s by size description + sprintf(_("%1$s root volume (%2$s)"), type, size) + : // TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since + // $1$s is replaced by filesystem type and %2$s by size description + sprintf(_("%1$s root partition (%2$s)"), type, size); } if (volTarget === "DEVICE") @@ -150,14 +161,14 @@ const BasicVolumeText = ({ volume, target }) => { // %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size return sprintf(_("%1$s %2$s at %3$s (%4$s)"), type, mount, device, size); - return (lv) - // TRANSLATORS: The filesystem is in an LVM logical volume. - // Results in something like "Ext4 /home volume (at least 10 GiB)" since - // %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description - ? sprintf(_("%1$s %2$s volume (%3$s)"), type, mount, size) - // TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since - // %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description - : sprintf(_("%1$s %2$s partition (%3$s)"), type, mount, size); + return lv + ? // TRANSLATORS: The filesystem is in an LVM logical volume. + // Results in something like "Ext4 /home volume (at least 10 GiB)" since + // %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description + sprintf(_("%1$s %2$s volume (%3$s)"), type, mount, size) + : // TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since + // %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description + sprintf(_("%1$s %2$s partition (%3$s)"), type, mount, size); }; /** @@ -168,11 +179,9 @@ const BasicVolumeText = ({ volume, target }) => { * @param {StorageDevice} props.device */ const BootLabelText = ({ configure, device }) => { - if (!configure) - return _("Do not configure partitions for booting"); + if (!configure) return _("Do not configure partitions for booting"); - if (!device) - return _("Boot partitions at installation disk"); + if (!device) return _("Boot partitions at installation disk"); // TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) return sprintf(_("Boot partitions at %s"), device.name); @@ -199,17 +208,22 @@ const AutoCalculatedHint = ({ volume }) => { {/* TRANSLATORS: header for a list of items referring to size limits for file systems */} {_("These limits are affected by:")} - {snapshotsAffectSizes && + {snapshotsAffectSizes && ( // TRANSLATORS: list item, this affects the computed partition size limits - {_("The configuration of snapshots")}} - {sizeRelevantVolumes.length > 0 && + {_("The configuration of snapshots")} + )} + {sizeRelevantVolumes.length > 0 && ( // TRANSLATORS: list item, this affects the computed partition size limits // %s is replaced by a list of the volumes (like "/home, /boot") - {sprintf(_("Presence of other volumes (%s)"), sizeRelevantVolumes.join(", "))}} - {adjustByRam && + + {sprintf(_("Presence of other volumes (%s)"), sizeRelevantVolumes.join(", "))} + + )} + {adjustByRam && ( // TRANSLATORS: list item, describes a factor that affects the computed size of a // file system; eg. adjusting the size of the swap - {_("The amount of RAM in the system")}} + {_("The amount of RAM in the system")} + )} ); @@ -224,7 +238,14 @@ const AutoCalculatedHint = ({ volume }) => { */ const VolumeLabel = ({ volume, target }) => { return ( - + {BasicVolumeText({ volume, target })} ); @@ -239,7 +260,14 @@ const VolumeLabel = ({ volume, target }) => { */ const BootLabel = ({ bootDevice, configureBoot }) => { return ( - + {BootLabelText({ configure: configureBoot, device: bootDevice })} ); @@ -249,10 +277,10 @@ const BootLabel = ({ bootDevice, configureBoot }) => { // components to a new file. /** - * @component - * @param {object} props - * @param {Volume} props.volume - */ + * @component + * @param {object} props + * @param {Volume} props.volume + */ const VolumeSizeLimits = ({ volume }) => { const isAuto = volume.autoSize; @@ -260,16 +288,18 @@ const VolumeSizeLimits = ({ volume }) => { {SizeText({ volume })} {/* TRANSLATORS: device flag, the partition size is automatically computed */} - {isAuto && !reuseDevice(volume) && {_("auto")}} + {isAuto && !reuseDevice(volume) && ( + {_("auto")} + )} ); }; /** - * @component - * @param {object} props - * @param {Volume} props.volume - */ + * @component + * @param {object} props + * @param {Volume} props.volume + */ const VolumeDetails = ({ volume }) => { const snapshots = hasSnapshots(volume); const transactional = isTransactionalRoot(volume); @@ -277,20 +307,18 @@ const VolumeDetails = ({ volume }) => { if (volume.target === "FILESYSTEM") // TRANSLATORS: %s will be replaced by a file-system type like "Btrfs" or "Ext4" return sprintf(_("Reused %s"), volume.targetDevice?.filesystem?.type || ""); - if (transactional) - return _("Transactional Btrfs"); - if (snapshots) - return _("Btrfs with snapshots"); + if (transactional) return _("Transactional Btrfs"); + if (snapshots) return _("Btrfs with snapshots"); return volume.fsType; }; /** - * @component - * @param {object} props - * @param {Volume} props.volume - * @param {ProposalTarget} props.target - */ + * @component + * @param {object} props + * @param {Volume} props.volume + * @param {ProposalTarget} props.target + */ const VolumeLocation = ({ volume, target }) => { if (volume.target === "NEW_PARTITION") // TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") @@ -300,27 +328,26 @@ const VolumeLocation = ({ volume, target }) => { return sprintf(_("Separate LVM at %s"), volume.targetDevice?.name || ""); if (volume.target === "DEVICE" || volume.target === "FILESYSTEM") return volume.targetDevice?.name || ""; - if (target === "NEW_LVM_VG") - return _("Logical volume at system LVM"); + if (target === "NEW_LVM_VG") return _("Logical volume at system LVM"); return _("Partition at installation disk"); }; /** - * @component - * @param {object} props - * @param {Volume} props.volume - * @param {() => void} props.onEdit - * @param {() => void} props.onResetLocation - * @param {() => void} props.onLocation - * @param {() => void} props.onDelete - */ + * @component + * @param {object} props + * @param {Volume} props.volume + * @param {() => void} props.onEdit + * @param {() => void} props.onResetLocation + * @param {() => void} props.onLocation + * @param {() => void} props.onDelete + */ const VolumeActions = ({ volume, onEdit, onResetLocation, onLocation, onDelete }) => { const actions = [ { title: _("Edit"), onClick: onEdit }, volume.target !== "DEFAULT" && { title: _("Reset location"), onClick: onResetLocation }, { title: _("Change location"), onClick: onLocation }, - !volume.outline.required && { title: _("Delete"), onClick: onDelete, isDanger: true } + !volume.outline.required && { title: _("Delete"), onClick: onDelete, isDanger: true }, ]; return ; @@ -352,7 +379,7 @@ const VolumeRow = ({ targetDevices, isLoading, onEdit = noop, - onDelete = noop + onDelete = noop, }) => { /** @type {[string, (dialog: string) => void]} */ const [dialog, setDialog] = useState(); @@ -378,7 +405,9 @@ const VolumeRow = ({ if (isLoading) { return (
      - + ); } @@ -387,9 +416,15 @@ const VolumeRow = ({ <> - - - + + + - {isEditDialogOpen && + {isEditDialogOpen && ( } - {isLocationDialogOpen && + /> + )} + {isLocationDialogOpen && ( } + /> + )} ); }; @@ -443,7 +480,7 @@ const VolumesTable = ({ target, targetDevices, isLoading, - onVolumesChange + onVolumesChange, }) => { const columns = { mountPath: _("Mount point"), @@ -451,12 +488,12 @@ const VolumesTable = ({ size: _("Size"), // TRANSLATORS: where (and how) the file-system is going to be created location: _("Location"), - actions: _("Actions") + actions: _("Actions"), }; /** @type {(volume: Volume) => void} */ const editVolume = (volume) => { - const index = volumes.findIndex(v => v.mountPath === volume.mountPath); + const index = volumes.findIndex((v) => v.mountPath === volume.mountPath); const newVolumes = [...volumes]; newVolumes[index] = volume; onVolumesChange(newVolumes); @@ -464,7 +501,7 @@ const VolumesTable = ({ /** @type {(volume: Volume) => void} */ const deleteVolume = (volume) => { - const newVolumes = volumes.filter(v => v.mountPath !== volume.mountPath); + const newVolumes = volumes.filter((v) => v.mountPath !== volume.mountPath); onVolumesChange(newVolumes); }; @@ -502,9 +539,7 @@ const VolumesTable = ({ - - {renderVolumes()} - + {renderVolumes()}
      selectAll(isSelecting), isSelected: filteredDevices.length === state.selectedDevices.length }} /> - {columns.map((column, index) => {column.label} selectAll(isSelecting), + isSelected: filteredDevices.length === state.selectedDevices.length, + }} + /> + {columns.map((column, index) => ( + + {column.label} +
      selectDevice(device, isSelecting), isSelected: selectedDevicesIds.includes(device.id), isDisabled: false }} /> - {columns.map(column => {columnData(device, column)} selectDevice(device, isSelecting), + isSelected: selectedDevicesIds.includes(device.id), + isDisabled: false, + }} + /> + {columns.map((column) => ( + + {columnData(device, column)} +
      + +
      {volume.mountPath} + + + + + +
      ); }; @@ -532,7 +567,9 @@ const Basic = ({ volumes, configureBoot, bootDevice, target, isLoading }) => { return ( - {volumes.map((v, i) => )} + {volumes.map((v, i) => ( + + ))} ); @@ -563,7 +600,9 @@ const AddVolumeButton = ({ options, onClick }) => { // Shows a button if the only option is to add an arbitrary volume. if (options.length === 1 && options[0] === "") { return ( - + ); } @@ -574,7 +613,7 @@ const AddVolumeButton = ({ options, onClick }) => { isOpen={isOpen} onSelect={onSelect} onOpenChange={setIsOpen} - toggle={toggleRef => ( + toggle={(toggleRef) => ( { return ( - {_("Other")} + + {_("Other")} + ); } else { return ( - {option} + + {option} + ); } })} @@ -637,7 +680,7 @@ const Advanced = ({ defaultBootDevice, onVolumesChange, onBootChange, - isLoading + isLoading, }) => { const [isVolumeDialogOpen, setIsVolumeDialogOpen] = useState(false); /** @type {[Volume|undefined, (volume: Volume) => void]} */ @@ -651,7 +694,7 @@ const Advanced = ({ const onAcceptVolumeDialog = (volume) => { closeVolumeDialog(); - const index = volumes.findIndex(v => v.mountPath === volume.mountPath); + const index = volumes.findIndex((v) => v.mountPath === volume.mountPath); if (index !== -1) { const newVolumes = [...volumes]; @@ -666,7 +709,7 @@ const Advanced = ({ /** @type {(mountPath: string) => void} */ const addVolume = (mountPath) => { - const template = templates.find(t => t.mountPath === mountPath); + const template = templates.find((t) => t.mountPath === mountPath); setTemplate(template); openVolumeDialog(); }; @@ -676,13 +719,13 @@ const Advanced = ({ * @type {() => string[]} */ const mountPathOptions = () => { - const mountPaths = volumes.map(v => v.mountPath); + const mountPaths = volumes.map((v) => v.mountPath); const isTransactional = isTransactionalSystem(templates); return templates - .map(t => t.mountPath) - .filter(p => !mountPaths.includes(p)) - .filter(p => !isTransactional || p.length); + .map((t) => t.mountPath) + .filter((p) => !mountPaths.includes(p)) + .filter((p) => !isTransactional || p.length); }; /** @@ -691,14 +734,14 @@ const Advanced = ({ */ const showAddVolume = () => { const hasOptionalVolumes = () => { - return templates.find(t => t.mountPath.length && !t.outline.required) !== undefined; + return templates.find((t) => t.mountPath.length && !t.outline.required) !== undefined; }; return !isTransactionalSystem(templates) || hasOptionalVolumes(); }; /** @type {Volume} */ - const rootVolume = volumes.find(v => v.mountPath === "/"); + const rootVolume = volumes.find((v) => v.mountPath === "/"); /** @type {(config: SnapshotsConfig) => void} */ const changeBtrfsSnapshots = ({ active }) => { @@ -716,7 +759,9 @@ const Advanced = ({ return ( - {showSnapshotsField && } + {showSnapshotsField && ( + + )} {showAddVolume() && } - + - {isVolumeDialogOpen && + {isVolumeDialogOpen && ( } + /> + )} setIsExpanded(!isExpanded); @@ -799,19 +847,21 @@ export default function PartitionsField({ return ( - {!isExpanded && + {!isExpanded && ( - } + + )} { return { ...original, - Skeleton: () =>
      PFSkeleton
      + Skeleton: () =>
      PFSkeleton
      , }; }); @@ -59,8 +59,8 @@ const rootVolume = { snapshotsAffectSizes: true, sizeRelevantVolumes: [], adjustByRam: false, - productDefined: true - } + productDefined: true, + }, }; /** @type {Volume} */ @@ -81,8 +81,8 @@ const swapVolume = { snapshotsAffectSizes: false, sizeRelevantVolumes: [], adjustByRam: false, - productDefined: true - } + productDefined: true, + }, }; /** @type {Volume} */ @@ -102,8 +102,8 @@ const homeVolume = { snapshotsAffectSizes: false, sizeRelevantVolumes: [], adjustByRam: false, - productDefined: true - } + productDefined: true, + }, }; /** @type {Volume} */ @@ -124,8 +124,8 @@ const arbitraryVolume = { snapshotsAffectSizes: false, adjustByRam: false, sizeRelevantVolumes: [], - productDefined: false - } + productDefined: false, + }, }; /** @type {StorageDevice} */ @@ -138,7 +138,7 @@ const sda = { vendor: "Micron", model: "Micron 1100 SATA", transport: "usb", - size: 1024 + size: 1024, }; /** @type {StorageDevice} */ @@ -151,8 +151,8 @@ const sda1 = { size: 256, filesystem: { sid: 169, - type: "Swap" - } + type: "Swap", + }, }; /** @type {StorageDevice} */ @@ -165,8 +165,8 @@ const sda2 = { size: 512, filesystem: { sid: 179, - type: "Ext4" - } + type: "Ext4", + }, }; /** @type {PartitionsFieldProps} */ @@ -191,7 +191,7 @@ beforeEach(() => { bootDevice: undefined, defaultBootDevice: undefined, onVolumesChange: jest.fn(), - onBootChange: jest.fn() + onBootChange: jest.fn(), }; }); @@ -304,7 +304,9 @@ describe.skip("if there are volumes", () => { expect(within(body).queryAllByRole("row").length).toEqual(3); within(body).getByRole("row", { name: "/ Btrfs 1 KiB - 2 KiB Partition at installation disk" }); - within(body).getByRole("row", { name: "/home XFS at least 1 KiB Partition at installation disk" }); + within(body).getByRole("row", { + name: "/home XFS at least 1 KiB Partition at installation disk", + }); within(body).getByRole("row", { name: "swap Swap 1 KiB Partition at installation disk" }); }); @@ -312,7 +314,9 @@ describe.skip("if there are volumes", () => { const { user } = await expandField(); const [, body] = await screen.findAllByRole("rowgroup"); - const row = within(body).getByRole("row", { name: "/home XFS at least 1 KiB Partition at installation disk" }); + const row = within(body).getByRole("row", { + name: "/home XFS at least 1 KiB Partition at installation disk", + }); const actions = within(row).getByRole("button", { name: "Actions" }); await user.click(actions); const deleteAction = within(row).queryByRole("menuitem", { name: "Delete" }); @@ -325,7 +329,9 @@ describe.skip("if there are volumes", () => { const { user } = await expandField(); const [, body] = await screen.findAllByRole("rowgroup"); - const row = within(body).getByRole("row", { name: "/home XFS at least 1 KiB Partition at installation disk" }); + const row = within(body).getByRole("row", { + name: "/home XFS at least 1 KiB Partition at installation disk", + }); const actions = within(row).getByRole("button", { name: "Actions" }); await user.click(actions); const editAction = within(row).queryByRole("menuitem", { name: "Edit" }); @@ -339,7 +345,9 @@ describe.skip("if there are volumes", () => { const { user } = await expandField(); const [, body] = await screen.findAllByRole("rowgroup"); - const row = within(body).getByRole("row", { name: "/home XFS at least 1 KiB Partition at installation disk" }); + const row = within(body).getByRole("row", { + name: "/home XFS at least 1 KiB Partition at installation disk", + }); const actions = within(row).getByRole("button", { name: "Actions" }); await user.click(actions); const locationAction = within(row).queryByRole("menuitem", { name: "Change location" }); @@ -354,7 +362,9 @@ describe.skip("if there are volumes", () => { const { user } = await expandField(); const [, body] = await screen.findAllByRole("rowgroup"); - const row = within(body).getByRole("row", { name: "/home XFS at least 1 KiB Partition at installation disk" }); + const row = within(body).getByRole("row", { + name: "/home XFS at least 1 KiB Partition at installation disk", + }); const actions = within(row).getByRole("button", { name: "Actions" }); await user.click(actions); expect(within(row).queryByRole("menuitem", { name: "Reset location" })).toBeNull(); @@ -369,15 +379,21 @@ describe.skip("if there are volumes", () => { const { user } = await expandField(); const [, body] = await screen.findAllByRole("rowgroup"); - const row = within(body).getByRole("row", { name: "/home XFS at least 1 KiB Partition at /dev/sda" }); + const row = within(body).getByRole("row", { + name: "/home XFS at least 1 KiB Partition at /dev/sda", + }); const actions = within(row).getByRole("button", { name: "Actions" }); await user.click(actions); const resetLocationAction = within(row).queryByRole("menuitem", { name: "Reset location" }); await user.click(resetLocationAction); expect(props.onVolumesChange).toHaveBeenCalledWith( expect.arrayContaining([ - expect.objectContaining({ mountPath: "/home", target: "DEFAULT", targetDevice: undefined }) - ]) + expect.objectContaining({ + mountPath: "/home", + target: "DEFAULT", + targetDevice: undefined, + }), + ]), ); // NOTE: sadly we cannot perform the below check because the component is @@ -397,7 +413,9 @@ describe.skip("if there are volumes", () => { const [, volumes] = await screen.findAllByRole("rowgroup"); - within(volumes).getByRole("row", { name: "/ Transactional Btrfs 1 KiB - 2 KiB Partition at installation disk" }); + within(volumes).getByRole("row", { + name: "/ Transactional Btrfs 1 KiB - 2 KiB Partition at installation disk", + }); }); }); @@ -411,7 +429,9 @@ describe.skip("if there are volumes", () => { const [, volumes] = await screen.findAllByRole("rowgroup"); - within(volumes).getByRole("row", { name: "/ Btrfs with snapshots 1 KiB - 2 KiB Partition at installation disk" }); + within(volumes).getByRole("row", { + name: "/ Btrfs with snapshots 1 KiB - 2 KiB Partition at installation disk", + }); }); }); @@ -420,7 +440,7 @@ describe.skip("if there are volumes", () => { props.volumes = [ rootVolume, { ...swapVolume, target: "NEW_PARTITION", targetDevice: sda }, - { ...homeVolume, target: "NEW_VG", targetDevice: sda } + { ...homeVolume, target: "NEW_VG", targetDevice: sda }, ]; }); @@ -430,7 +450,9 @@ describe.skip("if there are volumes", () => { const [, volumes] = await screen.findAllByRole("rowgroup"); within(volumes).getByRole("row", { name: "swap Swap 1 KiB Partition at /dev/sda" }); - within(volumes).getByRole("row", { name: "/home XFS at least 1 KiB Separate LVM at /dev/sda" }); + within(volumes).getByRole("row", { + name: "/home XFS at least 1 KiB Separate LVM at /dev/sda", + }); }); }); @@ -439,7 +461,7 @@ describe.skip("if there are volumes", () => { props.volumes = [ rootVolume, { ...swapVolume, target: "FILESYSTEM", targetDevice: sda1 }, - { ...homeVolume, target: "DEVICE", targetDevice: sda2 } + { ...homeVolume, target: "DEVICE", targetDevice: sda2 }, ]; }); diff --git a/web/src/components/storage/ProposalActionsDialog.jsx b/web/src/components/storage/ProposalActionsDialog.jsx index 89e19d7a8e..e3874dc578 100644 --- a/web/src/components/storage/ProposalActionsDialog.jsx +++ b/web/src/components/storage/ProposalActionsDialog.jsx @@ -20,7 +20,7 @@ */ import React, { useState } from "react"; -import { List, ListItem, ExpandableSection, } from "@patternfly/react-core"; +import { List, ListItem, ExpandableSection } from "@patternfly/react-core"; import { _, n_ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { partition } from "~/utils"; @@ -58,17 +58,23 @@ export default function ProposalActionsDialog({ actions = [] }) { if (actions.length === 0) return null; - const [generalActions, subvolActions] = partition(actions, a => !a.subvol); + const [generalActions, subvolActions] = partition(actions, (a) => !a.subvol); const toggleText = isExpanded - // TRANSLATORS: show/hide toggle action, this is a clickable link - ? sprintf(n_("Hide %d subvolume action", "Hide %d subvolume actions", subvolActions.length), subvolActions.length) - // TRANSLATORS: show/hide toggle action, this is a clickable link - : sprintf(n_("Show %d subvolume action", "Show %d subvolume actions", subvolActions.length), subvolActions.length); + ? // TRANSLATORS: show/hide toggle action, this is a clickable link + sprintf( + n_("Hide %d subvolume action", "Hide %d subvolume actions", subvolActions.length), + subvolActions.length, + ) + : // TRANSLATORS: show/hide toggle action, this is a clickable link + sprintf( + n_("Show %d subvolume action", "Show %d subvolume actions", subvolActions.length), + subvolActions.length, + ); return ( <> - {subvolActions.length > 0 && + {subvolActions.length > 0 && ( - } + + )} ); } diff --git a/web/src/components/storage/ProposalActionsDialog.test.jsx b/web/src/components/storage/ProposalActionsDialog.test.jsx index 8d97f6b47b..b12544d70a 100644 --- a/web/src/components/storage/ProposalActionsDialog.test.jsx +++ b/web/src/components/storage/ProposalActionsDialog.test.jsx @@ -25,27 +25,59 @@ import { plainRender } from "~/test-utils"; import { ProposalActionsDialog } from "~/components/storage"; const actions = [ - { text: 'Create GPT on /dev/vdc', subvol: false, delete: false }, - { text: 'Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition', subvol: false, delete: false }, - { text: 'Create encrypted partition /dev/vdc2 (29.99 GiB) as LVM physical volume', subvol: false, delete: false }, - { text: 'Create volume group system0 (29.98 GiB) with /dev/mapper/cr_vdc2 (29.99 GiB)', subvol: false, delete: false }, - { text: 'Create LVM logical volume /dev/system0/root (20.00 GiB) on volume group system0 for / with btrfs', subvol: false, delete: false }, + { text: "Create GPT on /dev/vdc", subvol: false, delete: false }, + { + text: "Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition", + subvol: false, + delete: false, + }, + { + text: "Create encrypted partition /dev/vdc2 (29.99 GiB) as LVM physical volume", + subvol: false, + delete: false, + }, + { + text: "Create volume group system0 (29.98 GiB) with /dev/mapper/cr_vdc2 (29.99 GiB)", + subvol: false, + delete: false, + }, + { + text: "Create LVM logical volume /dev/system0/root (20.00 GiB) on volume group system0 for / with btrfs", + subvol: false, + delete: false, + }, ]; const subvolumeActions = [ - { text: 'Create subvolume @ on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/var on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/usr/local on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/srv on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/root on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/opt on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/home on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/boot/writable on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/boot/grub2/x86_64-efi on /dev/system0/root (20.00 GiB)', subvol: true, delete: false }, - { text: 'Create subvolume @/boot/grub2/i386-pc on /dev/system0/root (20.00 GiB)', subvol: true, delete: false } + { text: "Create subvolume @ on /dev/system0/root (20.00 GiB)", subvol: true, delete: false }, + { text: "Create subvolume @/var on /dev/system0/root (20.00 GiB)", subvol: true, delete: false }, + { + text: "Create subvolume @/usr/local on /dev/system0/root (20.00 GiB)", + subvol: true, + delete: false, + }, + { text: "Create subvolume @/srv on /dev/system0/root (20.00 GiB)", subvol: true, delete: false }, + { text: "Create subvolume @/root on /dev/system0/root (20.00 GiB)", subvol: true, delete: false }, + { text: "Create subvolume @/opt on /dev/system0/root (20.00 GiB)", subvol: true, delete: false }, + { text: "Create subvolume @/home on /dev/system0/root (20.00 GiB)", subvol: true, delete: false }, + { + text: "Create subvolume @/boot/writable on /dev/system0/root (20.00 GiB)", + subvol: true, + delete: false, + }, + { + text: "Create subvolume @/boot/grub2/x86_64-efi on /dev/system0/root (20.00 GiB)", + subvol: true, + delete: false, + }, + { + text: "Create subvolume @/boot/grub2/i386-pc on /dev/system0/root (20.00 GiB)", + subvol: true, + delete: false, + }, ]; -const destructiveAction = { text: 'Delete ext4 on /dev/vdc', subvol: false, delete: true }; +const destructiveAction = { text: "Delete ext4 on /dev/vdc", subvol: false, delete: true }; const onCloseFn = jest.fn(); @@ -56,7 +88,7 @@ it.skip("renders nothing by default", () => { it.skip("renders nothing when isOpen=false", () => { const { container } = plainRender( - + , ); expect(container).toBeEmptyDOMElement(); }); @@ -77,11 +109,13 @@ describe.skip("when isOpen", () => { const dialog = screen.getByRole("dialog", { name: "Planned Actions" }); const actionsList = within(dialog).getByRole("list"); const actionsListItems = within(actionsList).getAllByRole("listitem"); - expect(actionsListItems.map(i => i.textContent)).toEqual(actions.map(a => a.text)); + expect(actionsListItems.map((i) => i.textContent)).toEqual(actions.map((a) => a.text)); }); it("triggers the onClose callback when user clicks the Close button", async () => { - const { user } = plainRender(); + const { user } = plainRender( + , + ); const closeButton = screen.getByRole("button", { name: "Close" }); await user.click(closeButton); @@ -92,12 +126,18 @@ describe.skip("when isOpen", () => { describe("when there is a destructive action", () => { it("emphasizes the action", () => { plainRender( - + , ); // https://stackoverflow.com/a/63080940 const actionItems = screen.getAllByRole("listitem"); - const destructiveActionItem = actionItems.find(item => item.textContent === destructiveAction.text); + const destructiveActionItem = actionItems.find( + (item) => item.textContent === destructiveAction.text, + ); expect(destructiveActionItem).toHaveClass("proposal-action--delete"); }); @@ -106,7 +146,11 @@ describe.skip("when isOpen", () => { describe("when there are subvolume actions", () => { it("does not render the subvolume actions", () => { plainRender( - + , ); // For now, we know that there are two lists and the subvolume list is the second one. @@ -120,7 +164,11 @@ describe.skip("when isOpen", () => { it("renders the subvolume actions after clicking on 'show subvolumes'", async () => { const { user } = plainRender( - + , ); const link = screen.getByText(/Show.*subvolume actions/); @@ -137,7 +185,7 @@ describe.skip("when isOpen", () => { const [, subvolList] = screen.getAllByRole("list"); const subvolItems = within(subvolList).getAllByRole("listitem"); - expect(subvolItems.map(i => i.textContent)).toEqual(subvolumeActions.map(a => a.text)); + expect(subvolItems.map((i) => i.textContent)).toEqual(subvolumeActions.map((a) => a.text)); }); }); }); diff --git a/web/src/components/storage/ProposalActionsSummary.jsx b/web/src/components/storage/ProposalActionsSummary.jsx index 157a599627..ce673db4c4 100644 --- a/web/src/components/storage/ProposalActionsSummary.jsx +++ b/web/src/components/storage/ProposalActionsSummary.jsx @@ -27,7 +27,7 @@ import { CardField, ButtonLink } from "~/components/core"; import DevicesManager from "~/components/storage/DevicesManager"; import { _, n_ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; /** * @typedef {import ("~/client/storage").Action} Action @@ -49,7 +49,7 @@ const DeletionsInfo = ({ policy, manager, spaceActions }) => { let label; let systemsLabel; const systems = manager.deletedSystems(); - const deleteActions = manager.actions.filter(a => a.delete && !a.subvol).length; + const deleteActions = manager.actions.filter((a) => a.delete && !a.subvol).length; const isDeletePolicy = policy?.id === "delete"; const hasDeleteActions = deleteActions !== 0; @@ -61,13 +61,14 @@ const DeletionsInfo = ({ policy, manager, spaceActions }) => { // TRANSLATORS: %d will be replaced by the amount of destructive actions label = ( - { - sprintf(n_( + {sprintf( + n_( "There is %d destructive action planned", "There are %d destructive actions planned", - deleteActions - ), deleteActions) - } + deleteActions, + ), + deleteActions, + )} ); } @@ -76,7 +77,11 @@ const DeletionsInfo = ({ policy, manager, spaceActions }) => { // FIXME: Use the Intl.ListFormat instead of the `join(", ")` used below. // Most probably, a `listFormat` or similar wrapper should live in src/i18n.js or so. // Read https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat - systemsLabel = <>{_("affecting")} {systems.join(", ")}; + systemsLabel = ( + <> + {_("affecting")} {systems.join(", ")} + + ); } return ( @@ -99,7 +104,7 @@ const ResizesInfo = ({ policy, manager, validProposal, spaceActions }) => { let label; let systemsLabel; const systems = manager.resizedSystems(); - const resizeActions = manager.actions.filter(a => a.resize).length; + const resizeActions = manager.actions.filter((a) => a.resize).length; const isResizePolicy = policy?.id === "resize"; const hasResizeActions = resizeActions !== 0; @@ -112,18 +117,21 @@ const ResizesInfo = ({ policy, manager, validProposal, spaceActions }) => { } else if (validProposal && (isResizePolicy || spaceActions.length > 0) && !hasResizeActions) { label = _("Shrinking some partitions is allowed but not needed"); } else if (hasResizeActions) { - label = sprintf(n_( - "%d partition will be shrunk", - "%d partitions will be shrunk", - resizeActions - ), resizeActions); + label = sprintf( + n_("%d partition will be shrunk", "%d partitions will be shrunk", resizeActions), + resizeActions, + ); } if (systems.length) { // FIXME: Use the Intl.ListFormat instead of the `join(", ")` used below. // Most probably, a `listFormat` or similar wrapper should live in src/i18n.js or so. // Read https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat - systemsLabel = <>{_("affecting")} {systems.join(", ")}; + systemsLabel = ( + <> + {_("affecting")} {systems.join(", ")} + + ); } return ( @@ -156,27 +164,23 @@ const ActionsInfo = ({ actions, validProposal, onClick }) => { label = ( ); } - return ( - - {label} - - ); + return {label}; }; const ActionsSkeleton = () => ( - + @@ -215,8 +219,11 @@ export default function ProposalActionsSummary({ // eslint-disable-next-line agama-i18n/string-literals value = _(policy.summaryLabels[0]); } else { - // eslint-disable-next-line agama-i18n/string-literals - value = sprintf(n_(policy.summaryLabels[0], policy.summaryLabels[1], devices.length), devices.length); + value = sprintf( + // eslint-disable-next-line agama-i18n/string-literals + n_(policy.summaryLabels[0], policy.summaryLabels[1], devices.length), + devices.length, + ); } const devicesManager = new DevicesManager(system, staging, actions); @@ -225,35 +232,37 @@ export default function ProposalActionsSummary({ : {_("Change")} + isLoading ? ( + + ) : ( + {_("Change")} + ) } cardProps={{ isFullHeight: false }} > - { - isLoading - ? - : ( - - a.action === "force_delete")} - /> - a.action === "resize")} - /> - - - ) - } + {isLoading ? ( + + ) : ( + + a.action === "force_delete")} + /> + a.action === "resize")} + /> + + + )} ); diff --git a/web/src/components/storage/ProposalActionsSummary.test.jsx b/web/src/components/storage/ProposalActionsSummary.test.jsx index eda355bc2f..3cb7aa52bb 100644 --- a/web/src/components/storage/ProposalActionsSummary.test.jsx +++ b/web/src/components/storage/ProposalActionsSummary.test.jsx @@ -50,9 +50,9 @@ const sda = { udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }; -const keepPolicy = SPACE_POLICIES.find(p => p.id === "keep"); -const deletePolicy = SPACE_POLICIES.find(p => p.id === "delete"); -const resizePolicy = SPACE_POLICIES.find(p => p.id === "resize"); +const keepPolicy = SPACE_POLICIES.find((p) => p.id === "keep"); +const deletePolicy = SPACE_POLICIES.find((p) => p.id === "delete"); +const resizePolicy = SPACE_POLICIES.find((p) => p.id === "resize"); const defaultProps = { isLoading: false, @@ -60,7 +60,7 @@ const defaultProps = { onActionsClick: jest.fn(), system: devices.system, staging: devices.staging, - actions + actions, }; describe("ProposalActionsSummary", () => { @@ -75,7 +75,7 @@ describe("ProposalActionsSummary", () => { const props = { ...defaultProps, policy: deletePolicy, - actions: [{ device: 79, subvol: false, delete: true, text: "" }] + actions: [{ device: 79, subvol: false, delete: true, text: "" }], }; installerRender(); @@ -90,7 +90,7 @@ describe("ProposalActionsSummary", () => { const props = { ...defaultProps, policy: resizePolicy, - actions: [{ device: 79, subvol: false, delete: false, resize: true, text: "" }] + actions: [{ device: 79, subvol: false, delete: false, resize: true, text: "" }], }; installerRender(); diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index b24897de5d..43956c5ca3 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -49,7 +49,7 @@ const initialState = { system: [], staging: [], actions: [], - errors: [] + errors: [], }; const reducer = (state, action) => { @@ -183,8 +183,8 @@ export default function ProposalPage() { }, [client, cancellablePromise]); const loadDevices = useCallback(async () => { - const system = await cancellablePromise(client.system.getDevices()) || []; - const staging = await cancellablePromise(client.staging.getDevices()) || []; + const system = (await cancellablePromise(client.system.getDevices())) || []; + const staging = (await cancellablePromise(client.staging.getDevices())) || []; return { system, staging }; }, [client, cancellablePromise]); @@ -193,9 +193,12 @@ export default function ProposalPage() { return issues.map(toValidationError); }, [client, cancellablePromise]); - const calculateProposal = useCallback(async (settings) => { - return await cancellablePromise(client.proposal.calculate(settings)); - }, [client, cancellablePromise]); + const calculateProposal = useCallback( + async (settings) => { + return await cancellablePromise(client.proposal.calculate(settings)); + }, + [client, cancellablePromise], + ); const load = useCallback(async () => { dispatch({ type: "START_LOADING" }); @@ -229,24 +232,38 @@ export default function ProposalPage() { dispatch({ type: "UPDATE_ERRORS", payload: { errors } }); if (result !== undefined) dispatch({ type: "STOP_LOADING" }); - }, [calculateProposal, cancellablePromise, client, loadAvailableDevices, loadVolumeDevices, loadDevices, loadEncryptionMethods, loadErrors, loadProposalResult, loadVolumeTemplates]); - - const calculate = useCallback(async (settings) => { - dispatch({ type: "START_LOADING" }); + }, [ + calculateProposal, + cancellablePromise, + client, + loadAvailableDevices, + loadVolumeDevices, + loadDevices, + loadEncryptionMethods, + loadErrors, + loadProposalResult, + loadVolumeTemplates, + ]); + + const calculate = useCallback( + async (settings) => { + dispatch({ type: "START_LOADING" }); + + await calculateProposal(settings); - await calculateProposal(settings); - - const result = await loadProposalResult(); - dispatch({ type: "UPDATE_RESULT", payload: { result } }); + const result = await loadProposalResult(); + dispatch({ type: "UPDATE_RESULT", payload: { result } }); - const devices = await loadDevices(); - dispatch({ type: "UPDATE_DEVICES", payload: devices }); + const devices = await loadDevices(); + dispatch({ type: "UPDATE_DEVICES", payload: devices }); - const errors = await loadErrors(); - dispatch({ type: "UPDATE_ERRORS", payload: { errors } }); + const errors = await loadErrors(); + dispatch({ type: "UPDATE_ERRORS", payload: { errors } }); - dispatch({ type: "STOP_LOADING" }); - }, [calculateProposal, loadDevices, loadErrors, loadProposalResult]); + dispatch({ type: "STOP_LOADING" }); + }, + [calculateProposal, loadDevices, loadErrors, loadProposalResult], + ); useEffect(() => { load().catch(console.error); @@ -275,7 +292,7 @@ export default function ProposalPage() { calculate(newSettings).catch(console.error); }; - const spacePolicy = SPACE_POLICIES.find(p => p.id === state.settings.spacePolicy); + const spacePolicy = SPACE_POLICIES.find((p) => p.id === state.settings.spacePolicy); /** * @todo Enable type checking and ensure the components are called with the correct props. @@ -292,9 +309,7 @@ export default function ProposalPage() { - + { return { ...original, - Skeleton: () =>
      PFSkeleton
      - + Skeleton: () =>
      PFSkeleton
      , }; }); jest.mock("./DevicesTechMenu", () => () =>
      Devices Tech Menu
      ); @@ -51,12 +50,12 @@ jest.mock("./DevicesTechMenu", () => () =>
      Devices Tech Menu
      ); jest.mock("~/queries/software", () => ({ ...jest.requireActual("~/queries/software"), useProduct: () => ({ - selectedProduct: { name: "Test" } + selectedProduct: { name: "Test" }, }), - useProductChanges: () => jest.fn() + useProductChanges: () => jest.fn(), })); -const createClientMock = /** @type {jest.Mock} */(createClient); +const createClientMock = /** @type {jest.Mock} */ (createClient); /** @type {StorageDevice} */ const vda = { @@ -73,7 +72,7 @@ const vda = { sdCard: true, active: true, name: "/dev/vda", - size: 1e+12, + size: 1e12, systems: ["Windows 11", "openSUSE Leap 15.2"], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], @@ -90,7 +89,7 @@ const vdb = { driver: ["ahci", "mmcblk"], bus: "IDE", name: "/dev/vdb", - size: 1e+6 + size: 1e6, }; /** @@ -98,28 +97,26 @@ const vdb = { * @returns {Volume} */ const volume = (mountPath) => { - return ( - { - mountPath, - target: "DEFAULT", - fsType: "Btrfs", - minSize: 1024, - maxSize: 1024, - autoSize: false, - snapshots: false, - transactional: false, - outline: { - required: false, - fsTypes: ["Btrfs"], - supportAutoSize: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, - sizeRelevantVolumes: [], - adjustByRam: false, - productDefined: false - } - } - ); + return { + mountPath, + target: "DEFAULT", + fsType: "Btrfs", + minSize: 1024, + maxSize: 1024, + autoSize: false, + snapshots: false, + transactional: false, + outline: { + required: false, + fsTypes: ["Btrfs"], + supportAutoSize: false, + snapshotsConfigurable: false, + snapshotsAffectSizes: false, + sizeRelevantVolumes: [], + adjustByRam: false, + productDefined: false, + }, + }; }; /** @type {StorageClient} */ @@ -141,9 +138,9 @@ beforeEach(() => { spacePolicy: "", spaceActions: [], volumes: [], - installationDevices: [] + installationDevices: [], }, - actions: [] + actions: [], }; storage = { @@ -155,21 +152,21 @@ beforeEach(() => { getEncryptionMethods: jest.fn().mockResolvedValue([]), getProductMountPoints: jest.fn().mockResolvedValue([]), getResult: jest.fn().mockResolvedValue(proposalResult), - defaultVolume: jest.fn(mountPath => Promise.resolve(volume(mountPath))), + defaultVolume: jest.fn((mountPath) => Promise.resolve(volume(mountPath))), calculate: jest.fn().mockResolvedValue(0), }, // @ts-expect-error Some methods have to be private to avoid type complaint. system: { - getDevices: jest.fn().mockResolvedValue([vda, vdb]) + getDevices: jest.fn().mockResolvedValue([vda, vdb]), }, // @ts-expect-error Some methods have to be private to avoid type complaint. staging: { - getDevices: jest.fn().mockResolvedValue([vda]) + getDevices: jest.fn().mockResolvedValue([vda]), }, getErrors: jest.fn().mockResolvedValue([]), isDeprecated: jest.fn().mockResolvedValue(false), onDeprecate: jest.fn(), - onStatusChange: jest.fn() + onStatusChange: jest.fn(), }; createClientMock.mockImplementation(() => ({ storage })); diff --git a/web/src/components/storage/ProposalResultSection.jsx b/web/src/components/storage/ProposalResultSection.jsx index 829398ae40..e8f7f3f062 100644 --- a/web/src/components/storage/ProposalResultSection.jsx +++ b/web/src/components/storage/ProposalResultSection.jsx @@ -39,7 +39,10 @@ import { _ } from "~/i18n"; */ const ResultSkeleton = () => ( - + @@ -72,13 +75,19 @@ export default function ProposalResultSection({ > {isLoading && } - {errors.length === 0 - ? - : ( - - {errors.map((e, i) =>
      {e.message}
      )} -
      - )} + {errors.length === 0 ? ( + + ) : ( + + {errors.map((e, i) => ( +
      {e.message}
      + ))} +
      + )}
      ); diff --git a/web/src/components/storage/ProposalResultSection.test.jsx b/web/src/components/storage/ProposalResultSection.test.jsx index ccbc15a971..0e3db28354 100644 --- a/web/src/components/storage/ProposalResultSection.test.jsx +++ b/web/src/components/storage/ProposalResultSection.test.jsx @@ -63,7 +63,7 @@ describe.skip("ProposalResultSection", () => { it("does not render a warning when there are not delete actions", () => { const props = { ...defaultProps, - actions: defaultProps.actions.filter(a => !a.delete) + actions: defaultProps.actions.filter((a) => !a.delete), }; plainRender(); @@ -81,7 +81,7 @@ describe.skip("ProposalResultSection", () => { // affected systems are rendered in the warning summary const props = { ...defaultProps, - actions: [{ device: 79, subvol: false, delete: true, text: "" }] + actions: [{ device: 79, subvol: false, delete: true, text: "" }], }; plainRender(); diff --git a/web/src/components/storage/ProposalResultTable.jsx b/web/src/components/storage/ProposalResultTable.jsx index c84ec4d86c..ebaec78719 100644 --- a/web/src/components/storage/ProposalResultTable.jsx +++ b/web/src/components/storage/ProposalResultTable.jsx @@ -24,7 +24,10 @@ import React from "react"; import { Label, Flex } from "@patternfly/react-core"; import { - DeviceName, DeviceDetails, DeviceSize, toStorageDevice + DeviceName, + DeviceDetails, + DeviceSize, + toStorageDevice, } from "~/components/storage/device-utils"; // eslint-disable-next-line @typescript-eslint/no-unused-vars import DevicesManager from "~/components/storage/DevicesManager"; @@ -71,7 +74,11 @@ const DeviceCustomDetails = ({ item, devicesManager }) => { return ( - {isNew() && } + {isNew() && ( + + )} ); }; @@ -90,14 +97,15 @@ const DeviceCustomSize = ({ item, devicesManager }) => { return ( - {isResized && + {isResized && ( } + + )} ); }; @@ -111,7 +119,9 @@ const columns = (devicesManager) => { const renderMountPoint = (item) => ; /** @type {(item: TableItem) => React.ReactNode} */ - const renderDetails = (item) => ; + const renderDetails = (item) => ( + + ); /** @type {(item: TableItem) => React.ReactNode} */ const renderSize = (item) => ; @@ -120,7 +130,7 @@ const columns = (devicesManager) => { { name: _("Device"), value: renderDevice }, { name: _("Mount Point"), value: renderMountPoint }, { name: _("Details"), value: renderDetails }, - { name: _("Size"), value: renderSize, classNames: "sizes-column" } + { name: _("Size"), value: renderSize, classNames: "sizes-column" }, ]; }; diff --git a/web/src/components/storage/ProposalSettingsSection.jsx b/web/src/components/storage/ProposalSettingsSection.jsx index 3c0b1d21f1..d35f6a5f16 100644 --- a/web/src/components/storage/ProposalSettingsSection.jsx +++ b/web/src/components/storage/ProposalSettingsSection.jsx @@ -74,18 +74,15 @@ export default function ProposalSettingsSection({ volumeTemplates, isLoading = false, changing = undefined, - onChange + onChange, }) { /** @param {import("~/components/storage/InstallationDeviceField").TargetConfig} targetConfig */ const changeTarget = ({ target, targetDevice, targetPVDevices }) => { - onChange( - CHANGING.TARGET, - { - target, - targetDevice: targetDevice?.name, - targetPVDevices: targetPVDevices.map(d => d.name) - } - ); + onChange(CHANGING.TARGET, { + target, + targetDevice: targetDevice?.name, + targetPVDevices: targetPVDevices.map((d) => d.name), + }); }; /** @param {import("~/components/storage/EncryptionField").EncryptionConfig} encryptionConfig */ @@ -100,20 +97,17 @@ export default function ProposalSettingsSection({ /** @param {import("~/components/storage/PartitionsField").BootConfig} bootConfig */ const changeBoot = ({ configureBoot, bootDevice }) => { - onChange( - CHANGING.BOOT, - { - configureBoot, - bootDevice: bootDevice?.name - } - ); + onChange(CHANGING.BOOT, { + configureBoot, + bootDevice: bootDevice?.name, + }); }; /** * @param {string} name * @returns {StorageDevice|undefined} */ - const findDevice = (name) => availableDevices.find(a => a.name === name); + const findDevice = (name) => availableDevices.find((a) => a.name === name); /** @type {StorageDevice|undefined} */ const targetDevice = findDevice(settings.targetDevice); @@ -156,7 +150,9 @@ export default function ProposalSettingsSection({ configureBoot={settings.configureBoot} bootDevice={bootDevice} defaultBootDevice={defaultBootDevice} - isLoading={showSkeleton(isLoading, "PartitionsField", changing) || settings.volumes === undefined} + isLoading={ + showSkeleton(isLoading, "PartitionsField", changing) || settings.volumes === undefined + } onVolumesChange={changeVolumes} onBootChange={changeBoot} /> diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index caa0a17729..44d19ffc13 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -36,7 +36,7 @@ jest.mock("@patternfly/react-core", () => { return { ...original, - Skeleton: () =>
      PFSkeleton
      + Skeleton: () =>
      PFSkeleton
      , }; }); @@ -58,7 +58,7 @@ const sda = { name: "/dev/sda", size: 1024, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }; @@ -81,9 +81,9 @@ const sdb = { name: "/dev/sdb", size: 2048, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: ["pci-0000:00-19"] + udevPaths: ["pci-0000:00-19"], }; /** @type {ProposalSettingsSectionProps} */ @@ -103,13 +103,13 @@ beforeEach(() => { spacePolicy: "delete", spaceActions: [], volumes: [], - installationDevices: [sda, sdb] + installationDevices: [sda, sdb], }, availableDevices: [], volumeDevices: [], encryptionMethods: [], volumeTemplates: [], - onChange: jest.fn() + onChange: jest.fn(), }; }); diff --git a/web/src/components/storage/ProposalTransactionalInfo.jsx b/web/src/components/storage/ProposalTransactionalInfo.jsx index 8e65762dc0..1221ad6374 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.jsx +++ b/web/src/components/storage/ProposalTransactionalInfo.jsx @@ -45,9 +45,15 @@ export default function ProposalTransactionalInfo({ settings }) { const title = _("Transactional root file system"); /* TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) */ const description = sprintf( - _("%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots."), - selectedProduct.name + _( + "%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.", + ), + selectedProduct.name, ); - return {description}; + return ( + + {description} + + ); } diff --git a/web/src/components/storage/ProposalTransactionalInfo.test.jsx b/web/src/components/storage/ProposalTransactionalInfo.test.jsx index 561793d4ba..4d6c9018dd 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.test.jsx +++ b/web/src/components/storage/ProposalTransactionalInfo.test.jsx @@ -27,9 +27,9 @@ import { ProposalTransactionalInfo } from "~/components/storage"; jest.mock("~/queries/software", () => ({ ...jest.requireActual("~/queries/software"), useProduct: () => ({ - selectedProduct : { name: "Test" } + selectedProduct: { name: "Test" }, }), - useProductChanges: () => jest.fn() + useProductChanges: () => jest.fn(), })); let props; diff --git a/web/src/components/storage/SnapshotsField.jsx b/web/src/components/storage/SnapshotsField.jsx index ead1c41e83..5aecc518e3 100644 --- a/web/src/components/storage/SnapshotsField.jsx +++ b/web/src/components/storage/SnapshotsField.jsx @@ -26,7 +26,7 @@ import { Split, Switch } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { noop } from "~/utils"; import { hasFS } from "~/components/storage/utils"; -import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; /** * @typedef {import ("~/client/storage").ProposalSettings} ProposalSettings @@ -34,8 +34,10 @@ import textStyles from '@patternfly/react-styles/css/utilities/Text/text'; */ const LABEL = _("Use Btrfs snapshots for the root file system"); -const DESCRIPTION = _("Allows to boot to a previous version of the \ -system after configuration changes or software upgrades."); +const DESCRIPTION = _( + "Allows to boot to a previous version of the \ +system after configuration changes or software upgrades.", +); /** * Allows to define snapshots enablement @@ -50,10 +52,7 @@ system after configuration changes or software upgrades."); * * @param {SnapshotsFieldProps} props */ -export default function SnapshotsField({ - rootVolume, - onChange = noop -}) { +export default function SnapshotsField({ rootVolume, onChange = noop }) { const isChecked = hasFS(rootVolume, "Btrfs") && rootVolume.snapshots; const switchState = () => { @@ -62,12 +61,7 @@ export default function SnapshotsField({ return ( - +
      {LABEL}
      {DESCRIPTION}
      diff --git a/web/src/components/storage/SnapshotsField.test.jsx b/web/src/components/storage/SnapshotsField.test.jsx index 44126ce6c6..2f29b470c4 100644 --- a/web/src/components/storage/SnapshotsField.test.jsx +++ b/web/src/components/storage/SnapshotsField.test.jsx @@ -48,8 +48,8 @@ const rootVolume = { snapshotsAffectSizes: true, adjustByRam: false, sizeRelevantVolumes: ["/home"], - productDefined: true - } + productDefined: true, + }, }; const onChangeFn = jest.fn(); diff --git a/web/src/components/storage/SpaceActionsTable.jsx b/web/src/components/storage/SpaceActionsTable.jsx index c756fc2a67..7007cc8090 100644 --- a/web/src/components/storage/SpaceActionsTable.jsx +++ b/web/src/components/storage/SpaceActionsTable.jsx @@ -24,17 +24,23 @@ import React from "react"; import { Button, - Flex, FlexItem, - List, ListItem, + Flex, + FlexItem, + List, + ListItem, Popover, - ToggleGroup, ToggleGroupItem + ToggleGroup, + ToggleGroupItem, } from "@patternfly/react-core"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -import { deviceChildren, deviceSize } from '~/components/storage/utils'; +import { deviceChildren, deviceSize } from "~/components/storage/utils"; import { - DeviceName, DeviceDetails, DeviceSize, toStorageDevice + DeviceName, + DeviceDetails, + DeviceSize, + toStorageDevice, } from "~/components/storage/device-utils"; import { TreeTable } from "~/components/core"; import { Icon } from "~/components/layout"; @@ -58,8 +64,9 @@ const DeviceInfoContent = ({ device }) => { if (minSize) { const recoverable = device.size - minSize; - return ( - sprintf(_("Up to %s can be recovered by shrinking the device."), deviceSize(recoverable)) + return sprintf( + _("Up to %s can be recovered by shrinking the device."), + deviceSize(recoverable), ); } @@ -69,7 +76,9 @@ const DeviceInfoContent = ({ device }) => { <> {_("The device cannot be shrunk:")} - {reasons.map((reason, idx) => {reason})} + {reasons.map((reason, idx) => ( + {reason} + ))} ); @@ -84,10 +93,7 @@ const DeviceInfoContent = ({ device }) => { */ const DeviceInfo = ({ device }) => { return ( - } - > + }> + ); }; @@ -426,18 +447,24 @@ const ControllersSection = ({ client, manager, load = noop, isLoading = false }) const Content = () => { const LUNScanInfo = () => { const msg = allowLUNScan - // TRANSLATORS: the text in the square brackets [] will be displayed in bold - ? _("Automatic LUN scan is [enabled]. Activating a controller which is \ -running in NPIV mode will automatically configures all its LUNs.") - // TRANSLATORS: the text in the square brackets [] will be displayed in bold - : _("Automatic LUN scan is [disabled]. LUNs have to be manually \ -configured after activating a controller."); + ? // TRANSLATORS: the text in the square brackets [] will be displayed in bold + _( + "Automatic LUN scan is [enabled]. Activating a controller which is \ +running in NPIV mode will automatically configures all its LUNs.", + ) + : // TRANSLATORS: the text in the square brackets [] will be displayed in bold + _( + "Automatic LUN scan is [disabled]. LUNs have to be manually \ +configured after activating a controller.", + ); const [msgStart, msgBold, msgEnd] = msg.split(/[[\]]/); return (

      - {msgStart}{msgBold}{msgEnd} + {msgStart} + {msgBold} + {msgEnd}

      ); }; @@ -474,7 +501,9 @@ const DiskPopup = ({ client, manager, onClose = noop }) => { const onSubmit = async (formData) => { setIsAcceptDisabled(true); const controller = manager.getController(formData.channel); - const result = await cancellablePromise(client.activateDisk(controller, formData.wwpn, formData.lun)); + const result = await cancellablePromise( + client.activateDisk(controller, formData.wwpn, formData.lun), + ); setIsAcceptDisabled(false); if (result === 0) onClose(); @@ -495,11 +524,7 @@ const DiskPopup = ({ client, manager, onClose = noop }) => { onLoading={onLoading} /> - + {_("Accept")} @@ -525,9 +550,7 @@ const DisksSection = ({ client, manager, isLoading = false }) => { const EmptyState = () => { const NoActiveControllers = () => { - return ( -
      {_("Please, try to activate a zFCP controller.")}
      - ); + return
      {_("Please, try to activate a zFCP controller.")}
      ; }; const NoActiveDisks = () => { @@ -535,7 +558,9 @@ const DisksSection = ({ client, manager, isLoading = false }) => { <>
      {_("Please, try to activate a zFCP disk.")}
      {/* TRANSLATORS: button label */} - + ); }; @@ -557,7 +582,9 @@ const DisksSection = ({ client, manager, isLoading = false }) => { {/* TRANSLATORS: button label */} - + @@ -572,12 +599,7 @@ const DisksSection = ({ client, manager, isLoading = false }) => {
      {isLoading && } {!isLoading && manager.disks.length === 0 ? : } - {isActivateOpen && - } + {isActivateOpen && }
      ); }; @@ -630,7 +652,7 @@ const reducer = (state, action) => { const initialState = { manager: new Manager(), - isLoading: true + isLoading: true, }; /** @@ -642,17 +664,20 @@ export default function ZFCPPage() { const { cancellablePromise } = useCancellablePromise(); const [state, dispatch] = useReducer(reducer, initialState); - const getLUNs = useCallback(async (controller) => { - const luns = []; - const wwpns = await cancellablePromise(client.zfcp.getWWPNs(controller)); - for (const wwpn of wwpns) { - const all = await cancellablePromise(client.zfcp.getLUNs(controller, wwpn)); - for (const lun of all) { - luns.push({ channel: controller.channel, wwpn, lun }); + const getLUNs = useCallback( + async (controller) => { + const luns = []; + const wwpns = await cancellablePromise(client.zfcp.getWWPNs(controller)); + for (const wwpn of wwpns) { + const all = await cancellablePromise(client.zfcp.getLUNs(controller, wwpn)); + for (const lun of all) { + luns.push({ channel: controller.channel, wwpn, lun }); + } } - } - return luns; - }, [client.zfcp, cancellablePromise]); + return luns; + }, + [client.zfcp, cancellablePromise], + ); const load = useCallback(async () => { dispatch({ type: "START_LOADING" }); @@ -688,13 +713,13 @@ export default function ZFCPPage() { action("ADD_LUNS", { luns }); } }), - await client.zfcp.onDiskAdded(d => action("ADD_DISK", { disk: d })), - await client.zfcp.onDiskRemoved(d => action("REMOVE_DISK", { disk: d })) + await client.zfcp.onDiskAdded((d) => action("ADD_DISK", { disk: d })), + await client.zfcp.onDiskRemoved((d) => action("REMOVE_DISK", { disk: d })), ); }; const unsubscribe = () => { - subscriptions.forEach(fn => fn()); + subscriptions.forEach((fn) => fn()); }; subscribe(); @@ -709,11 +734,7 @@ export default function ZFCPPage() { load={load} isLoading={state.isLoading} /> - + ); } diff --git a/web/src/components/storage/ZFCPPage.test.jsx b/web/src/components/storage/ZFCPPage.test.jsx index 3d57819152..ba2fac17f3 100644 --- a/web/src/components/storage/ZFCPPage.test.jsx +++ b/web/src/components/storage/ZFCPPage.test.jsx @@ -31,19 +31,30 @@ jest.mock("@patternfly/react-core", () => { return { ...original, - Skeleton: () =>
      PFSkeleton
      - + Skeleton: () =>
      PFSkeleton
      , }; }); const controllers = [ { id: "1", channel: "0.0.fa00", active: false, lunScan: false }, - { id: "2", channel: "0.0.fb00", active: true, lunScan: false } + { id: "2", channel: "0.0.fb00", active: true, lunScan: false }, ]; const disks = [ - { id: "1", name: "/dev/sda", channel: "0.0.fb00", wwpn: "0x500507630703d3b3", lun: "0x0000000000000001" }, - { id: "2", name: "/dev/sdb", channel: "0.0.fb00", wwpn: "0x500507630703d3b3", lun: "0x0000000000000002" } + { + id: "1", + name: "/dev/sda", + channel: "0.0.fb00", + wwpn: "0x500507630703d3b3", + lun: "0x0000000000000001", + }, + { + id: "2", + name: "/dev/sdb", + channel: "0.0.fb00", + wwpn: "0x500507630703d3b3", + lun: "0x0000000000000002", + }, ]; const defaultClient = { @@ -59,7 +70,7 @@ const defaultClient = { activateController: jest.fn().mockResolvedValue(0), getAllowLUNScan: jest.fn().mockResolvedValue(false), activateDisk: jest.fn().mockResolvedValue(0), - deactivateDisk: jest.fn().mockResolvedValue(0) + deactivateDisk: jest.fn().mockResolvedValue(0), }; let client; @@ -83,10 +94,18 @@ it.skip("loads the zFCP devices", async () => { screen.getAllByText(/PFSkeleton/); expect(screen.queryAllByRole("grid").length).toBe(0); await waitFor(() => expect(client.probe).toHaveBeenCalled()); - await waitFor(() => expect(client.getLUNs).toHaveBeenCalledWith(controllers[1], "0x500507630703d3b3")); - await waitFor(() => expect(client.getLUNs).toHaveBeenCalledWith(controllers[1], "0x500507630704d3b3")); - await waitFor(() => expect(client.getLUNs).not.toHaveBeenCalledWith(controllers[0], "0x500507630703d3b3")); - await waitFor(() => expect(client.getLUNs).not.toHaveBeenCalledWith(controllers[0], "0x500507630704d3b3")); + await waitFor(() => + expect(client.getLUNs).toHaveBeenCalledWith(controllers[1], "0x500507630703d3b3"), + ); + await waitFor(() => + expect(client.getLUNs).toHaveBeenCalledWith(controllers[1], "0x500507630704d3b3"), + ); + await waitFor(() => + expect(client.getLUNs).not.toHaveBeenCalledWith(controllers[0], "0x500507630703d3b3"), + ); + await waitFor(() => + expect(client.getLUNs).not.toHaveBeenCalledWith(controllers[0], "0x500507630704d3b3"), + ); expect(screen.getAllByRole("grid").length).toBe(2); }); @@ -177,16 +196,20 @@ describe.skip("if there are not controllers", () => { describe.skip("if there are disks", () => { beforeEach(() => { client.getWWPNs = jest.fn().mockResolvedValue(["0x500507630703d3b3"]); - client.getLUNs = jest.fn().mockResolvedValue( - ["0x0000000000000001", "0x0000000000000002", "0x0000000000000003"] - ); + client.getLUNs = jest + .fn() + .mockResolvedValue(["0x0000000000000001", "0x0000000000000002", "0x0000000000000003"]); }); it("renders the information for each disk", async () => { installerRender(); - await screen.findByRole("row", { name: "/dev/sda 0.0.fb00 0x500507630703d3b3 0x0000000000000001" }); - await screen.findByRole("row", { name: "/dev/sdb 0.0.fb00 0x500507630703d3b3 0x0000000000000002" }); + await screen.findByRole("row", { + name: "/dev/sda 0.0.fb00 0x500507630703d3b3 0x0000000000000001", + }); + await screen.findByRole("row", { + name: "/dev/sdb 0.0.fb00 0x500507630703d3b3 0x0000000000000002", + }); }); it("renders a button for activating a disk", async () => { @@ -212,9 +235,9 @@ describe.skip("if there are disks", () => { describe("if the controller is not using auto LUN scan", () => { beforeEach(() => { - client.getControllers = jest.fn().mockResolvedValue([ - { id: "1", channel: "0.0.fb00", active: true, lunScan: false } - ]); + client.getControllers = jest + .fn() + .mockResolvedValue([{ id: "1", channel: "0.0.fb00", active: true, lunScan: false }]); }); it("allows deactivating a disk", async () => { @@ -229,15 +252,17 @@ describe.skip("if there are disks", () => { const [controller] = await client.getControllers(); const [disk] = await client.getDisks(); - await waitFor(() => expect(client.deactivateDisk).toHaveBeenCalledWith(controller, disk.wwpn, disk.lun)); + await waitFor(() => + expect(client.deactivateDisk).toHaveBeenCalledWith(controller, disk.wwpn, disk.lun), + ); }); }); describe("if the controller is using auto LUN scan", () => { beforeEach(() => { - client.getControllers = jest.fn().mockResolvedValue([ - { id: "1", channel: "0.0.fb00", active: true, lunScan: true } - ]); + client.getControllers = jest + .fn() + .mockResolvedValue([{ id: "1", channel: "0.0.fb00", active: true, lunScan: true }]); }); it("does not allow deactivating a disk", async () => { @@ -278,7 +303,9 @@ describe.skip("if there are not disks", () => { await screen.findByText("No zFCP disks found."); await screen.findByText(/try to activate a zFCP controller/); - await waitFor(() => expect(screen.queryByRole("button", { name: "Activate zFCP disk" })).toBeNull()); + await waitFor(() => + expect(screen.queryByRole("button", { name: "Activate zFCP disk" })).toBeNull(), + ); }); }); }); @@ -286,9 +313,9 @@ describe.skip("if there are not disks", () => { describe.skip("if the button for adding a disk is used", () => { beforeEach(() => { client.getWWPNs = jest.fn().mockResolvedValue(["0x500507630703d3b3"]); - client.getLUNs = jest.fn().mockResolvedValue( - ["0x0000000000000001", "0x0000000000000002", "0x0000000000000003"] - ); + client.getLUNs = jest + .fn() + .mockResolvedValue(["0x0000000000000001", "0x0000000000000002", "0x0000000000000003"]); }); it("opens a popup with the form for a new disk", async () => { @@ -331,7 +358,9 @@ describe.skip("if the button for adding a disk is used", () => { await user.click(accept); expect(client.activateDisk).toHaveBeenCalledWith( - controllers[1], "0x500507630703d3b3", "0x0000000000000003" + controllers[1], + "0x500507630703d3b3", + "0x0000000000000003", ); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); diff --git a/web/src/components/storage/device-utils.jsx b/web/src/components/storage/device-utils.jsx index 9262681db7..5f98ba43c5 100644 --- a/web/src/components/storage/device-utils.jsx +++ b/web/src/components/storage/device-utils.jsx @@ -59,7 +59,11 @@ const FilesystemLabel = ({ item }) => { const label = device.filesystem?.label; if (!label) return null; - return ; + return ( + + ); }; /** @@ -88,8 +92,7 @@ const DeviceDetails = ({ item }) => { if (!device) return _("Unused space"); const renderContent = (device) => { - if (!device.partitionTable && device.systems?.length > 0) - return device.systems.join(", "); + if (!device.partitionTable && device.systems?.length > 0) return device.systems.join(", "); return device.description; }; @@ -100,7 +103,9 @@ const DeviceDetails = ({ item }) => { }; return ( -
      {renderContent(device)} {renderPTableType(device)}
      +
      + {renderContent(device)} {renderPTableType(device)} +
      ); }; diff --git a/web/src/components/storage/device-utils.test.jsx b/web/src/components/storage/device-utils.test.jsx index 218208233f..6f4b380660 100644 --- a/web/src/components/storage/device-utils.test.jsx +++ b/web/src/components/storage/device-utils.test.jsx @@ -25,7 +25,11 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { - DeviceDetails, DeviceName, DeviceSize, FilesystemLabel, toStorageDevice + DeviceDetails, + DeviceName, + DeviceSize, + FilesystemLabel, + toStorageDevice, } from "~/components/storage/device-utils"; /** @@ -52,9 +56,9 @@ const vda = { name: "/dev/vda", description: "", size: 1024, - systems : ["Windows 11", "openSUSE Leap 15.2"], + systems: ["Windows 11", "openSUSE Leap 15.2"], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"] + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], }; /** @type {StorageDevice} */ @@ -69,11 +73,11 @@ const vda1 = { start: 123, encrypted: false, shrinking: { supported: 128 }, - systems : [], + systems: [], udevIds: [], udevPaths: [], isEFI: false, - filesystem: { sid: 100, type: "ext4", mountPath: "/test", label: "system" } + filesystem: { sid: 100, type: "ext4", mountPath: "/test", label: "system" }, }; /** @type {StorageDevice} */ @@ -88,9 +92,9 @@ const lvmLv1 = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; describe("FilesystemLabel", () => { @@ -156,7 +160,7 @@ describe("DeviceDetails", () => { type: "gpt", partitions: [], unpartitionedSize: 0, - unusedSlots: [] + unusedSlots: [], }; }); diff --git a/web/src/components/storage/iscsi/AuthFields.jsx b/web/src/components/storage/iscsi/AuthFields.jsx index 9c4b07d51a..811a82f28e 100644 --- a/web/src/components/storage/iscsi/AuthFields.jsx +++ b/web/src/components/storage/iscsi/AuthFields.jsx @@ -32,28 +32,28 @@ export default function AuthFields({ data, onChange, onValidate }) { const onReverseUsernameChange = (_, v) => onChange("reverseUsername", v); const onReversePasswordChange = (_, v) => onChange("reversePassword", v); - const isValidText = v => v.length > 0; + const isValidText = (v) => v.length > 0; const isValidUsername = () => isValidText(data.username); const isValidPassword = () => isValidText(data.password); const isValidReverseUsername = () => isValidText(data.reverseUsername); const isValidReversePassword = () => isValidText(data.reversePassword); const isValidAuth = () => isValidUsername() && isValidPassword(); - const showUsernameError = () => isValidPassword() ? !isValidUsername() : false; - const showPasswordError = () => isValidUsername() ? !isValidPassword() : false; + const showUsernameError = () => (isValidPassword() ? !isValidUsername() : false); + const showPasswordError = () => (isValidUsername() ? !isValidPassword() : false); const showReverseUsernameError = () => { - return (isValidAuth() && isValidReversePassword()) ? !isValidReverseUsername() : false; + return isValidAuth() && isValidReversePassword() ? !isValidReverseUsername() : false; }; const showReversePasswordError = () => { - return (isValidAuth() && isValidReverseUsername()) ? !isValidReversePassword() : false; + return isValidAuth() && isValidReverseUsername() ? !isValidReversePassword() : false; }; useEffect(() => { onValidate( !showUsernameError() && - !showPasswordError() && - !showReverseUsernameError() && - !showReversePasswordError() + !showPasswordError() && + !showReverseUsernameError() && + !showReversePasswordError(), ); }); @@ -75,10 +75,7 @@ export default function AuthFields({ data, onChange, onValidate }) { return ( <>
      - + - +
      - + - + - + - +
      diff --git a/web/src/components/storage/iscsi/DiscoverForm.jsx b/web/src/components/storage/iscsi/DiscoverForm.jsx index e66574e60a..788d9dc075 100644 --- a/web/src/components/storage/iscsi/DiscoverForm.jsx +++ b/web/src/components/storage/iscsi/DiscoverForm.jsx @@ -20,11 +20,7 @@ */ import React, { useEffect, useRef, useState } from "react"; -import { - Alert, - Form, FormGroup, - TextInput, -} from "@patternfly/react-core"; +import { Alert, Form, FormGroup, TextInput } from "@patternfly/react-core"; import { FormValidationError, Popup } from "~/components/core"; import { AuthFields } from "~/components/storage/iscsi"; @@ -38,7 +34,7 @@ const defaultData = { username: "", password: "", reverseUsername: "", - reversePassword: "" + reversePassword: "", }; export default function DiscoverForm({ onSubmit: onSubmitProp, onCancel }) { @@ -84,11 +80,7 @@ export default function DiscoverForm({ onSubmit: onSubmitProp, onCancel }) { const isValidAddress = () => isValidIp(data.address); const isValidPort = () => Number.isInteger(parseInt(data.port)); const isValidForm = () => { - return ( - isValidAddress() && - isValidPort() && - isValidAuth - ); + return isValidAddress() && isValidPort() && isValidAuth; }; const showAddressError = () => data.address.length > 0 && !isValidAddress(data.address); @@ -101,23 +93,14 @@ export default function DiscoverForm({ onSubmit: onSubmitProp, onCancel }) { // TRANSLATORS: popup title
      - {isFailed && - ( -
      - -

      {_("Make sure you provide the correct values")}

      -
      -
      - )} - + {isFailed && ( +
      + +

      {_("Make sure you provide the correct values")}

      +
      +
      + )} + - + - + - + - setIsValidAuth(v)} - /> + setIsValidAuth(v)} /> - +
      diff --git a/web/src/components/storage/iscsi/EditNodeForm.jsx b/web/src/components/storage/iscsi/EditNodeForm.jsx index acdfe44bf7..8ea1b32081 100644 --- a/web/src/components/storage/iscsi/EditNodeForm.jsx +++ b/web/src/components/storage/iscsi/EditNodeForm.jsx @@ -20,9 +20,7 @@ */ import React, { useState } from "react"; -import { - Form, FormGroup, FormSelect, FormSelectOption -} from "@patternfly/react-core"; +import { Form, FormGroup, FormSelect, FormSelectOption } from "@patternfly/react-core"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; @@ -61,10 +59,7 @@ export default function EditNodeForm({ node, onSubmit: onSubmitProp, onCancel })
      - + diff --git a/web/src/components/storage/iscsi/InitiatorForm.jsx b/web/src/components/storage/iscsi/InitiatorForm.jsx index 67ae7df209..4eb9dcd26c 100644 --- a/web/src/components/storage/iscsi/InitiatorForm.jsx +++ b/web/src/components/storage/iscsi/InitiatorForm.jsx @@ -56,11 +56,7 @@ export default function InitiatorForm({ initiator, onSubmit: onSubmitProp, onCan
      - + diff --git a/web/src/components/storage/iscsi/InitiatorPresenter.jsx b/web/src/components/storage/iscsi/InitiatorPresenter.jsx index 9c06f77ea9..2a0c3362b4 100644 --- a/web/src/components/storage/iscsi/InitiatorPresenter.jsx +++ b/web/src/components/storage/iscsi/InitiatorPresenter.jsx @@ -92,14 +92,9 @@ export default function InitiatorPresenter({ initiator, client }) { - {isFormOpen && - ( - - )} + {isFormOpen && ( + + )} ); } diff --git a/web/src/components/storage/iscsi/InitiatorSection.jsx b/web/src/components/storage/iscsi/InitiatorSection.jsx index ebe9c3eeb4..908d99c9db 100644 --- a/web/src/components/storage/iscsi/InitiatorSection.jsx +++ b/web/src/components/storage/iscsi/InitiatorSection.jsx @@ -47,10 +47,7 @@ export default function InitiatorSection() { return ( // TRANSLATORS: iSCSI initiator section name
      - +
      ); } diff --git a/web/src/components/storage/iscsi/LoginForm.jsx b/web/src/components/storage/iscsi/LoginForm.jsx index 8775944087..c94a89fa70 100644 --- a/web/src/components/storage/iscsi/LoginForm.jsx +++ b/web/src/components/storage/iscsi/LoginForm.jsx @@ -20,10 +20,7 @@ */ import React, { useState } from "react"; -import { - Alert, - Form, FormGroup, FormSelect, FormSelectOption -} from "@patternfly/react-core"; +import { Alert, Form, FormGroup, FormSelect, FormSelectOption } from "@patternfly/react-core"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; @@ -36,7 +33,7 @@ export default function LoginForm({ node, onSubmit: onSubmitProp, onCancel }) { password: "", reverseUsername: "", reversePassword: "", - startup: "onboot" + startup: "onboot", }); const [isLoading, setIsLoading] = useState(false); const [isFailed, setIsFailed] = useState(false); @@ -68,10 +65,11 @@ export default function LoginForm({ node, onSubmit: onSubmitProp, onCancel }) { // TRANSLATORS: %s is replaced by the iSCSI target name
      - { isFailed && + {isFailed && (

      {_("Make sure you provide the correct values")}

      -
      } + + )} {/* TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) */} - setIsValidAuth(v)} - /> + setIsValidAuth(v)} /> - +
      diff --git a/web/src/components/storage/iscsi/NodeStartupOptions.js b/web/src/components/storage/iscsi/NodeStartupOptions.js index 1b9486f15a..acf61668df 100644 --- a/web/src/components/storage/iscsi/NodeStartupOptions.js +++ b/web/src/components/storage/iscsi/NodeStartupOptions.js @@ -24,7 +24,7 @@ import { _ } from "~/i18n"; const NodeStartupOptions = Object.freeze({ MANUAL: { label: _("Manual"), value: "manual" }, ONBOOT: { label: _("On boot"), value: "onboot" }, - AUTOMATIC: { label: _("Automatic"), value: "automatic" } + AUTOMATIC: { label: _("Automatic"), value: "automatic" }, }); export default NodeStartupOptions; diff --git a/web/src/components/storage/iscsi/NodesPresenter.jsx b/web/src/components/storage/iscsi/NodesPresenter.jsx index b1e07a7282..bc6fef9e9a 100644 --- a/web/src/components/storage/iscsi/NodesPresenter.jsx +++ b/web/src/components/storage/iscsi/NodesPresenter.jsx @@ -20,14 +20,14 @@ */ import React, { useState } from "react"; -import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; +import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -import { RowActions } from '~/components/core'; +import { RowActions } from "~/components/core"; import { EditNodeForm, LoginForm, NodeStartupOptions } from "~/components/storage/iscsi"; -export default function NodesPresenter ({ nodes, client }) { +export default function NodesPresenter({ nodes, client }) { const [currentNode, setCurrentNode] = useState(); const [isEditFormOpen, setIsEditFormOpen] = useState(false); const [isLoginFormOpen, setIsLoginFormOpen] = useState(false); @@ -62,7 +62,7 @@ export default function NodesPresenter ({ nodes, client }) { // TRANSLATORS: iSCSI connection status if (!node.connected) return _("Disconnected"); - const startup = Object.values(NodeStartupOptions).find(o => o.value === node.startup); + const startup = Object.values(NodeStartupOptions).find((o) => o.value === node.startup); // TRANSLATORS: iSCSI connection status, %s is replaced by node label return sprintf(_("Connected (%s)"), startup.label); }; @@ -71,27 +71,25 @@ export default function NodesPresenter ({ nodes, client }) { const actions = { edit: { title: _("Edit"), - onClick: () => openEditForm(node) + onClick: () => openEditForm(node), }, delete: { title: _("Delete"), onClick: () => client.iscsi.delete(node), - isDanger: true + isDanger: true, }, login: { title: _("Login"), - onClick: () => openLoginForm(node) + onClick: () => openLoginForm(node), }, logout: { title: _("Logout"), - onClick: () => client.iscsi.logout(node) - } + onClick: () => client.iscsi.logout(node), + }, }; - if (node.connected) - return [actions.edit, actions.logout]; - else - return [actions.login, actions.delete]; + if (node.connected) return [actions.edit, actions.logout]; + else return [actions.login, actions.delete]; }; const NodeRow = ({ node }) => { @@ -110,7 +108,7 @@ export default function NodesPresenter ({ nodes, client }) { }; const Content = () => { - return nodes.map(n => ); + return nodes.map((n) => ); }; return ( @@ -130,18 +128,12 @@ export default function NodesPresenter ({ nodes, client }) { - { isLoginFormOpen && - } - { isEditFormOpen && - } + {isLoginFormOpen && ( + + )} + {isEditFormOpen && ( + + )} ); } diff --git a/web/src/components/storage/iscsi/TargetsSection.jsx b/web/src/components/storage/iscsi/TargetsSection.jsx index 52170d05e7..b34890e1c5 100644 --- a/web/src/components/storage/iscsi/TargetsSection.jsx +++ b/web/src/components/storage/iscsi/TargetsSection.jsx @@ -20,11 +20,7 @@ */ import React, { useEffect, useReducer } from "react"; -import { - Button, - Toolbar, ToolbarItem, ToolbarContent, - Stack -} from "@patternfly/react-core"; +import { Button, Toolbar, ToolbarItem, ToolbarContent, Stack } from "@patternfly/react-core"; import { Section, SectionSkeleton } from "~/components/core"; import { NodesPresenter, DiscoverForm } from "~/components/storage/iscsi"; import { useInstallerClient } from "~/context/installer"; @@ -38,7 +34,7 @@ const reducer = (state, action) => { } case "ADD_NODE": { - if (state.nodes.find(n => n.id === action.payload.node.id)) return state; + if (state.nodes.find((n) => n.id === action.payload.node.id)) return state; const nodes = [...state.nodes]; nodes.push(action.payload.node); @@ -46,7 +42,7 @@ const reducer = (state, action) => { } case "UPDATE_NODE": { - const index = state.nodes.findIndex(n => n.id === action.payload.node.id); + const index = state.nodes.findIndex((n) => n.id === action.payload.node.id); if (index === -1) return state; const nodes = [...state.nodes]; @@ -55,7 +51,7 @@ const reducer = (state, action) => { } case "REMOVE_NODE": { - const nodes = state.nodes.filter(n => n.id !== action.payload.node.id); + const nodes = state.nodes.filter((n) => n.id !== action.payload.node.id); return { ...state, nodes }; } @@ -84,7 +80,7 @@ const reducer = (state, action) => { const initialState = { nodes: [], isDiscoverFormOpen: false, - isLoading: true + isLoading: true, }; export default function TargetsSection() { @@ -106,9 +102,9 @@ export default function TargetsSection() { useEffect(() => { const action = (type, node) => dispatch({ type, payload: { node } }); - client.iscsi.onNodeAdded(n => action("ADD_NODE", n)); - client.iscsi.onNodeChanged(n => action("UPDATE_NODE", n)); - client.iscsi.onNodeRemoved(n => action("REMOVE_NODE", n)); + client.iscsi.onNodeAdded((n) => action("ADD_NODE", n)); + client.iscsi.onNodeChanged((n) => action("UPDATE_NODE", n)); + client.iscsi.onNodeRemoved((n) => action("REMOVE_NODE", n)); }, [client.iscsi]); const openDiscoverForm = () => { @@ -140,9 +136,13 @@ export default function TargetsSection() { return (
      {_("No iSCSI targets found.")}
      -
      {_("Please, perform an iSCSI discovery in order to find available iSCSI targets.")}
      +
      + {_("Please, perform an iSCSI discovery in order to find available iSCSI targets.")} +
      {/* TRANSLATORS: button label, starts iSCSI discovery */} - +
      ); } @@ -157,10 +157,7 @@ export default function TargetsSection() { - + ); }; @@ -169,11 +166,9 @@ export default function TargetsSection() { // TRANSLATORS: iSCSI targets section title
      - {state.isDiscoverFormOpen && - } + {state.isDiscoverFormOpen && ( + + )}
      ); } diff --git a/web/src/components/storage/routes.js b/web/src/components/storage/routes.js index 7efffd35cc..a8be087e3e 100644 --- a/web/src/components/storage/routes.js +++ b/web/src/components/storage/routes.js @@ -34,7 +34,7 @@ import { N_ } from "~/i18n"; const navigation = [ // FIXME: use index: true { path: "/storage", element: , handle: { name: N_("Proposal") } }, - { path: "iscsi", element: , handle: { name: N_("iSCSI") } } + { path: "iscsi", element: , handle: { name: N_("iSCSI") } }, ]; // if (something) { @@ -48,7 +48,7 @@ const navigation = [ const selectors = [ { path: "target-device", element: }, { path: "booting-partition", element: }, - { path: "space-policy", element: } + { path: "space-policy", element: }, ]; const routes = { @@ -56,12 +56,9 @@ const routes = { element: , handle: { name: N_("Storage"), - icon: "hard_drive" + icon: "hard_drive", }, - children: [ - ...navigation, - ...selectors, - ] + children: [...navigation, ...selectors], }; export default routes; diff --git a/web/src/components/storage/test-data/full-result-example.js b/web/src/components/storage/test-data/full-result-example.js index 79aaa3ed32..f3587215e4 100644 --- a/web/src/components/storage/test-data/full-result-example.js +++ b/web/src/components/storage/test-data/full-result-example.js @@ -1,1446 +1,1281 @@ export const settings = { - "bootDevice": "/dev/vdc", - "lvm": false, - "spacePolicy": "custom", - "spaceActions": [ + bootDevice: "/dev/vdc", + lvm: false, + spacePolicy: "custom", + spaceActions: [ { - "device": "/dev/vdc3", - "action": "force_delete" + device: "/dev/vdc3", + action: "force_delete", }, { - "device": "/dev/vdc4", - "action": "resize" + device: "/dev/vdc4", + action: "resize", }, { - "device": "/dev/vdc1", - "action": "force_delete" - } + device: "/dev/vdc1", + action: "force_delete", + }, ], - "systemVGDevices": [], - "encryptionPassword": "", - "encryptionMethod": "luks2", - "volumes": [ + systemVGDevices: [], + encryptionPassword: "", + encryptionMethod: "luks2", + volumes: [ { - "mountPath": "/", - "fsType": "Btrfs", - "minSize": 18790481920, - "autoSize": true, - "snapshots": true, - "transactional": false, - "outline": { - "required": true, - "fsTypes": [ - "Btrfs", - "Ext2", - "Ext3", - "Ext4", - "XFS" - ], - "supportAutoSize": true, - "snapshotsConfigurable": true, - "snapshotsAffectSizes": true, - "sizeRelevantVolumes": [ - "/home" - ] - } + mountPath: "/", + fsType: "Btrfs", + minSize: 18790481920, + autoSize: true, + snapshots: true, + transactional: false, + outline: { + required: true, + fsTypes: ["Btrfs", "Ext2", "Ext3", "Ext4", "XFS"], + supportAutoSize: true, + snapshotsConfigurable: true, + snapshotsAffectSizes: true, + sizeRelevantVolumes: ["/home"], + }, }, { - "mountPath": "swap", - "fsType": "Swap", - "minSize": 1610612736, - "maxSize": 1610612736, - "autoSize": false, - "snapshots": false, - "transactional": false, - "outline": { - "required": false, - "fsTypes": [ - "Swap" - ], - "supportAutoSize": false, - "snapshotsConfigurable": false, - "snapshotsAffectSizes": false, - "sizeRelevantVolumes": [] - } - } + mountPath: "swap", + fsType: "Swap", + minSize: 1610612736, + maxSize: 1610612736, + autoSize: false, + snapshots: false, + transactional: false, + outline: { + required: false, + fsTypes: ["Swap"], + supportAutoSize: false, + snapshotsConfigurable: false, + snapshotsAffectSizes: false, + sizeRelevantVolumes: [], + }, + }, ], - "installationDevices": [ + installationDevices: [ { - "sid": 70, - "name": "/dev/vdc", - "description": "Disk", - "isDrive": true, - "type": "disk", - "vendor": "", - "model": "Disk", - "driver": [ - "virtio-pci", - "virtio_blk" - ], - "bus": "None", - "busId": "", - "transport": "unknown", - "sdCard": false, - "dellBOSS": false, - "active": true, - "encrypted": false, - "start": 0, - "size": 32212254720, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0" - ], - "partitionTable": { - "type": "gpt", - "partitions": [ + sid: 70, + name: "/dev/vdc", + description: "Disk", + isDrive: true, + type: "disk", + vendor: "", + model: "Disk", + driver: ["virtio-pci", "virtio_blk"], + bus: "None", + busId: "", + transport: "unknown", + sdCard: false, + dellBOSS: false, + active: true, + encrypted: false, + start: 0, + size: 32212254720, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0"], + partitionTable: { + type: "gpt", + partitions: [ { - "sid": 78, - "name": "/dev/vdc1", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part1" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } + sid: 78, + name: "/dev/vdc1", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part1"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, }, { - "sid": 79, - "name": "/dev/vdc2", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 10487808, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [ - "openSUSE Leap 15.2", - "Fedora 10.30" - ], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } + sid: 79, + name: "/dev/vdc2", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 10487808, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: ["openSUSE Leap 15.2", "Fedora 10.30"], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part2"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, }, { - "sid": 80, - "name": "/dev/vdc3", - "description": "XFS Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 20973568, - "size": 1073741824, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part3" - ], - "isEFI": false, - "filesystem": { - "sid": 92, - "type": "xfs" - } + sid: 80, + name: "/dev/vdc3", + description: "XFS Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 20973568, + size: 1073741824, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part3"], + isEFI: false, + filesystem: { + sid: 92, + type: "xfs", + }, }, { - "sid": 81, - "name": "/dev/vdc4", - "description": "Linux", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 23070720, - "size": 2147483648, - "shrinking": { "supported": 2147483136 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part4" - ], - "isEFI": false - } + sid: 81, + name: "/dev/vdc4", + description: "Linux", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 23070720, + size: 2147483648, + shrinking: { supported: 2147483136 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part4"], + isEFI: false, + }, ], - "unpartitionedSize": 18253611008, - "unusedSlots": [ + unpartitionedSize: 18253611008, + unusedSlots: [ { - "start": 27265024, - "size": 18252545536 - } - ] - } - } - ] + start: 27265024, + size: 18252545536, + }, + ], + }, + }, + ], }; export const devices = { - "system": [ + system: [ { - "sid": 71, - "name": "/dev/vda", - "description": "Disk", - "isDrive": true, - "type": "disk", - "vendor": "", - "model": "Disk", - "driver": [ - "virtio-pci", - "virtio_blk" - ], - "bus": "None", - "busId": "", - "transport": "unknown", - "sdCard": false, - "dellBOSS": false, - "active": true, - "encrypted": false, - "start": 0, - "size": 53687091200, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0" - ], - "partitionTable": { - "type": "gpt", - "partitions": [ + sid: 71, + name: "/dev/vda", + description: "Disk", + isDrive: true, + type: "disk", + vendor: "", + model: "Disk", + driver: ["virtio-pci", "virtio_blk"], + bus: "None", + busId: "", + transport: "unknown", + sdCard: false, + dellBOSS: false, + active: true, + encrypted: false, + start: 0, + size: 53687091200, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0"], + partitionTable: { + type: "gpt", + partitions: [ { - "sid": 83, - "name": "/dev/vda1", - "description": "BIOS Boot Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 8388608, - "shrinking": { "supported": 8388096 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part1" - ], - "isEFI": false + sid: 83, + name: "/dev/vda1", + description: "BIOS Boot Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 8388608, + shrinking: { supported: 8388096 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part1"], + isEFI: false, }, { - "sid": 84, - "name": "/dev/vda2", - "description": "PV of system", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 53677637120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "physical_volume", - "deviceNames": [ - "/dev/system" - ] - } - } + sid: 84, + name: "/dev/vda2", + description: "PV of system", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 53677637120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part2"], + isEFI: false, + component: { + type: "physical_volume", + deviceNames: ["/dev/system"], + }, + }, ], - "unpartitionedSize": 1065472, - "unusedSlots": [] - } + unpartitionedSize: 1065472, + unusedSlots: [], + }, }, { - "sid": 69, - "name": "/dev/vdb", - "description": "Ext4 Disk", - "isDrive": true, - "type": "disk", - "vendor": "", - "model": "Disk", - "driver": [ - "virtio-pci", - "virtio_blk" - ], - "bus": "None", - "busId": "", - "transport": "unknown", - "sdCard": false, - "dellBOSS": false, - "active": true, - "encrypted": false, - "start": 0, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:08:00.0" - ], - "filesystem": { - "sid": 87, - "type": "ext4" - } + sid: 69, + name: "/dev/vdb", + description: "Ext4 Disk", + isDrive: true, + type: "disk", + vendor: "", + model: "Disk", + driver: ["virtio-pci", "virtio_blk"], + bus: "None", + busId: "", + transport: "unknown", + sdCard: false, + dellBOSS: false, + active: true, + encrypted: false, + start: 0, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:08:00.0"], + filesystem: { + sid: 87, + type: "ext4", + }, }, { - "sid": 70, - "name": "/dev/vdc", - "description": "Disk", - "isDrive": true, - "type": "disk", - "vendor": "", - "model": "Disk", - "driver": [ - "virtio-pci", - "virtio_blk" - ], - "bus": "None", - "busId": "", - "transport": "unknown", - "sdCard": false, - "dellBOSS": false, - "active": true, - "encrypted": false, - "start": 0, - "size": 32212254720, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0" - ], - "partitionTable": { - "type": "gpt", - "partitions": [ + sid: 70, + name: "/dev/vdc", + description: "Disk", + isDrive: true, + type: "disk", + vendor: "", + model: "Disk", + driver: ["virtio-pci", "virtio_blk"], + bus: "None", + busId: "", + transport: "unknown", + sdCard: false, + dellBOSS: false, + active: true, + encrypted: false, + start: 0, + size: 32212254720, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0"], + partitionTable: { + type: "gpt", + partitions: [ { - "sid": 78, - "name": "/dev/vdc1", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part1" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } + sid: 78, + name: "/dev/vdc1", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part1"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, }, { - "sid": 79, - "name": "/dev/vdc2", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 10487808, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [ - "openSUSE Leap 15.2", - "Fedora 10.30" - ], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } + sid: 79, + name: "/dev/vdc2", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 10487808, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: ["openSUSE Leap 15.2", "Fedora 10.30"], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part2"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, }, { - "sid": 80, - "name": "/dev/vdc3", - "description": "XFS Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 20973568, - "size": 1073741824, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part3" - ], - "isEFI": false, - "filesystem": { - "sid": 92, - "type": "xfs" - } + sid: 80, + name: "/dev/vdc3", + description: "XFS Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 20973568, + size: 1073741824, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part3"], + isEFI: false, + filesystem: { + sid: 92, + type: "xfs", + }, }, { - "sid": 81, - "name": "/dev/vdc4", - "description": "Linux", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 23070720, - "size": 2147483648, - "shrinking": { "supported": 2147483136 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part4" - ], - "isEFI": false - } + sid: 81, + name: "/dev/vdc4", + description: "Linux", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 23070720, + size: 2147483648, + shrinking: { supported: 2147483136 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part4"], + isEFI: false, + }, ], - "unpartitionedSize": 18253611008, - "unusedSlots": [ + unpartitionedSize: 18253611008, + unusedSlots: [ { - "start": 27265024, - "size": 18252545536 - } - ] - } + start: 27265024, + size: 18252545536, + }, + ], + }, }, { - "sid": 72, - "name": "/dev/md0", - "description": "Disk", - "isDrive": false, - "type": "md", - "level": "raid0", - "uuid": "644aeee1:5f5b946a:4da99758:3f85b3ea", - "devices": [ + sid: 72, + name: "/dev/md0", + description: "Disk", + isDrive: false, + type: "md", + level: "raid0", + uuid: "644aeee1:5f5b946a:4da99758:3f85b3ea", + devices: [ { - "sid": 78, - "name": "/dev/vdc1", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part1" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } + sid: 78, + name: "/dev/vdc1", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part1"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, }, { - "sid": 79, - "name": "/dev/vdc2", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 10487808, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [ - "openSUSE Leap 15.2", - "Fedora 10.30" - ], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } - } - ], - "active": true, - "encrypted": false, - "start": 0, - "size": 10737287168, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [ - "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea" + sid: 79, + name: "/dev/vdc2", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 10487808, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: ["openSUSE Leap 15.2", "Fedora 10.30"], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part2"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, + }, ], - "udevPaths": [], - "partitionTable": { - "type": "gpt", - "partitions": [ + active: true, + encrypted: false, + start: 0, + size: 10737287168, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: ["md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea"], + udevPaths: [], + partitionTable: { + type: "gpt", + partitions: [ { - "sid": 86, - "name": "/dev/md0p1", - "description": "Ext4 Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 2147483648, - "shrinking": { "supported": 2040147968 }, - "systems": [], - "udevIds": [ - "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1" - ], - "udevPaths": [], - "isEFI": false, - "filesystem": { - "sid": 93, - "type": "ext4" - } - } + sid: 86, + name: "/dev/md0p1", + description: "Ext4 Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 2147483648, + shrinking: { supported: 2040147968 }, + systems: [], + udevIds: ["md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1"], + udevPaths: [], + isEFI: false, + filesystem: { + sid: 93, + type: "ext4", + }, + }, ], - "unpartitionedSize": 8589803520, - "unusedSlots": [ + unpartitionedSize: 8589803520, + unusedSlots: [ { - "start": 4196352, - "size": 8588738048 - } - ] - } + start: 4196352, + size: 8588738048, + }, + ], + }, }, { - "sid": 73, - "name": "/dev/system", - "description": "LVM", - "isDrive": false, - "type": "lvmVg", - "size": 53674508288, - "physicalVolumes": [ + sid: 73, + name: "/dev/system", + description: "LVM", + isDrive: false, + type: "lvmVg", + size: 53674508288, + physicalVolumes: [ { - "sid": 84, - "name": "/dev/vda2", - "description": "PV of system", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 53677637120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "physical_volume", - "deviceNames": [ - "/dev/system" - ] - } - } + sid: 84, + name: "/dev/vda2", + description: "PV of system", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 53677637120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part2"], + isEFI: false, + component: { + type: "physical_volume", + deviceNames: ["/dev/system"], + }, + }, ], - "logicalVolumes": [ + logicalVolumes: [ { - "sid": 75, - "name": "/dev/system/root", - "description": "Ext4 LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 51527024640, - "shrinking": { "supported": 30647779328 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 88, - "type": "ext4", - "mountPath": "/" - } + sid: 75, + name: "/dev/system/root", + description: "Ext4 LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 51527024640, + shrinking: { supported: 30647779328 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 88, + type: "ext4", + mountPath: "/", + }, }, { - "sid": 76, - "name": "/dev/system/swap", - "description": "Swap LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 2147483648, - "shrinking": { "supported": 2143289344 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 90, - "type": "swap", - "mountPath": "swap" - } - } - ] + sid: 76, + name: "/dev/system/swap", + description: "Swap LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 2147483648, + shrinking: { supported: 2143289344 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 90, + type: "swap", + mountPath: "swap", + }, + }, + ], }, { - "sid": 75, - "name": "/dev/system/root", - "description": "Ext4 LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 51527024640, - "shrinking": { "supported": 30647779328 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 88, - "type": "ext4", - "mountPath": "/" - } + sid: 75, + name: "/dev/system/root", + description: "Ext4 LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 51527024640, + shrinking: { supported: 30647779328 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 88, + type: "ext4", + mountPath: "/", + }, }, { - "sid": 76, - "name": "/dev/system/swap", - "description": "Swap LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 2147483648, - "shrinking": { "supported": 2143289344 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 90, - "type": "swap", - "mountPath": "swap" - } + sid: 76, + name: "/dev/system/swap", + description: "Swap LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 2147483648, + shrinking: { supported: 2143289344 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 90, + type: "swap", + mountPath: "swap", + }, }, { - "sid": 83, - "name": "/dev/vda1", - "description": "BIOS Boot Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 8388608, - "shrinking": { "supported": 8388096 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part1" - ], - "isEFI": false + sid: 83, + name: "/dev/vda1", + description: "BIOS Boot Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 8388608, + shrinking: { supported: 8388096 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part1"], + isEFI: false, }, { - "sid": 84, - "name": "/dev/vda2", - "description": "PV of system", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 53677637120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "physical_volume", - "deviceNames": [ - "/dev/system" - ] - } + sid: 84, + name: "/dev/vda2", + description: "PV of system", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 53677637120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part2"], + isEFI: false, + component: { + type: "physical_volume", + deviceNames: ["/dev/system"], + }, }, { - "sid": 78, - "name": "/dev/vdc1", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part1" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } + sid: 78, + name: "/dev/vdc1", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part1"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, }, { - "sid": 79, - "name": "/dev/vdc2", - "description": "Part of md0", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 10487808, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [ - "openSUSE Leap 15.2", - "Fedora 10.30" - ], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "md_device", - "deviceNames": [ - "/dev/md0" - ] - } + sid: 79, + name: "/dev/vdc2", + description: "Part of md0", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 10487808, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: ["openSUSE Leap 15.2", "Fedora 10.30"], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part2"], + isEFI: false, + component: { + type: "md_device", + deviceNames: ["/dev/md0"], + }, }, { - "sid": 80, - "name": "/dev/vdc3", - "description": "XFS Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 20973568, - "size": 1073741824, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part3" - ], - "isEFI": false, - "filesystem": { - "sid": 92, - "type": "xfs" - } + sid: 80, + name: "/dev/vdc3", + description: "XFS Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 20973568, + size: 1073741824, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part3"], + isEFI: false, + filesystem: { + sid: 92, + type: "xfs", + }, }, { - "sid": 81, - "name": "/dev/vdc4", - "description": "Linux", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 23070720, - "size": 2147483648, - "shrinking": { "supported": 2147483136 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part4" - ], - "isEFI": false + sid: 81, + name: "/dev/vdc4", + description: "Linux", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 23070720, + size: 2147483648, + shrinking: { supported: 2147483136 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part4"], + isEFI: false, }, { - "sid": 86, - "name": "/dev/md0p1", - "description": "Ext4 Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 2147483648, - "shrinking": { "supported": 2040147968 }, - "systems": [], - "udevIds": [ - "md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1" - ], - "udevPaths": [], - "isEFI": false, - "filesystem": { - "sid": 93, - "type": "ext4" - } - } + sid: 86, + name: "/dev/md0p1", + description: "Ext4 Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 2147483648, + shrinking: { supported: 2040147968 }, + systems: [], + udevIds: ["md-uuid-644aeee1:5f5b946a:4da99758:3f85b3ea-part1"], + udevPaths: [], + isEFI: false, + filesystem: { + sid: 93, + type: "ext4", + }, + }, ], - "staging": [ + staging: [ { - "sid": 71, - "name": "/dev/vda", - "description": "Disk", - "isDrive": true, - "type": "disk", - "vendor": "", - "model": "Disk", - "driver": [ - "virtio-pci", - "virtio_blk" - ], - "bus": "None", - "busId": "", - "transport": "unknown", - "sdCard": false, - "dellBOSS": false, - "active": true, - "encrypted": false, - "start": 0, - "size": 53687091200, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0" - ], - "partitionTable": { - "type": "gpt", - "partitions": [ + sid: 71, + name: "/dev/vda", + description: "Disk", + isDrive: true, + type: "disk", + vendor: "", + model: "Disk", + driver: ["virtio-pci", "virtio_blk"], + bus: "None", + busId: "", + transport: "unknown", + sdCard: false, + dellBOSS: false, + active: true, + encrypted: false, + start: 0, + size: 53687091200, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0"], + partitionTable: { + type: "gpt", + partitions: [ { - "sid": 83, - "name": "/dev/vda1", - "description": "BIOS Boot Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 8388608, - "shrinking": { "supported": 8388096 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part1" - ], - "isEFI": false + sid: 83, + name: "/dev/vda1", + description: "BIOS Boot Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 8388608, + shrinking: { supported: 8388096 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part1"], + isEFI: false, }, { - "sid": 84, - "name": "/dev/vda2", - "description": "PV of system", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 53677637120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "physical_volume", - "deviceNames": [ - "/dev/system" - ] - } - } + sid: 84, + name: "/dev/vda2", + description: "PV of system", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 53677637120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part2"], + isEFI: false, + component: { + type: "physical_volume", + deviceNames: ["/dev/system"], + }, + }, ], - "unpartitionedSize": 1065472, - "unusedSlots": [] - } + unpartitionedSize: 1065472, + unusedSlots: [], + }, }, { - "sid": 69, - "name": "/dev/vdb", - "description": "Ext4 Disk", - "isDrive": true, - "type": "disk", - "vendor": "", - "model": "Disk", - "driver": [ - "virtio-pci", - "virtio_blk" - ], - "bus": "None", - "busId": "", - "transport": "unknown", - "sdCard": false, - "dellBOSS": false, - "active": true, - "encrypted": false, - "start": 0, - "size": 5368709120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:08:00.0" - ], - "filesystem": { - "sid": 87, - "type": "ext4" - } + sid: 69, + name: "/dev/vdb", + description: "Ext4 Disk", + isDrive: true, + type: "disk", + vendor: "", + model: "Disk", + driver: ["virtio-pci", "virtio_blk"], + bus: "None", + busId: "", + transport: "unknown", + sdCard: false, + dellBOSS: false, + active: true, + encrypted: false, + start: 0, + size: 5368709120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:08:00.0"], + filesystem: { + sid: 87, + type: "ext4", + }, }, { - "sid": 70, - "name": "/dev/vdc", - "description": "Disk", - "isDrive": true, - "type": "disk", - "vendor": "", - "model": "Disk", - "driver": [ - "virtio-pci", - "virtio_blk" - ], - "bus": "None", - "busId": "", - "transport": "unknown", - "sdCard": false, - "dellBOSS": false, - "active": true, - "encrypted": false, - "start": 0, - "size": 32212254720, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0" - ], - "partitionTable": { - "type": "gpt", - "partitions": [ + sid: 70, + name: "/dev/vdc", + description: "Disk", + isDrive: true, + type: "disk", + vendor: "", + model: "Disk", + driver: ["virtio-pci", "virtio_blk"], + bus: "None", + busId: "", + transport: "unknown", + sdCard: false, + dellBOSS: false, + active: true, + encrypted: false, + start: 0, + size: 32212254720, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0"], + partitionTable: { + type: "gpt", + partitions: [ { - "sid": 79, - "name": "/dev/vdc2", - "description": "Linux RAID", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 10487808, - "size": 5368709120, - "shrinking": { "supported": 5368708608 }, - "systems": [ - "openSUSE Leap 15.2", - "Fedora 10.30" - ], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part2" - ], - "isEFI": false + sid: 79, + name: "/dev/vdc2", + description: "Linux RAID", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 10487808, + size: 5368709120, + shrinking: { supported: 5368708608 }, + systems: ["openSUSE Leap 15.2", "Fedora 10.30"], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part2"], + isEFI: false, }, { - "sid": 81, - "name": "/dev/vdc4", - "description": "Linux", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 23070720, - "size": 1608515584, - "shrinking": { "supported": 1608515072 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part4" - ], - "isEFI": false + sid: 81, + name: "/dev/vdc4", + description: "Linux", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 23070720, + size: 1608515584, + shrinking: { supported: 1608515072 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part4"], + isEFI: false, }, { - "sid": 459, - "name": "/dev/vdc1", - "description": "BIOS Boot Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 8388608, - "shrinking": { "supported": 8388096 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part1" - ], - "isEFI": false + sid: 459, + name: "/dev/vdc1", + description: "BIOS Boot Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 8388608, + shrinking: { supported: 8388096 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part1"], + isEFI: false, }, { - "sid": 460, - "name": "/dev/vdc3", - "description": "Swap Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 1610612736, - "shrinking": { "supported": 1610571776 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part3" - ], - "isEFI": false, - "filesystem": { - "sid": 461, - "type": "swap", - "mountPath": "swap" - } + sid: 460, + name: "/dev/vdc3", + description: "Swap Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 1610612736, + shrinking: { supported: 1610571776 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part3"], + isEFI: false, + filesystem: { + sid: 461, + type: "swap", + mountPath: "swap", + }, }, { - "sid": 463, - "name": "/dev/vdc5", - "description": "Btrfs Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 26212352, - "size": 18791513600, - "shrinking": { "supported": 18523078144 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part5" - ], - "isEFI": false, - "filesystem": { - "sid": 464, - "type": "btrfs", - "mountPath": "/" - } - } + sid: 463, + name: "/dev/vdc5", + description: "Btrfs Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 26212352, + size: 18791513600, + shrinking: { supported: 18523078144 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part5"], + isEFI: false, + filesystem: { + sid: 464, + type: "btrfs", + mountPath: "/", + }, + }, ], - "unpartitionedSize": 4824515072, - "unusedSlots": [ + unpartitionedSize: 4824515072, + unusedSlots: [ { - "start": 3164160, - "size": 3749707776 + start: 3164160, + size: 3749707776, }, { - "start": 20973568, - "size": 1073741824 - } - ] - } + start: 20973568, + size: 1073741824, + }, + ], + }, }, { - "sid": 73, - "name": "/dev/system", - "description": "LVM", - "isDrive": false, - "type": "lvmVg", - "size": 53674508288, - "physicalVolumes": [ + sid: 73, + name: "/dev/system", + description: "LVM", + isDrive: false, + type: "lvmVg", + size: 53674508288, + physicalVolumes: [ { - "sid": 84, - "name": "/dev/vda2", - "description": "PV of system", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 53677637120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "physical_volume", - "deviceNames": [ - "/dev/system" - ] - } - } + sid: 84, + name: "/dev/vda2", + description: "PV of system", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 53677637120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part2"], + isEFI: false, + component: { + type: "physical_volume", + deviceNames: ["/dev/system"], + }, + }, ], - "logicalVolumes": [ + logicalVolumes: [ { - "sid": 75, - "name": "/dev/system/root", - "description": "Ext4 LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 51527024640, - "shrinking": { "supported": 30647779328 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 88, - "type": "ext4", - "mountPath": "/" - } + sid: 75, + name: "/dev/system/root", + description: "Ext4 LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 51527024640, + shrinking: { supported: 30647779328 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 88, + type: "ext4", + mountPath: "/", + }, }, { - "sid": 76, - "name": "/dev/system/swap", - "description": "Swap LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 2147483648, - "shrinking": { "supported": 2143289344 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 90, - "type": "swap", - "mountPath": "swap" - } - } - ] + sid: 76, + name: "/dev/system/swap", + description: "Swap LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 2147483648, + shrinking: { supported: 2143289344 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 90, + type: "swap", + mountPath: "swap", + }, + }, + ], }, { - "sid": 75, - "name": "/dev/system/root", - "description": "Ext4 LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 51527024640, - "shrinking": { "supported": 30647779328 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 88, - "type": "ext4", - "mountPath": "/" - } + sid: 75, + name: "/dev/system/root", + description: "Ext4 LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 51527024640, + shrinking: { supported: 30647779328 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 88, + type: "ext4", + mountPath: "/", + }, }, { - "sid": 76, - "name": "/dev/system/swap", - "description": "Swap LV", - "isDrive": false, - "type": "lvmLv", - "active": true, - "encrypted": false, - "start": 0, - "size": 2147483648, - "shrinking": { "supported": 2143289344 }, - "systems": [], - "udevIds": [], - "udevPaths": [], - "filesystem": { - "sid": 90, - "type": "swap", - "mountPath": "swap" - } + sid: 76, + name: "/dev/system/swap", + description: "Swap LV", + isDrive: false, + type: "lvmLv", + active: true, + encrypted: false, + start: 0, + size: 2147483648, + shrinking: { supported: 2143289344 }, + systems: [], + udevIds: [], + udevPaths: [], + filesystem: { + sid: 90, + type: "swap", + mountPath: "swap", + }, }, { - "sid": 83, - "name": "/dev/vda1", - "description": "BIOS Boot Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 8388608, - "shrinking": { "supported": 8388096 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part1" - ], - "isEFI": false + sid: 83, + name: "/dev/vda1", + description: "BIOS Boot Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 8388608, + shrinking: { supported: 8388096 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part1"], + isEFI: false, }, { - "sid": 84, - "name": "/dev/vda2", - "description": "PV of system", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 53677637120, - "shrinking": { "unsupported": ["Resizing is not supported"] }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:04:00.0-part2" - ], - "isEFI": false, - "component": { - "type": "physical_volume", - "deviceNames": [ - "/dev/system" - ] - } + sid: 84, + name: "/dev/vda2", + description: "PV of system", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 53677637120, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:04:00.0-part2"], + isEFI: false, + component: { + type: "physical_volume", + deviceNames: ["/dev/system"], + }, }, { - "sid": 79, - "name": "/dev/vdc2", - "description": "Linux RAID", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 10487808, - "size": 5368709120, - "shrinking": { "supported": 5368708608 }, - "systems": [ - "openSUSE Leap 15.2", - "Fedora 10.30" - ], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part2" - ], - "isEFI": false + sid: 79, + name: "/dev/vdc2", + description: "Linux RAID", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 10487808, + size: 5368709120, + shrinking: { supported: 5368708608 }, + systems: ["openSUSE Leap 15.2", "Fedora 10.30"], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part2"], + isEFI: false, }, { - "sid": 81, - "name": "/dev/vdc4", - "description": "Linux", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 23070720, - "size": 1608515584, - "shrinking": { "supported": 1608515072 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part4" - ], - "isEFI": false + sid: 81, + name: "/dev/vdc4", + description: "Linux", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 23070720, + size: 1608515584, + shrinking: { supported: 1608515072 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part4"], + isEFI: false, }, { - "sid": 459, - "name": "/dev/vdc1", - "description": "BIOS Boot Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 2048, - "size": 8388608, - "shrinking": { "supported": 8388096 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part1" - ], - "isEFI": false + sid: 459, + name: "/dev/vdc1", + description: "BIOS Boot Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 2048, + size: 8388608, + shrinking: { supported: 8388096 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part1"], + isEFI: false, }, { - "sid": 460, - "name": "/dev/vdc3", - "description": "Swap Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 18432, - "size": 1610612736, - "shrinking": { "supported": 1610571776 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part3" - ], - "isEFI": false, - "filesystem": { - "sid": 461, - "type": "swap", - "mountPath": "swap" - } + sid: 460, + name: "/dev/vdc3", + description: "Swap Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 18432, + size: 1610612736, + shrinking: { supported: 1610571776 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part3"], + isEFI: false, + filesystem: { + sid: 461, + type: "swap", + mountPath: "swap", + }, }, { - "sid": 463, - "name": "/dev/vdc5", - "description": "Btrfs Partition", - "isDrive": false, - "type": "partition", - "active": true, - "encrypted": false, - "start": 26212352, - "size": 18791513600, - "shrinking": { "supported": 18523078144 }, - "systems": [], - "udevIds": [], - "udevPaths": [ - "pci-0000:09:00.0-part5" - ], - "isEFI": false, - "filesystem": { - "sid": 464, - "type": "btrfs", - "mountPath": "/" - } - } - ] + sid: 463, + name: "/dev/vdc5", + description: "Btrfs Partition", + isDrive: false, + type: "partition", + active: true, + encrypted: false, + start: 26212352, + size: 18791513600, + shrinking: { supported: 18523078144 }, + systems: [], + udevIds: [], + udevPaths: ["pci-0000:09:00.0-part5"], + isEFI: false, + filesystem: { + sid: 464, + type: "btrfs", + mountPath: "/", + }, + }, + ], }; export const actions = [ { - "device": 86, - "text": "Delete partition /dev/md0p1 (2.00 GiB)", - "subvol": false, - "delete": true + device: 86, + text: "Delete partition /dev/md0p1 (2.00 GiB)", + subvol: false, + delete: true, }, { - "device": 72, - "text": "Delete RAID0 /dev/md0 (10.00 GiB)", - "subvol": false, - "delete": true + device: 72, + text: "Delete RAID0 /dev/md0 (10.00 GiB)", + subvol: false, + delete: true, }, { - "device": 80, - "text": "Delete partition /dev/vdc3 (1.00 GiB)", - "subvol": false, - "delete": true + device: 80, + text: "Delete partition /dev/vdc3 (1.00 GiB)", + subvol: false, + delete: true, }, { - "device": 78, - "text": "Delete partition /dev/vdc1 (5.00 GiB)", - "subvol": false, - "delete": true + device: 78, + text: "Delete partition /dev/vdc1 (5.00 GiB)", + subvol: false, + delete: true, }, { - "device": 81, - "text": "Shrink partition /dev/vdc4 from 2.00 GiB to 1.50 GiB", - "subvol": false, - "delete": false + device: 81, + text: "Shrink partition /dev/vdc4 from 2.00 GiB to 1.50 GiB", + subvol: false, + delete: false, }, { - "device": 459, - "text": "Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition", - "subvol": false, - "delete": false + device: 459, + text: "Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition", + subvol: false, + delete: false, }, { - "device": 460, - "text": "Create partition /dev/vdc3 (1.50 GiB) for swap", - "subvol": false, - "delete": false + device: 460, + text: "Create partition /dev/vdc3 (1.50 GiB) for swap", + subvol: false, + delete: false, }, { - "device": 463, - "text": "Create partition /dev/vdc5 (17.50 GiB) for / with btrfs", - "subvol": false, - "delete": false + device: 463, + text: "Create partition /dev/vdc5 (17.50 GiB) for / with btrfs", + subvol: false, + delete: false, }, { - "device": 467, - "text": "Create subvolume @ on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 467, + text: "Create subvolume @ on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 482, - "text": "Create subvolume @/boot/grub2/x86_64-efi on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 482, + text: "Create subvolume @/boot/grub2/x86_64-efi on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 480, - "text": "Create subvolume @/boot/grub2/i386-pc on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 480, + text: "Create subvolume @/boot/grub2/i386-pc on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 478, - "text": "Create subvolume @/var on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 478, + text: "Create subvolume @/var on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 476, - "text": "Create subvolume @/usr/local on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 476, + text: "Create subvolume @/usr/local on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 474, - "text": "Create subvolume @/srv on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 474, + text: "Create subvolume @/srv on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 472, - "text": "Create subvolume @/root on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 472, + text: "Create subvolume @/root on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 470, - "text": "Create subvolume @/opt on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false + device: 470, + text: "Create subvolume @/opt on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, }, { - "device": 468, - "text": "Create subvolume @/home on /dev/vdc5 (17.50 GiB)", - "subvol": true, - "delete": false - } + device: 468, + text: "Create subvolume @/home on /dev/vdc5 (17.50 GiB)", + subvol: true, + delete: false, + }, ]; diff --git a/web/src/components/storage/utils.js b/web/src/components/storage/utils.js index 8cb5e408f0..47d9367ec7 100644 --- a/web/src/components/storage/utils.js +++ b/web/src/components/storage/utils.js @@ -57,7 +57,7 @@ import { N_ } from "~/i18n"; const SIZE_METHODS = Object.freeze({ AUTO: "auto", MANUAL: "fixed", - RANGE: "range" + RANGE: "range", }); const SIZE_UNITS = Object.freeze({ @@ -79,8 +79,8 @@ const SPACE_POLICIES = [ summaryLabels: [ // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence // would read as "Find space deleting current content". Keep it short - N_("deleting current content") - ] + N_("deleting current content"), + ], }, { id: "resize", @@ -89,8 +89,8 @@ const SPACE_POLICIES = [ summaryLabels: [ // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence // would read as "Find space shrinking partitions". Keep it short. - N_("shrinking partitions") - ] + N_("shrinking partitions"), + ], }, { id: "keep", @@ -99,8 +99,8 @@ const SPACE_POLICIES = [ summaryLabels: [ // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence // would read as "Find space without modifying any partition". Keep it short. - N_("without modifying any partition") - ] + N_("without modifying any partition"), + ], }, { id: "custom", @@ -109,9 +109,9 @@ const SPACE_POLICIES = [ summaryLabels: [ // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence // would read as "Find space with custom actions". Keep it short. - N_("with custom actions") - ] - } + N_("with custom actions"), + ], + }, ]; /** @@ -130,7 +130,8 @@ const splitSize = (size) => { // From D-Bus, maxSize comes as undefined when set as "unlimited", but for Agama UI // it means "leave it empty" const sanitizedSize = size === undefined ? "" : size; - const parsedSize = typeof sanitizedSize === "string" ? sanitizedSize : xbytes(sanitizedSize, { iec: true }); + const parsedSize = + typeof sanitizedSize === "string" ? sanitizedSize : xbytes(sanitizedSize, { iec: true }); const [qty, unit] = parsedSize.split(" "); // `Number` will remove trailing zeroes; // parseFloat ensures Number does not transform "" into 0. @@ -138,7 +139,7 @@ const splitSize = (size) => { return { unit, - size: isNaN(sanitizedQty) ? undefined : sanitizedQty + size: isNaN(sanitizedQty) ? undefined : sanitizedQty, }; }; @@ -226,12 +227,12 @@ const deviceLabel = (device) => { const deviceChildren = (device) => { const partitionTableChildren = (partitionTable) => { const { partitions, unusedSlots } = partitionTable; - const children = partitions.concat(unusedSlots).filter(i => !!i); - return children.sort((a, b) => a.start < b.start ? -1 : 1); + const children = partitions.concat(unusedSlots).filter((i) => !!i); + return children.sort((a, b) => (a.start < b.start ? -1 : 1)); }; const lvmVgChildren = (lvmVg) => { - return lvmVg.logicalVolumes.sort((a, b) => a.name < b.name ? -1 : 1); + return lvmVg.logicalVolumes.sort((a, b) => (a.name < b.name ? -1 : 1)); }; if (device.partitionTable) return partitionTableChildren(device.partitionTable); @@ -283,7 +284,7 @@ const isTransactionalRoot = (volume) => { * @returns {boolean} */ const isTransactionalSystem = (volumes = []) => { - return volumes.find(v => isTransactionalRoot(v)) !== undefined; + return volumes.find((v) => isTransactionalRoot(v)) !== undefined; }; /** @@ -311,13 +312,13 @@ const reuseDevice = (volume) => volume.target === "FILESYSTEM" || volume.target * @param {Volume} volume * @returns {string} */ -const volumeLabel = (volume) => volume.mountPath === "/" ? "root" : volume.mountPath; +const volumeLabel = (volume) => (volume.mountPath === "/" ? "root" : volume.mountPath); /** * GiB to Bytes. * * @type {(value: number) => number } */ -const gib = (value) => value * (1024 ** 3); +const gib = (value) => value * 1024 ** 3; export { DEFAULT_SIZE_UNIT, @@ -337,5 +338,5 @@ export { isTransactionalSystem, mountFilesystem, reuseDevice, - volumeLabel + volumeLabel, }; diff --git a/web/src/components/storage/utils.test.js b/web/src/components/storage/utils.test.js index 72359504c3..0f07410a00 100644 --- a/web/src/components/storage/utils.test.js +++ b/web/src/components/storage/utils.test.js @@ -31,7 +31,7 @@ import { hasFS, hasSnapshots, isTransactionalRoot, - isTransactionalSystem + isTransactionalSystem, } from "./utils"; /** @@ -64,8 +64,8 @@ const volume = (properties = {}) => { snapshotsAffectSizes: false, sizeRelevantVolumes: [], adjustByRam: false, - productDefined: false - } + productDefined: false, + }, }; return { ...testVolume, ...properties }; @@ -87,7 +87,7 @@ const sda = { name: "/dev/sda", description: "", size: 1024, - systems : [], + systems: [], udevIds: [], udevPaths: [], }; @@ -104,10 +104,10 @@ const sda1 = { start: 123, encrypted: false, shrinking: { supported: 128 }, - systems : [], + systems: [], udevIds: [], udevPaths: [], - isEFI: false + isEFI: false, }; /** @type {StorageDevice} */ @@ -122,10 +122,10 @@ const sda2 = { start: 1789, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], udevPaths: [], - isEFI: false + isEFI: false, }; sda.partitionTable = { @@ -134,8 +134,8 @@ sda.partitionTable = { unpartitionedSize: 0, unusedSlots: [ { start: 1, size: 1024 }, - { start: 2345, size: 512 } - ] + { start: 2345, size: 512 }, + ], }; /** @type {StorageDevice} */ @@ -145,7 +145,7 @@ const lvmVg = { type: "lvmVg", name: "/dev/vg0", description: "LVM", - size: 512 + size: 512, }; /** @type {StorageDevice} */ @@ -160,9 +160,9 @@ const lvmLv1 = { start: 0, encrypted: false, shrinking: { unsupported: ["Resizing is not supported"] }, - systems : [], + systems: [], udevIds: [], - udevPaths: [] + udevPaths: [], }; lvmVg.logicalVolumes = [lvmLv1]; @@ -209,8 +209,8 @@ describe("deviceChildren", () => { it("returns the partitions and unused slots", () => { const children = deviceChildren(device); expect(children.length).toEqual(4); - device.partitionTable.partitions.forEach(p => expect(children).toContainEqual(p)); - device.partitionTable.unusedSlots.forEach(s => expect(children).toContainEqual(s)); + device.partitionTable.partitions.forEach((p) => expect(children).toContainEqual(p)); + device.partitionTable.unusedSlots.forEach((s) => expect(children).toContainEqual(s)); }); }); @@ -222,7 +222,7 @@ describe("deviceChildren", () => { it("returns the logical volumes", () => { const children = deviceChildren(device); expect(children.length).toEqual(1); - device.logicalVolumes.forEach(l => expect(children).toContainEqual(l)); + device.logicalVolumes.forEach((l) => expect(children).toContainEqual(l)); }); }); @@ -333,7 +333,7 @@ describe("isTransactionalSystem", () => { const volumes = [ volume({ mountPath: "/" }), - volume({ mountPath: "/home", transactional: true }) + volume({ mountPath: "/home", transactional: true }), ]; expect(isTransactionalSystem(volumes)).toBe(false); }); @@ -341,7 +341,7 @@ describe("isTransactionalSystem", () => { it("returns true if volumes includes a transactional root", () => { const volumes = [ volume({ mountPath: "EXT4" }), - volume({ mountPath: "/", transactional: true }) + volume({ mountPath: "/", transactional: true }), ]; expect(isTransactionalSystem(volumes)).toBe(true); }); diff --git a/web/src/components/users/FirstUser.jsx b/web/src/components/users/FirstUser.jsx index 4d16709c20..d0677714c9 100644 --- a/web/src/components/users/FirstUser.jsx +++ b/web/src/components/users/FirstUser.jsx @@ -21,9 +21,9 @@ import React, { useState, useEffect } from "react"; import { Skeleton, Split, Stack } from "@patternfly/react-core"; -import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; +import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { useNavigate } from "react-router-dom"; -import { RowActions, ButtonLink } from '~/components/core'; +import { RowActions, ButtonLink } from "~/components/core"; import { _ } from "~/i18n"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; @@ -35,11 +35,15 @@ const UserNotDefined = ({ actionCb }) => {
      {_("No user defined yet.")}
      - {_("Please, be aware that a user must be defined before installing the system to be able to log into it.")} + {_( + "Please, be aware that a user must be defined before installing the system to be able to log into it.", + )}
      - {_("Define a user now")} + + {_("Define a user now")} + @@ -83,14 +87,14 @@ export default function FirstUser() { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - cancellablePromise(client.users.getUser()).then(userValues => { + cancellablePromise(client.users.getUser()).then((userValues) => { setUser(userValues); setIsLoading(false); }); }, [client.users, cancellablePromise]); useEffect(() => { - return client.users.onUsersChange(changes => { + return client.users.onUsersChange((changes) => { if (changes.firstUser !== undefined) { setUser(changes.firstUser); } @@ -114,13 +118,13 @@ export default function FirstUser() { const actions = [ { title: _("Edit"), - onClick: () => navigate('/users/first/edit') + onClick: () => navigate("/users/first/edit"), }, { title: _("Discard"), onClick: remove, - isDanger: true - } + isDanger: true, + }, ]; if (isLoading) { diff --git a/web/src/components/users/FirstUserForm.jsx b/web/src/components/users/FirstUserForm.jsx index 94c9c36220..9915c2d980 100644 --- a/web/src/components/users/FirstUserForm.jsx +++ b/web/src/components/users/FirstUserForm.jsx @@ -23,22 +23,33 @@ import React, { useState, useEffect, useRef } from "react"; import { Alert, Checkbox, - Form, FormGroup, + Form, + FormGroup, TextInput, - Menu, MenuContent, MenuList, MenuItem, - Grid, GridItem, + Menu, + MenuContent, + MenuList, + MenuItem, + Grid, + GridItem, Stack, - Switch + Switch, } from "@patternfly/react-core"; import { useNavigate } from "react-router-dom"; import { Loading } from "~/components/layout"; -import { PasswordAndConfirmationInput, Page } from '~/components/core'; +import { PasswordAndConfirmationInput, Page } from "~/components/core"; import { _ } from "~/i18n"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; -import { suggestUsernames } from '~/components/users/utils'; +import { suggestUsernames } from "~/components/users/utils"; -const UsernameSuggestions = ({ isOpen = false, entries, onSelect, setInsideDropDown, focusedIndex = -1 }) => { +const UsernameSuggestions = ({ + isOpen = false, + entries, + onSelect, + setInsideDropDown, + focusedIndex = -1, +}) => { if (!isOpen) return; return ( @@ -57,7 +68,7 @@ const UsernameSuggestions = ({ isOpen = false, entries, onSelect, setInsideDropD isFocused={focusedIndex === index} onClick={() => onSelect(suggestion)} > - { /* TRANSLATORS: dropdown username suggestions */} + {/* TRANSLATORS: dropdown username suggestions */} {_("Use suggested username")} {suggestion} ))} @@ -85,12 +96,12 @@ export default function FirstUserForm() { const passwordRef = useRef(); useEffect(() => { - cancellablePromise(client.users.getUser()).then(userValues => { + cancellablePromise(client.users.getUser()).then((userValues) => { const editing = userValues.userName !== ""; setState({ load: true, user: userValues, - isEditing: editing + isEditing: editing, }); setChangePassword(!editing); }); @@ -136,7 +147,7 @@ export default function FirstUserForm() { } // FIXME: improve validations - if (Object.values(user).some(v => v === "")) { + if (Object.values(user).some((v) => v === "")) { setErrors([_("All fields are required")]); return; } @@ -165,24 +176,27 @@ export default function FirstUserForm() { const handleKeyDown = (e) => { switch (e.key) { - case 'ArrowDown': + case "ArrowDown": e.preventDefault(); // Prevent page scrolling renderSuggestions(e); setFocusedIndex((prevIndex) => (prevIndex + 1) % suggestions.length); break; - case 'ArrowUp': + case "ArrowUp": e.preventDefault(); // Prevent page scrolling renderSuggestions(e); - setFocusedIndex((prevIndex) => (prevIndex - (prevIndex === -1 ? 0 : 1) + suggestions.length) % suggestions.length); + setFocusedIndex( + (prevIndex) => + (prevIndex - (prevIndex === -1 ? 0 : 1) + suggestions.length) % suggestions.length, + ); break; - case 'Enter': + case "Enter": if (focusedIndex >= 0) { e.preventDefault(); onSuggestionSelected(suggestions[focusedIndex]); } break; - case 'Escape': - case 'Tab': + case "Escape": + case "Tab": setShowSuggestions(false); break; default: @@ -199,10 +213,13 @@ export default function FirstUserForm() {
      - {errors.length > 0 && + {errors.length > 0 && ( - {errors.map((e, i) =>

      {e}

      )} -
      } + {errors.map((e, i) => ( +

      {e}

      + ))} + + )} @@ -249,12 +266,13 @@ export default function FirstUserForm() { - {state.isEditing && + {state.isEditing && ( setChangePassword(!changePassword)} - />} + /> + )} {
      {_("No root authentication method defined yet.")}
      - {_("Please, define at least one authentication method for logging into the system as root.")} + {_( + "Please, define at least one authentication method for logging into the system as root.", + )}
      {/* TRANSLATORS: push button label */} - + {/* TRANSLATORS: push button label */} - +
      ); @@ -76,7 +82,7 @@ export default function RootAuthMethods() { }, [client, cancellablePromise]); useEffect(() => { - return client.onUsersChange(changes => { + return client.onUsersChange((changes) => { if (changes.rootPasswordSet !== undefined) setIsPasswordDefined(changes.rootPasswordSet); if (changes.rootSSHKey !== undefined) setSSHKey(changes.rootSSHKey); }); @@ -92,27 +98,25 @@ export default function RootAuthMethods() { const passwordActions = [ { title: isPasswordDefined ? _("Change") : _("Set"), - onClick: openPasswordForm - + onClick: openPasswordForm, }, isPasswordDefined && { title: _("Discard"), onClick: () => client.removeRootPassword(), - isDanger: true - } + isDanger: true, + }, ].filter(Boolean); const sshKeyActions = [ { title: isSSHKeyDefined ? _("Change") : _("Set"), - onClick: openSSHKeyForm + onClick: openSSHKeyForm, }, sshKey && { title: _("Discard"), onClick: () => client.setRootSSHKey(""), - isDanger: true - } - + isDanger: true, + }, ].filter(Boolean); if (isLoading) { @@ -125,9 +129,7 @@ export default function RootAuthMethods() { } const PasswordLabel = () => { - return isPasswordDefined - ? _("Already set") - : _("Not set"); + return isPasswordDefined ? _("Already set") : _("Not set"); }; const SSHKeyLabel = () => { @@ -161,14 +163,18 @@ export default function RootAuthMethods() { {_("Password")} - + + + {_("SSH Key")} - + + + @@ -181,20 +187,26 @@ export default function RootAuthMethods() { return ( <> - {isPasswordFormOpen && + {isPasswordFormOpen && ( } + /> + )} - {isSSHKeyFormOpen && + {isSSHKeyFormOpen && ( } + /> + )} ); } diff --git a/web/src/components/users/RootAuthMethods.test.jsx b/web/src/components/users/RootAuthMethods.test.jsx index 0b9af79247..bc41d28d5c 100644 --- a/web/src/components/users/RootAuthMethods.test.jsx +++ b/web/src/components/users/RootAuthMethods.test.jsx @@ -34,7 +34,7 @@ jest.mock("@patternfly/react-core", () => { return { ...original, - Skeleton: () =>
      PFSkeleton
      + Skeleton: () =>
      PFSkeleton
      , }; }); @@ -56,8 +56,8 @@ beforeEach(() => { getRootSSHKey: getRootSSHKeyFn, setRootSSHKey: setRootSSHKeyFn, onUsersChange: onUsersChangeFn, - removeRootPassword: removeRootPasswordFn - } + removeRootPassword: removeRootPasswordFn, + }, }; }); }); @@ -123,8 +123,7 @@ describe("when ready", () => { installerRender(); const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password") - .closest("tr"); + const passwordRow = within(table).getByText("Password").closest("tr"); within(passwordRow).getByText("Already set"); }); @@ -132,8 +131,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password") - .closest("tr"); + const passwordRow = within(table).getByText("Password").closest("tr"); const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const setAction = within(passwordRow).queryByRole("menuitem", { name: "Set" }); @@ -144,8 +142,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password") - .closest("tr"); + const passwordRow = within(table).getByText("Password").closest("tr"); const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const changeAction = await within(passwordRow).queryByRole("menuitem", { name: "Change" }); @@ -158,8 +155,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password") - .closest("tr"); + const passwordRow = within(table).getByText("Password").closest("tr"); const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const discardAction = await within(passwordRow).queryByRole("menuitem", { name: "Discard" }); @@ -177,8 +173,7 @@ describe("when ready", () => { installerRender(); const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password") - .closest("tr"); + const passwordRow = within(table).getByText("Password").closest("tr"); within(passwordRow).getByText("Not set"); }); @@ -186,8 +181,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password") - .closest("tr"); + const passwordRow = within(table).getByText("Password").closest("tr"); const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const setAction = within(passwordRow).getByRole("menuitem", { name: "Set" }); @@ -199,8 +193,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password") - .closest("tr"); + const passwordRow = within(table).getByText("Password").closest("tr"); const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); @@ -219,8 +212,7 @@ describe("when ready", () => { installerRender(); const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key") - .closest("tr"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); within(sshKeyRow).getByText("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+"); within(sshKeyRow).getByText("test@example"); }); @@ -229,8 +221,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key") - .closest("tr"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const setAction = within(sshKeyRow).queryByRole("menuitem", { name: "Set" }); @@ -241,8 +232,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key") - .closest("tr"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const changeAction = await within(sshKeyRow).queryByRole("menuitem", { name: "Change" }); @@ -255,8 +245,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key") - .closest("tr"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const discardAction = await within(sshKeyRow).queryByRole("menuitem", { name: "Discard" }); @@ -274,8 +263,7 @@ describe("when ready", () => { installerRender(); const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key") - .closest("tr"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); within(sshKeyRow).getByText("Not set"); }); @@ -283,8 +271,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key") - .closest("tr"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); const setAction = within(sshKeyRow).getByRole("menuitem", { name: "Set" }); @@ -296,8 +283,7 @@ describe("when ready", () => { const { user } = installerRender(); const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key") - .closest("tr"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); await user.click(actionsToggler); diff --git a/web/src/components/users/RootPasswordPopup.jsx b/web/src/components/users/RootPasswordPopup.jsx index 34b2e6cda1..aa3dc015d0 100644 --- a/web/src/components/users/RootPasswordPopup.jsx +++ b/web/src/components/users/RootPasswordPopup.jsx @@ -21,7 +21,7 @@ import React, { useState } from "react"; import { Form } from "@patternfly/react-core"; -import { PasswordAndConfirmationInput, Popup } from '~/components/core'; +import { PasswordAndConfirmationInput, Popup } from "~/components/core"; import { _ } from "~/i18n"; import { useInstallerClient } from "~/context/installer"; @@ -38,11 +38,7 @@ import { useInstallerClient } from "~/context/installer"; * @param {boolean} props.isOpen - whether the dialog should be visible * @param {function} props.onClose - the function to be called when the dialog is closed */ -export default function RootPasswordPopup({ - title = _("Root password"), - isOpen, - onClose -}) { +export default function RootPasswordPopup({ title = _("Root password"), isOpen, onClose }) { const { users: client } = useInstallerClient(); const [password, setPassword] = useState(""); const [isValidPassword, setIsValidPassword] = useState(true); @@ -74,7 +70,11 @@ export default function RootPasswordPopup({ - + diff --git a/web/src/components/users/RootPasswordPopup.test.jsx b/web/src/components/users/RootPasswordPopup.test.jsx index 90671eeae5..4340e75475 100644 --- a/web/src/components/users/RootPasswordPopup.test.jsx +++ b/web/src/components/users/RootPasswordPopup.test.jsx @@ -38,7 +38,7 @@ beforeEach(() => { return { users: { setRootPassword: setRootPasswordFn, - } + }, }; }); }); diff --git a/web/src/components/users/RootSSHKeyPopup.jsx b/web/src/components/users/RootSSHKeyPopup.jsx index 4b047fa8f3..cd444f5bf4 100644 --- a/web/src/components/users/RootSSHKeyPopup.jsx +++ b/web/src/components/users/RootSSHKeyPopup.jsx @@ -23,7 +23,7 @@ import React, { useState } from "react"; import { Form, FormGroup, FileUpload } from "@patternfly/react-core"; import { _ } from "~/i18n"; -import { Popup } from '~/components/core'; +import { Popup } from "~/components/core"; import { useInstallerClient } from "~/context/installer"; /** @@ -43,7 +43,7 @@ export default function RootSSHKeyPopup({ title = _("Set root SSH public key"), currentKey = "", isOpen, - onClose + onClose, }) { const client = useInstallerClient(); const [isLoading, setIsLoading] = useState(false); diff --git a/web/src/components/users/RootSSHKeyPopup.test.jsx b/web/src/components/users/RootSSHKeyPopup.test.jsx index 124a01c75f..64ed1fd177 100644 --- a/web/src/components/users/RootSSHKeyPopup.test.jsx +++ b/web/src/components/users/RootSSHKeyPopup.test.jsx @@ -37,7 +37,7 @@ beforeEach(() => { return { users: { setRootSSHKey: setRootSSHKeyFn, - } + }, }; }); }); diff --git a/web/src/components/users/routes.js b/web/src/components/users/routes.js index e5ebf086eb..4c108d1f4c 100644 --- a/web/src/components/users/routes.js +++ b/web/src/components/users/routes.js @@ -30,19 +30,19 @@ const routes = { element: , handle: { name: N_("Users"), - icon: "manage_accounts" + icon: "manage_accounts", }, children: [ { index: true, element: }, { path: "first", - element: + element: , }, { path: "first/edit", - element: - } - ] + element: , + }, + ], }; export default routes; diff --git a/web/src/components/users/utils.js b/web/src/components/users/utils.js index b13576d8b8..9f36687099 100644 --- a/web/src/components/users/utils.js +++ b/web/src/components/users/utils.js @@ -32,9 +32,9 @@ const suggestUsernames = (fullName) => { // Cleaning the name. const cleanedName = fullName - .normalize('NFD') + .normalize("NFD") .trim() - .replace(/[\u0300-\u036f]/g, '') // Replacing accented characters with English equivalents, eg. š with s. + .replace(/[\u0300-\u036f]/g, "") // Replacing accented characters with English equivalents, eg. š with s. .replace(/[^\p{L}\p{N} ]/gu, "") // Keep only letters, numbers and spaces. Covering the whole Unicode range, not just ASCII. .toLowerCase(); @@ -42,7 +42,7 @@ const suggestUsernames = (fullName) => { const parts = cleanedName.split(/\s+/); const suggestions = new Set(); - const firstLetters = parts.map(p => p[0]).join(''); + const firstLetters = parts.map((p) => p[0]).join(""); const lastPosition = parts.length - 1; const [firstPart, ...allExceptFirst] = parts; @@ -52,16 +52,16 @@ const suggestUsernames = (fullName) => { // Just the first part of the name suggestions.add(firstPart); // The first letter of the first part plus all other parts - suggestions.add(firstLetter + allExceptFirst.join('')); + suggestions.add(firstLetter + allExceptFirst.join("")); // The first part plus the first letters of all other parts - suggestions.add(firstPart + allExceptFirstLetter.join('')); + suggestions.add(firstPart + allExceptFirstLetter.join("")); // The first letters except the last one plus the last part suggestions.add(firstLetters.substring(0, lastPosition) + lastPart); // All parts without spaces - suggestions.add(parts.join('')); + suggestions.add(parts.join("")); // let's drop suggestions with less than 3 characters - suggestions.forEach(s => { + suggestions.forEach((s) => { if (s.length < 3) suggestions.delete(s); }); @@ -69,6 +69,4 @@ const suggestUsernames = (fullName) => { return [...suggestions]; }; -export { - suggestUsernames -}; +export { suggestUsernames }; diff --git a/web/src/components/users/utils.test.js b/web/src/components/users/utils.test.js index 0c303e2803..5754994ccc 100644 --- a/web/src/components/users/utils.test.js +++ b/web/src/components/users/utils.test.js @@ -23,45 +23,65 @@ import { suggestUsernames } from "./utils"; -describe('suggestUsernames', () => { - test('returns empty collection if fullName not defined', () => { +describe("suggestUsernames", () => { + test("returns empty collection if fullName not defined", () => { expect(suggestUsernames(undefined)).toEqual([]); expect(suggestUsernames(null)).toEqual([]); }); - test('handles basic single name', () => { - expect(suggestUsernames('John')).toEqual(expect.arrayContaining(['john'])); + test("handles basic single name", () => { + expect(suggestUsernames("John")).toEqual(expect.arrayContaining(["john"])); }); - test('handles basic two-part name', () => { - expect(suggestUsernames('John Doe')).toEqual(expect.arrayContaining(['john', 'jdoe', 'johnd', 'johndoe'])); + test("handles basic two-part name", () => { + expect(suggestUsernames("John Doe")).toEqual( + expect.arrayContaining(["john", "jdoe", "johnd", "johndoe"]), + ); }); - test('handles name with middle initial', () => { - expect(suggestUsernames('John Q. Doe')).toEqual(expect.arrayContaining(['john', 'jqdoe', 'johnqd', 'johnqdoe'])); + test("handles name with middle initial", () => { + expect(suggestUsernames("John Q. Doe")).toEqual( + expect.arrayContaining(["john", "jqdoe", "johnqd", "johnqdoe"]), + ); }); - test('normalizes accented characters', () => { - expect(suggestUsernames('José María')).toEqual(expect.arrayContaining(['jose', 'jmaria', 'josem', 'josemaria'])); + test("normalizes accented characters", () => { + expect(suggestUsernames("José María")).toEqual( + expect.arrayContaining(["jose", "jmaria", "josem", "josemaria"]), + ); }); - test('removes hyphens and apostrophes', () => { - expect(suggestUsernames("Jean-Luc O'Neill")).toEqual(expect.arrayContaining(['jeanluc', 'joneill', 'jeanluco', 'jeanluconeill'])); + test("removes hyphens and apostrophes", () => { + expect(suggestUsernames("Jean-Luc O'Neill")).toEqual( + expect.arrayContaining(["jeanluc", "joneill", "jeanluco", "jeanluconeill"]), + ); }); - test('removes non-alphanumeric characters', () => { - expect(suggestUsernames("Anna*#& Maria$%^")).toEqual(expect.arrayContaining(['anna', 'amaria', 'annam', 'annamaria'])); + test("removes non-alphanumeric characters", () => { + expect(suggestUsernames("Anna*#& Maria$%^")).toEqual( + expect.arrayContaining(["anna", "amaria", "annam", "annamaria"]), + ); }); - test('handles long name with multiple parts', () => { - expect(suggestUsernames("Maria del Carmen Fernandez Vega")).toEqual(expect.arrayContaining(['maria', 'mdelcarmenfernandezvega', 'mariadcfv', 'mdcfvega', 'mariadelcarmenfernandezvega'])); + test("handles long name with multiple parts", () => { + expect(suggestUsernames("Maria del Carmen Fernandez Vega")).toEqual( + expect.arrayContaining([ + "maria", + "mdelcarmenfernandezvega", + "mariadcfv", + "mdcfvega", + "mariadelcarmenfernandezvega", + ]), + ); }); - test('handles empty or invalid input', () => { + test("handles empty or invalid input", () => { expect(suggestUsernames("")).toEqual(expect.arrayContaining([])); }); - test('trims spaces and handles multiple spaces between names', () => { - expect(suggestUsernames(" John Doe ")).toEqual(expect.arrayContaining(['john', 'jdoe', 'johnd', 'johndoe'])); + test("trims spaces and handles multiple spaces between names", () => { + expect(suggestUsernames(" John Doe ")).toEqual( + expect.arrayContaining(["john", "jdoe", "johnd", "johndoe"]), + ); }); }); diff --git a/web/src/context/app.jsx b/web/src/context/app.jsx index 704e03339d..121e07e5e8 100644 --- a/web/src/context/app.jsx +++ b/web/src/context/app.jsx @@ -40,9 +40,7 @@ function AppProviders({ children }) { - - {children} - + {children} diff --git a/web/src/context/auth.jsx b/web/src/context/auth.jsx index 90cdde5933..caf7daefd5 100644 --- a/web/src/context/auth.jsx +++ b/web/src/context/auth.jsx @@ -40,7 +40,7 @@ function useAuth() { const AuthErrors = Object.freeze({ SERVER: "server", AUTH: "auth", - OTHER: "other" + OTHER: "other", }); /** @@ -59,10 +59,10 @@ function AuthProvider({ children }) { }); const result = response.status === 200; - if ((response.status >= 500) && (response.status < 600)) { + if (response.status >= 500 && response.status < 600) { setError(AuthErrors.SERVER); } - if ((response.status >= 400) && (response.status < 500)) { + if (response.status >= 400 && response.status < 500) { setError(AuthErrors.AUTH); } setIsLoggedIn(result); @@ -83,10 +83,10 @@ function AuthProvider({ children }) { }) .then((response) => { setIsLoggedIn(response.status === 200); - if ((response.status >= 500) && (response.status < 600)) { + if (response.status >= 500 && response.status < 600) { setError(AuthErrors.SERVER); } - if ((response.status >= 400) && (response.status < 500)) { + if (response.status >= 400 && response.status < 500) { setError(AuthErrors.AUTH); } }) diff --git a/web/src/context/installer.jsx b/web/src/context/installer.jsx index fd4f063a3b..0b4b4c8246 100644 --- a/web/src/context/installer.jsx +++ b/web/src/context/installer.jsx @@ -28,7 +28,10 @@ const InstallerClientContext = React.createContext(null); // TODO: we use a separate context to avoid changing all the codes to // `useInstallerClient`. We should merge them in the future. const InstallerClientStatusContext = React.createContext({ - connected: false, error: false, phase: undefined, status: undefined + connected: false, + error: false, + phase: undefined, + status: undefined, }); /** @@ -65,17 +68,15 @@ function useInstallerClientStatus() { } /** - * @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 -}) { + * @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 }) { const [value, setValue] = useState(client); const [connected, setConnected] = useState(false); const [error, setError] = useState(false); @@ -91,7 +92,7 @@ function InstallerClientProvider({ // allow hot replacement for the clients code if (module.hot) { // if anything coming from `import ... from "~/client"` is updated then this hook is called - module.hot.accept("~/client", async function() { + module.hot.accept("~/client", async function () { console.log("[Agama HMR] A client module has been updated"); const updated_client = await createDefaultClient(); @@ -151,8 +152,4 @@ function InstallerClientProvider({ ); } -export { - InstallerClientProvider, - useInstallerClient, - useInstallerClientStatus -}; +export { InstallerClientProvider, useInstallerClient, useInstallerClientStatus }; diff --git a/web/src/context/installer.test.jsx b/web/src/context/installer.test.jsx index ef84f406c6..0e025f2a82 100644 --- a/web/src/context/installer.test.jsx +++ b/web/src/context/installer.test.jsx @@ -52,8 +52,8 @@ describe("installer context", () => { getPhase: jest.fn().mockResolvedValue(STARTUP), getStatus: jest.fn().mockResolvedValue(BUSY), onPhaseChange: jest.fn(), - onStatusChange: jest.fn() - } + onStatusChange: jest.fn(), + }, }; }); }); @@ -62,7 +62,7 @@ describe("installer context", () => { plainRender( - + , ); await screen.findByText("connected: false"); await screen.findByText("phase: 0"); diff --git a/web/src/context/installerL10n.jsx b/web/src/context/installerL10n.jsx index 6849e523f1..08df7fda73 100644 --- a/web/src/context/installerL10n.jsx +++ b/web/src/context/installerL10n.jsx @@ -62,7 +62,9 @@ function useInstallerL10n() { function agamaLanguage() { // language from cookie, empty string if not set (regexp taken from Cockpit) // https://github.com/cockpit-project/cockpit/blob/98a2e093c42ea8cd2431cf15c7ca0e44bb4ce3f1/pkg/shell/shell-modals.jsx#L91 - const languageString = decodeURIComponent(document.cookie.replace(/(?:(?:^|.*;\s*)agamaLang\s*=\s*([^;]*).*$)|^.*$/, "$1")); + const languageString = decodeURIComponent( + document.cookie.replace(/(?:(?:^|.*;\s*)agamaLang\s*=\s*([^;]*).*$)|^.*$/, "$1"), + ); if (languageString) { return languageString.toLowerCase(); } @@ -81,12 +83,16 @@ function storeAgamaLanguage(language) { if (current === language) return false; // Code taken from Cockpit. - const cookie = "agamaLang=" + encodeURIComponent(language) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; + const cookie = + "agamaLang=" + encodeURIComponent(language) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; document.cookie = cookie; // for backward compatibility, CockpitLang cookie is needed to load correct po.js content from Cockpit // TODO: remove after dropping Cockpit completely - const cockpit_cookie = "CockpitLang=" + encodeURIComponent(language) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; + const cockpit_cookie = + "CockpitLang=" + + encodeURIComponent(language) + + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; document.cookie = cockpit_cookie; window.localStorage.setItem("cockpit.lang", language); @@ -101,11 +107,11 @@ function storeAgamaLanguage(language) { * @return {string|undefined} Undefined if not set. */ function languageFromQuery() { - const lang = (new URLSearchParams(window.location.search)).get("lang"); + const lang = new URLSearchParams(window.location.search).get("lang"); if (!lang) return undefined; const [language, country] = lang.toLowerCase().split(/[-_]/); - return (country) ? `${language}-${country}` : language; + return country ? `${language}-${country}` : language; } /** @@ -137,7 +143,7 @@ function languageFromLocale(locale) { */ function languageToLocale(language) { const [lang, country] = language.split("-"); - const locale = (country) ? `${lang}_${country.toUpperCase()}` : lang; + const locale = country ? `${lang}_${country.toUpperCase()}` : lang; return `${locale}.UTF-8`; } @@ -147,7 +153,7 @@ function languageToLocale(language) { * @return {Array} */ function navigatorLanguages() { - return navigator.languages.map(l => l.toLowerCase()); + return navigator.languages.map((l) => l.toLowerCase()); } /** @@ -162,7 +168,7 @@ function findSupportedLanguage(languages) { for (const candidate of languages) { const [language, country] = candidate.split("-"); - const match = supported.find(s => { + const match = supported.find((s) => { const [supportedLanguage, supportedCountry] = s.split("-"); if (language === supportedLanguage) { return country === undefined || country === supportedCountry; @@ -215,53 +221,62 @@ function InstallerL10nProvider({ children }) { const [backendPending, setBackendPending] = useState(false); const { cancellablePromise } = useCancellablePromise(); - const storeInstallerLanguage = useCallback(async (newLanguage) => { - if (!client) { - setBackendPending(true); - return false; - } + const storeInstallerLanguage = useCallback( + async (newLanguage) => { + if (!client) { + setBackendPending(true); + return false; + } - const locale = await cancellablePromise(client.l10n.getUILocale()); - const currentLanguage = languageFromLocale(locale); + const locale = await cancellablePromise(client.l10n.getUILocale()); + const currentLanguage = languageFromLocale(locale); - if (currentLanguage !== newLanguage) { - // FIXME: fallback to en-US if the language is not supported. - await cancellablePromise(client.l10n.setUILocale(languageToLocale(newLanguage))); - return true; - } + if (currentLanguage !== newLanguage) { + // FIXME: fallback to en-US if the language is not supported. + await cancellablePromise(client.l10n.setUILocale(languageToLocale(newLanguage))); + return true; + } - return false; - }, [client, cancellablePromise]); + return false; + }, + [client, cancellablePromise], + ); - const changeLanguage = useCallback(async (lang) => { - const wanted = lang || languageFromQuery(); + const changeLanguage = useCallback( + async (lang) => { + const wanted = lang || languageFromQuery(); - if (wanted === "xx" || wanted === "xx-xx") { - agama.language = wanted; - setLanguage(wanted); - return; - } + if (wanted === "xx" || wanted === "xx-xx") { + agama.language = wanted; + setLanguage(wanted); + return; + } - const current = agamaLanguage(); - const candidateLanguages = [wanted, current].concat(navigatorLanguages()).filter(l => l); - const newLanguage = findSupportedLanguage(candidateLanguages) || "en-us"; + const current = agamaLanguage(); + const candidateLanguages = [wanted, current].concat(navigatorLanguages()).filter((l) => l); + const newLanguage = findSupportedLanguage(candidateLanguages) || "en-us"; - let mustReload = storeAgamaLanguage(newLanguage); - mustReload = await storeInstallerLanguage(newLanguage) || mustReload; + let mustReload = storeAgamaLanguage(newLanguage); + mustReload = (await storeInstallerLanguage(newLanguage)) || mustReload; - if (mustReload) { - reload(newLanguage); - } else { - setLanguage(newLanguage); - } - }, [storeInstallerLanguage, setLanguage]); + if (mustReload) { + reload(newLanguage); + } else { + setLanguage(newLanguage); + } + }, + [storeInstallerLanguage, setLanguage], + ); - const changeKeymap = useCallback(async (id) => { - if (!client) return; + const changeKeymap = useCallback( + async (id) => { + if (!client) return; - setKeymap(id); - client.l10n.setUIKeymap(id); - }, [setKeymap, client]); + setKeymap(id); + client.l10n.setUIKeymap(id); + }, + [setKeymap, client], + ); useEffect(() => { if (!language) changeLanguage(); @@ -281,12 +296,7 @@ function InstallerL10nProvider({ children }) { const value = { language, changeLanguage, keymap, changeKeymap }; - return ( - {children} - ); + return {children}; } -export { - InstallerL10nProvider, - useInstallerL10n -}; +export { InstallerL10nProvider, useInstallerL10n }; diff --git a/web/src/context/installerL10n.test.jsx b/web/src/context/installerL10n.test.jsx index 1808b398a6..7b47d3f140 100644 --- a/web/src/context/installerL10n.test.jsx +++ b/web/src/context/installerL10n.test.jsx @@ -39,7 +39,7 @@ const client = { getPhase: jest.fn(), getStatus: jest.fn(), onPhaseChange: jest.fn(), - onStatusChange: jest.fn() + onStatusChange: jest.fn(), }, l10n: { getUILocale: getUILocaleFn, diff --git a/web/src/context/root.jsx b/web/src/context/root.jsx index dc2f4eb170..c8ce648267 100644 --- a/web/src/context/root.jsx +++ b/web/src/context/root.jsx @@ -31,11 +31,7 @@ import { AuthProvider } from "./auth"; * @param {React.ReactNode} [props.children] - content to display within the provider. */ function RootProviders({ children }) { - return ( - - {children} - - ); + return {children}; } export { RootProviders }; diff --git a/web/src/hooks/useNodeSiblings.js b/web/src/hooks/useNodeSiblings.js index f95d5b6074..cf98a42d5e 100644 --- a/web/src/hooks/useNodeSiblings.js +++ b/web/src/hooks/useNodeSiblings.js @@ -28,16 +28,16 @@ import { noop } from "~/utils"; const useNodeSiblings = (node) => { if (!node) return [noop, noop]; - const siblings = [...node.parentNode.children].filter(n => n !== node); + const siblings = [...node.parentNode.children].filter((n) => n !== node); const addAttribute = (attribute, value) => { - siblings.forEach(sibling => { + siblings.forEach((sibling) => { sibling.setAttribute(attribute, value); }); }; const removeAttribute = (attribute) => { - siblings.forEach(sibling => { + siblings.forEach((sibling) => { sibling.removeAttribute(attribute); }); }; diff --git a/web/src/hooks/useNodeSiblings.test.js b/web/src/hooks/useNodeSiblings.test.js index c668d67e37..b2a569f845 100644 --- a/web/src/hooks/useNodeSiblings.test.js +++ b/web/src/hooks/useNodeSiblings.test.js @@ -1,5 +1,5 @@ -import { renderHook } from '@testing-library/react'; -import useNodeSiblings from './useNodeSiblings'; +import { renderHook } from "@testing-library/react"; +import useNodeSiblings from "./useNodeSiblings"; // Mocked HTMLElement for testing const mockNode = { @@ -12,8 +12,8 @@ const mockNode = { }, }; -describe('useNodeSiblings', () => { - it('should return noop functions when node is not provided', () => { +describe("useNodeSiblings", () => { + it("should return noop functions when node is not provided", () => { const { result } = renderHook(() => useNodeSiblings(null)); const [addAttribute, removeAttribute] = result.current; @@ -23,27 +23,36 @@ describe('useNodeSiblings', () => { 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(); + expect(() => addAttribute("attribute", "value")).not.toThrow(); + expect(() => removeAttribute("attribute")).not.toThrow(); }); - it('should add attribute to all siblings when addAttribute is called', () => { + 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'; + 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); + 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', () => { + it("should remove attribute from all siblings when removeAttribute is called", () => { const { result } = renderHook(() => useNodeSiblings(mockNode)); const [, removeAttribute] = result.current; - const attributeName = 'attribute'; + const attributeName = "attribute"; removeAttribute(attributeName); diff --git a/web/src/i18n.js b/web/src/i18n.js index ac78dd7f5c..3ae1d68095 100644 --- a/web/src/i18n.js +++ b/web/src/i18n.js @@ -72,7 +72,7 @@ const xTranslate = (str) => { * @param {string} str the input string to translate * @return {string} translated or original text */ -const _ = (str) => isTestingLanguage() ? xTranslate(str) : agama.gettext(str); +const _ = (str) => (isTestingLanguage() ? xTranslate(str) : agama.gettext(str)); /** * Similar to the _() function. This variant returns singular or plural form @@ -86,9 +86,7 @@ const _ = (str) => isTestingLanguage() ? xTranslate(str) : agama.gettext(str); * @return {string} translated or original text */ const n_ = (str1, strN, n) => { - return isTestingLanguage() - ? xTranslate((n === 1) ? str1 : strN) - : agama.ngettext(str1, strN, n); + return isTestingLanguage() ? xTranslate(n === 1 ? str1 : strN) : agama.ngettext(str1, strN, n); }; /** @@ -137,11 +135,6 @@ const N_ = (str) => str; * @return {string} the original text, either "string1" or "stringN" depending * on the value "num" */ -const Nn_ = (str1, strN, n) => (n === 1) ? str1 : strN; +const Nn_ = (str1, strN, n) => (n === 1 ? str1 : strN); -export { - _, - n_, - N_, - Nn_ -}; +export { _, n_, N_, Nn_ }; diff --git a/web/src/index.html b/web/src/index.html index 918d6d681b..138aa7d9d1 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -1,9 +1,9 @@ - + - + Agama diff --git a/web/src/index.js b/web/src/index.js index 0d487303f7..f9028aa93d 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -50,5 +50,5 @@ const root = createRoot(container); root.render( - + , ); diff --git a/web/src/languages.json b/web/src/languages.json index 8c64959024..d406f186b0 100644 --- a/web/src/languages.json +++ b/web/src/languages.json @@ -1,12 +1,12 @@ { - "ca-es": "Català", - "de-de": "Deutsch", - "en-us": "English", - "es-es": "Español", - "ja-jp": "日本語", - "nb-NO": "Norsk bokmål", - "pt-BR": "Português", - "ru-ru": "Русский", - "sv-se": "Svenska", - "zh-Hans": "中文" -} \ No newline at end of file + "ca-es": "Català", + "de-de": "Deutsch", + "en-us": "English", + "es-es": "Español", + "ja-jp": "日本語", + "nb-NO": "Norsk bokmål", + "pt-BR": "Português", + "ru-ru": "Русский", + "sv-se": "Svenska", + "zh-Hans": "中文" +} diff --git a/web/src/queries/l10n.ts b/web/src/queries/l10n.ts index 2dd5ee7fda..525178a93d 100644 --- a/web/src/queries/l10n.ts +++ b/web/src/queries/l10n.ts @@ -30,7 +30,7 @@ import { timezoneUTCOffset } from "~/utils"; const configQuery = () => { return { queryKey: ["l10n/config"], - queryFn: () => fetch("/api/l10n/config").then(res => res.json()) + queryFn: () => fetch("/api/l10n/config").then((res) => res.json()), }; }; @@ -46,7 +46,7 @@ const localesQuery = () => ({ return { id, name: language, territory }; }); }, - staleTime: Infinity + staleTime: Infinity, }); /** @@ -62,7 +62,7 @@ const timezonesQuery = () => ({ return { id: code, parts, country, utcOffset: offset }; }); }, - staleTime: Infinity + staleTime: Infinity, }); /** @@ -78,7 +78,7 @@ const keymapsQuery = () => ({ }); return keymaps.sort((a, b) => (a.name < b.name ? -1 : 1)); }, - staleTime: Infinity + staleTime: Infinity, }); /** @@ -88,14 +88,14 @@ const keymapsQuery = () => ({ */ const useConfigMutation = () => { const query = { - mutationFn: newConfig => + mutationFn: (newConfig) => fetch("/api/l10n/config", { method: "PATCH", body: JSON.stringify(newConfig), headers: { - "Content-Type": "application/json" - } - }) + "Content-Type": "application/json", + }, + }), }; return useMutation(query); }; @@ -113,7 +113,7 @@ const useL10nConfigChanges = () => { React.useEffect(() => { if (!client) return; - return client.ws().onEvent(event => { + return client.ws().onEvent((event) => { if (event.type === "L10nConfigChanged") { queryClient.invalidateQueries({ queryKey: ["l10n/config"] }); } @@ -125,12 +125,12 @@ const useL10nConfigChanges = () => { const useL10n = () => { const [{ data: config }, { data: locales }, { data: keymaps }, { data: timezones }] = useSuspenseQueries({ - queries: [configQuery(), localesQuery(), keymapsQuery(), timezonesQuery()] + queries: [configQuery(), localesQuery(), keymapsQuery(), timezonesQuery()], }); - const selectedLocale = locales.find(l => l.id === config.locales[0]); - const selectedKeymap = keymaps.find(k => k.id === config.keymap); - const selectedTimezone = timezones.find(t => t.id === config.timezone); + const selectedLocale = locales.find((l) => l.id === config.locales[0]); + const selectedKeymap = keymaps.find((k) => k.id === config.keymap); + const selectedTimezone = timezones.find((t) => t.id === config.timezone); return { locales, @@ -138,7 +138,7 @@ const useL10n = () => { timezones, selectedLocale, selectedKeymap, - selectedTimezone + selectedTimezone, }; }; @@ -149,5 +149,5 @@ export { timezonesQuery, useConfigMutation, useL10n, - useL10nConfigChanges + useL10nConfigChanges, }; diff --git a/web/src/queries/software.js b/web/src/queries/software.js index cb2ef253dc..a902d10ad7 100644 --- a/web/src/queries/software.js +++ b/web/src/queries/software.js @@ -24,19 +24,19 @@ import { QueryClient, useMutation, useQueryClient, - useSuspenseQueries + useSuspenseQueries, } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; const configQuery = () => ({ queryKey: ["software/config"], - queryFn: () => fetch("/api/software/config").then(res => res.json()) + queryFn: () => fetch("/api/software/config").then((res) => res.json()), }); const productsQuery = () => ({ queryKey: ["software/products"], - queryFn: () => fetch("/api/software/products").then(res => res.json()), - staleTime: Infinity + queryFn: () => fetch("/api/software/products").then((res) => res.json()), + staleTime: Infinity, }); /** @@ -49,19 +49,19 @@ const useConfigMutation = () => { const client = useInstallerClient(); const query = { - mutationFn: newConfig => + mutationFn: (newConfig) => fetch("/api/software/config", { // FIXME: use "PATCH" instead method: "PUT", body: JSON.stringify(newConfig), headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["software/config"] }); client.manager.startProbing(); - } + }, }; return useMutation(query); }; @@ -79,7 +79,7 @@ const useProductChanges = () => { if (!client) return; const queryClient = new QueryClient(); - return client.ws().onEvent(event => { + return client.ws().onEvent((event) => { if (event.type === "ProductChanged") { queryClient.invalidateQueries({ queryKey: ["software/config"] }); } @@ -89,13 +89,13 @@ const useProductChanges = () => { const useProduct = () => { const [{ data: config }, { data: products }] = useSuspenseQueries({ - queries: [configQuery(), productsQuery()] + queries: [configQuery(), productsQuery()], }); - const selectedProduct = products.find(p => p.id === config.product); + const selectedProduct = products.find((p) => p.id === config.product); return { products, - selectedProduct + selectedProduct, }; }; diff --git a/web/src/router.js b/web/src/router.js index a4b6732b5f..333f28e83f 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -42,7 +42,7 @@ const rootRoutes = [ networkRoutes, storageRoutes, softwareRoutes, - usersRoutes + usersRoutes, ]; const protectedRoutes = [ @@ -55,17 +55,17 @@ const protectedRoutes = [ children: [ { index: true, - element: + element: , }, - ...rootRoutes - ] + ...rootRoutes, + ], }, { element: , - children: [productsRoutes] - } - ] - } + children: [productsRoutes], + }, + ], + }, ]; const routes = [ @@ -76,20 +76,17 @@ const routes = [ children: [ { index: true, - element: - } - ] + element: , + }, + ], }, { path: "/", element: , - children: [...protectedRoutes] - } + children: [...protectedRoutes], + }, ]; const router = createHashRouter(routes); -export { - router, - rootRoutes -}; +export { router, rootRoutes }; diff --git a/web/src/routes/l10n.js b/web/src/routes/l10n.js index 3e0df54833..17f9a2a919 100644 --- a/web/src/routes/l10n.js +++ b/web/src/routes/l10n.js @@ -23,12 +23,7 @@ import React from "react"; import { Page } from "~/components/core"; import { L10nPage, LocaleSelection, KeymapSelection, TimezoneSelection } from "~/components/l10n"; import { queryClient } from "~/context/app"; -import { - configQuery, - localesQuery, - keymapsQuery, - timezonesQuery, -} from "~/queries/l10n"; +import { configQuery, localesQuery, keymapsQuery, timezonesQuery } from "~/queries/l10n"; import { N_ } from "~/i18n"; const L10N_PATH = "/l10n"; @@ -41,12 +36,12 @@ const routes = { element: , handle: { name: N_("Localization"), - icon: "globe" + icon: "globe", }, children: [ { index: true, - element: + element: , }, { path: LOCALE_SELECTION_PATH, @@ -59,14 +54,9 @@ const routes = { { path: TIMEZONE_SELECTION_PATH, element: , - } - ] + }, + ], }; export default routes; -export { - L10N_PATH, - LOCALE_SELECTION_PATH, - KEYMAP_SELECTION_PATH, - TIMEZONE_SELECTION_PATH -}; +export { L10N_PATH, LOCALE_SELECTION_PATH, KEYMAP_SELECTION_PATH, TIMEZONE_SELECTION_PATH }; diff --git a/web/src/routes/products.js b/web/src/routes/products.js index 9ddb6eef94..0b30ce962c 100644 --- a/web/src/routes/products.js +++ b/web/src/routes/products.js @@ -32,13 +32,13 @@ const productsRoutes = { children: [ { index: true, - element: + element: , }, { path: "progress", - element: - } - ] + element: , + }, + ], }; export default productsRoutes; diff --git a/web/src/test-utils.js b/web/src/test-utils.js index 7aaa06f560..8452491f88 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -72,12 +72,12 @@ const mockUseRevalidator = jest.fn(); const mockRoutes = (...routes) => initialRoutes.mockReturnValueOnce(routes); // Centralize the react-router-dom mock here -jest.mock('react-router-dom', () => ({ +jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), useNavigate: () => mockNavigateFn, Navigate: ({ to: route }) => <>Navigating to {route}, Outlet: () => <>Outlet Content, - useRevalidator: () => mockUseRevalidator + useRevalidator: () => mockUseRevalidator, })); const Providers = ({ children, withL10n }) => { @@ -102,24 +102,18 @@ const Providers = ({ children, withL10n }) => { getStatus: noop, onPhaseChange: noop, onStatusChange: noop, - ...client.manager + ...client.manager, }; if (withL10n) { return ( - - {children} - + {children} ); } - return ( - - {children} - - ); + return {children}; }; /** @@ -134,19 +128,15 @@ const installerRender = (ui, options = {}) => { const Wrapper = ({ children }) => ( - - {children} - + {children} ); - return ( - { - user: userEvent.setup(), - ...render(ui, { wrapper: Wrapper, ...options }) - } - ); + return { + user: userEvent.setup(), + ...render(ui, { wrapper: Wrapper, ...options }), + }; }; /** @@ -165,16 +155,12 @@ const plainRender = (ui, options = {}) => { const queryClient = new QueryClient({}); const Wrapper = ({ children }) => ( - - {children} - - ); - return ( - { - user: userEvent.setup(), - ...render(ui, { wrapper: Wrapper, ...options }) - } + {children} ); + return { + user: userEvent.setup(), + ...render(ui, { wrapper: Wrapper, ...options }), + }; }; /** @@ -234,5 +220,5 @@ export { mockNavigateFn, mockRoutes, mockUseRevalidator, - resetLocalStorage + resetLocalStorage, }; diff --git a/web/src/test-utils.test.js b/web/src/test-utils.test.js index a76ccfb583..c5670616d6 100644 --- a/web/src/test-utils.test.js +++ b/web/src/test-utils.test.js @@ -19,9 +19,7 @@ * find current contact information at www.suse.com. */ -import { - resetLocalStorage -} from "./test-utils"; +import { resetLocalStorage } from "./test-utils"; beforeAll(() => { jest.spyOn(Storage.prototype, "clear"); @@ -49,7 +47,7 @@ describe("resetLocalStorage", () => { it("sets an initial state if given value is an object", () => { resetLocalStorage({ storage: "something", - for: "later" + for: "later", }); expect(window.localStorage.setItem).toHaveBeenCalledWith("storage", "something"); expect(window.localStorage.setItem).toHaveBeenCalledWith("for", "later"); diff --git a/web/src/utils.js b/web/src/utils.js index 938e6af327..dd90e6cbd3 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -30,15 +30,14 @@ import { useEffect, useRef, useCallback, useState } from "react"; * @param {any} value - the value to be checked * @return {boolean} true when given value is an object; false otherwise */ -const isObject = (value) => ( - typeof value === 'object' && - value !== null && - !Array.isArray(value) && - !(value instanceof RegExp) && - !(value instanceof Date) && - !(value instanceof Set) && - !(value instanceof Map) -); +const isObject = (value) => + typeof value === "object" && + value !== null && + !Array.isArray(value) && + !(value instanceof RegExp) && + !(value instanceof Date) && + !(value instanceof Set) && + !(value instanceof Map); /** * Returns an empty function useful to be used as a default callback. @@ -64,7 +63,7 @@ const partition = (collection, filter) => { const pass = []; const fail = []; - collection.forEach(element => { + collection.forEach((element) => { filter(element) ? pass.push(element) : fail.push(element); }); @@ -79,7 +78,7 @@ const partition = (collection, filter) => { * @returns {Array} */ function compact(collection) { - return collection.filter(e => e !== null && e !== undefined); + return collection.filter((e) => e !== null && e !== undefined); } /** @@ -106,7 +105,7 @@ function uniq(collection) { * @returns {String} - CSS classes joined together after ignoring falsy values */ function classNames(...classes) { - return classes.filter((item) => !!item).join(' '); + return classes.filter((item) => !!item).join(" "); } /** @@ -128,15 +127,15 @@ function makeCancellable(promise) { const cancellablePromise = new Promise((resolve, reject) => { promise - .then((value) => (!isCanceled && resolve(value))) - .catch((error) => (!isCanceled && reject(error))); + .then((value) => !isCanceled && resolve(value)) + .catch((error) => !isCanceled && reject(error)); }); return { promise: cancellablePromise, cancel() { isCanceled = true; - } + }, }; } @@ -175,7 +174,7 @@ function useCancellablePromise() { promises.current = []; return () => { - promises.current.forEach(p => p.cancel()); + promises.current.forEach((p) => p.cancel()); promises.current = []; }; }, []); @@ -197,9 +196,7 @@ function useCancellablePromise() { * @param {*} fallbackState */ const useLocalStorage = (storageKey, fallbackState) => { - const [value, setValue] = useState( - JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState - ); + const [value, setValue] = useState(JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState); useEffect(() => { localStorage.setItem(storageKey, JSON.stringify(value)); @@ -338,10 +335,11 @@ const remoteConnection = (...args) => !localConnection(...args); */ const timezoneTime = (timezone, { date = new Date() }) => { try { - const formatter = new Intl.DateTimeFormat( - "en-US", - { timeZone: timezone, timeStyle: "short", hour12: false } - ); + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + timeStyle: "short", + hour12: false, + }); return formatter.format(date); } catch (e) { @@ -360,11 +358,11 @@ const timezoneTime = (timezone, { date = new Date() }) => { const timezoneUTCOffset = (timezone) => { try { const date = new Date(); - const dateLocaleString = date.toLocaleString( - "en-US", - { timeZone: timezone, timeZoneName: "short" } - ); - const [timezoneName] = dateLocaleString.split(' ').slice(-1); + const dateLocaleString = date.toLocaleString("en-US", { + timeZone: timezone, + timeZoneName: "short", + }); + const [timezoneName] = dateLocaleString.split(" ").slice(-1); const dateString = date.toString(); const offset = Date.parse(`${dateString} UTC`) - Date.parse(`${dateString} ${timezoneName}`); @@ -394,5 +392,5 @@ export { localConnection, remoteConnection, timezoneTime, - timezoneUTCOffset + timezoneUTCOffset, }; diff --git a/web/src/utils.test.js b/web/src/utils.test.js index 5d340828f8..e9f5671de8 100644 --- a/web/src/utils.test.js +++ b/web/src/utils.test.js @@ -20,8 +20,15 @@ */ import { - classNames, partition, compact, uniq, noop, toValidationError, - localConnection, remoteConnection, isObject + classNames, + partition, + compact, + uniq, + noop, + toValidationError, + localConnection, + remoteConnection, + isObject, } from "./utils"; describe("noop", () => { @@ -34,7 +41,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); expect(odd).toEqual([1, 3, 5]); expect(even).toEqual([2, 4, 6]); @@ -44,28 +51,38 @@ describe("partition", () => { describe("compact", () => { it("removes null and undefined values", () => { expect(compact([])).toEqual([]); - expect(compact([undefined, null, "", 0, 1, NaN, false, true])) - .toEqual(["", 0, 1, NaN, false, true]); + expect(compact([undefined, null, "", 0, 1, NaN, false, true])).toEqual([ + "", + 0, + 1, + NaN, + false, + true, + ]); }); }); describe("uniq", () => { it("removes duplicated values", () => { expect(uniq([])).toEqual([]); - expect(uniq([undefined, null, null, 0, 1, NaN, false, true, false, "test"])) - .toEqual([undefined, null, 0, 1, NaN, false, true, "test"]); + expect(uniq([undefined, null, null, 0, 1, NaN, false, true, false, "test"])).toEqual([ + undefined, + null, + 0, + 1, + NaN, + false, + true, + "test", + ]); }); }); describe("classNames", () => { it("join given arguments, ignoring falsy values", () => { - expect(classNames( - "bg-yellow", - false && "h-24", - undefined, - null, - true && "w-24", - )).toEqual("bg-yellow w-24"); + expect(classNames("bg-yellow", false && "h-24", undefined, null, true && "w-24")).toEqual( + "bg-yellow w-24", + ); }); }); @@ -75,7 +92,7 @@ describe("toValidationError", () => { description: "Issue 1", details: "Details issue 1", source: "config", - severity: "warn" + severity: "warn", }; expect(toValidationError(issue)).toEqual({ message: "Issue 1" }); }); @@ -159,9 +176,7 @@ describe("isObject", () => { }); it("returns false when called with a map", () => { - const map = new Map([ - ["dummy", "map"] - ]); + const map = new Map([["dummy", "map"]]); expect(isObject(map)).toBe(false); }); }); diff --git a/web/svgo.config.js b/web/svgo.config.js index c77334001a..c6ea4118a4 100644 --- a/web/svgo.config.js +++ b/web/svgo.config.js @@ -1,8 +1,8 @@ module.exports = { plugins: [ { - name: 'removeViewBox', - active: false - } - ] + name: "removeViewBox", + active: false, + }, + ], }; diff --git a/web/tsconfig.json b/web/tsconfig.json index bde7ef8c06..fc69f759e1 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -10,18 +10,10 @@ "jsx": "react", "allowSyntheticDefaultImports": true, "paths": { - "~/*": [ - "src/*" - ], - "~/client": [ - "src/client/index.js" - ], - "@icons/*": [ - "node_modules/@material-symbols/svg-400/outlined/*" - ] + "~/*": ["src/*"], + "~/client": ["src/client/index.js"], + "@icons/*": ["node_modules/@material-symbols/svg-400/outlined/*"] } }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/web/typedoc.json b/web/typedoc.json index c93eacf7cd..7d0b6cc273 100644 --- a/web/typedoc.json +++ b/web/typedoc.json @@ -2,24 +2,13 @@ "$schema": "https://typedoc.org/schema.json", "name": "Agama Installer", "entryPointStrategy": "expand", - "exclude": [ - "./src/lib" - ], + "exclude": ["./src/lib"], "excludeNotDocumented": true, - "plugin": [ - "typedoc-plugin-external-module-map", - "typedoc-plugin-merge-modules", - ], + "plugin": ["typedoc-plugin-external-module-map", "typedoc-plugin-merge-modules"], "excludeReferences": true, "mergeModulesMergeMode": "module-category", - "external-modulemap": [ - ".*\/src\/(client)\/", - ".*\/src\/components\/([\\w\\-_]+)\/", - ], - "sort": [ - "alphabetical", - "visibility" - ], + "external-modulemap": [".*/src/(client)/", ".*/src/components/([\\w\\-_]+)/"], + "sort": ["alphabetical", "visibility"], "navigation": { "includeCategories": true, "includeGroups": true @@ -28,5 +17,5 @@ "navigationLinks": { "GitHub": "https://github.com/openSUSE/agama" }, - "skipErrorChecking": true, + "skipErrorChecking": true } diff --git a/web/webpack.config.js b/web/webpack.config.js index 87df2a4819..1ea7895157 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -24,7 +24,7 @@ const development = !production; const eslint = process.env.ESLINT !== "0"; /* Default to disable csslint for faster production builds */ -const stylelint = process.env.STYLELINT ? (process.env.STYLELINT !== "0") : development; +const stylelint = process.env.STYLELINT ? process.env.STYLELINT !== "0" : development; // Agama API server. By default it connects to a local development server. let agamaServer = process.env.AGAMA_SERVER || "localhost"; @@ -83,7 +83,7 @@ module.exports = { resolve: { modules: ["node_modules", path.resolve(__dirname, "src/lib")], plugins: [new TsconfigPathsPlugin({ extensions: [".ts", ".tsx", ".js", ".jsx", ".json"] })], - extensions: [".ts", ".tsx", ".js", ".jsx", ".json"] + extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], }, resolveLoader: { modules: ["node_modules", path.resolve(__dirname, "src/lib")], @@ -132,20 +132,20 @@ module.exports = { // Thus, it's needed not mangling function names ending in PageMenu to keep it working in production // until adopting a better solution, if any. terserOptions: { - keep_fnames: /PageMenu$/ + keep_fnames: /PageMenu$/, }, extractComments: { condition: true, filename: `[file].LICENSE.txt?query=[query]&filebase=[base]`, banner(licenseFile) { return `License information can be found in ${licenseFile}`; - } - } + }, + }, }), // remove also the spaces between the tags new HtmlMinimizerPlugin({ minimizerOptions: { conservativeCollapse: false } }), - new CssMinimizerPlugin() - ] + new CssMinimizerPlugin(), + ], }, module: { @@ -158,12 +158,12 @@ module.exports = { loader: require.resolve("ts-loader"), options: { getCustomTransformers: () => ({ - before: [development && ReactRefreshTypeScript()].filter(Boolean) + before: [development && ReactRefreshTypeScript()].filter(Boolean), }), - transpileOnly: development - } - } - ] + transpileOnly: development, + }, + }, + ], }, { test: /\.(js|jsx)$/, From 0d3874baa87a30e4b592e22ea480b37bce1ba8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:31:10 +0100 Subject: [PATCH 170/430] fix(web): change element wrapping core/EmptyState children (#1462) ## Problem The elements in the login page are rendered wrongly since https://github.com/openSUSE/agama/pull/1441. That's because the `Flex` component used for wrapping the `core/EmptyState` children. It is missing the "direction" prop for telling it to layout children vertically instead of horizontally. Moreover, it looks like a `Stack` element is a better choice for this case. ## Solution Adapt `core/EmptyState` for using an `` instead of a `` configured to behave almost the same. ## Testing - Tested manually --- web/src/components/core/EmptyState.jsx | 6 ++--- web/src/components/core/LoginPage.jsx | 36 ++++++++++++-------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx index 487a9279c4..e147766895 100644 --- a/web/src/components/core/EmptyState.jsx +++ b/web/src/components/core/EmptyState.jsx @@ -22,7 +22,7 @@ // @ts-check import React from "react"; -import { EmptyState, EmptyStateHeader, EmptyStateBody, Flex } from "@patternfly/react-core"; +import { EmptyState, EmptyStateHeader, EmptyStateBody, Stack } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; /** @@ -68,9 +68,7 @@ export default function EmptyStateWrapper({ icon={} /> - - {children} - + {children} ); diff --git a/web/src/components/core/LoginPage.jsx b/web/src/components/core/LoginPage.jsx index 76acd31cb8..1ff500b7c1 100644 --- a/web/src/components/core/LoginPage.jsx +++ b/web/src/components/core/LoginPage.jsx @@ -92,27 +92,25 @@ user privileges.", {rootExplanationStart} {rootUser} {rootExplanationEnd}

      {_("Please, provide its password to log in to the system.")}

      - -
      - - setPassword(v)} - /> - + + + setPassword(v)} + /> + - {error && } + {error && } - - - - -
      + + + + From 804aa4d44403721019d84fee485c48b7d411053b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 15:32:50 +0100 Subject: [PATCH 171/430] fix(web): install Webpack types definitions They allows to avoid some type errors like `Property 'hot' does not exist on type 'NodeModule'` See https://stackoverflow.com/a/55318758 --- web/package-lock.json | 8 ++++++++ web/package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/web/package-lock.json b/web/package-lock.json index 4499acd7f6..a2deca342b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -37,6 +37,7 @@ "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@types/webpack-env": "^1.18.5", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "ajv": "^8.12.0", @@ -4967,6 +4968,13 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/webpack-env": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.5.tgz", + "integrity": "sha512-wz7kjjRRj8/Lty4B+Kr0LN6Ypc/3SymeCCGSbaXp2leH0ZVg/PriNiOwNj4bD4uphI7A8NXS4b6Gl373sfO5mA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", diff --git a/web/package.json b/web/package.json index 861ea8933a..b830def6a1 100644 --- a/web/package.json +++ b/web/package.json @@ -42,6 +42,7 @@ "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@types/webpack-env": "^1.18.5", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "ajv": "^8.12.0", From 100cf2ff3ea0479a38afe1906797eab66367e422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 15:37:48 +0100 Subject: [PATCH 172/430] fix(web): avoid complaint about navigateTo prop The whole Page component has to be improved with possible rewrite to .tsx, but let's avoid the typecheck error by now. --- web/src/components/core/Page.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.jsx index cdfae2514b..b972c65273 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.jsx @@ -85,6 +85,7 @@ const Action = ({ navigateTo, children, ...props }) => { /** * Simple action for navigating back + * @param {ActionProps & { text?: string }} props */ const CancelAction = ({ text = _("Cancel"), navigateTo }) => { const navigate = useNavigate(); From 4bbc0948853373f49b37afc22d79ad95cfaff33c Mon Sep 17 00:00:00 2001 From: Natasha Ament-Teusink Date: Fri, 12 Jul 2024 15:48:53 +0200 Subject: [PATCH 173/430] change leap_160.yaml url to distribution change leap_160.yaml url to distribution --- products.d/agama-products-opensuse.changes | 5 +++++ products.d/leap_160.yaml | 11 ++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/products.d/agama-products-opensuse.changes b/products.d/agama-products-opensuse.changes index af643fa4cd..286f805583 100644 --- a/products.d/agama-products-opensuse.changes +++ b/products.d/agama-products-opensuse.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Tue Jul 12 13:39:15 UTC 2024 - Natasha Ament + +- change url for leap_160.yaml to distribution + ------------------------------------------------------------------- Tue Jul 2 13:54:15 UTC 2024 - Lubos Kocman diff --git a/products.d/leap_160.yaml b/products.d/leap_160.yaml index 293b7cd1ba..1cde6e6e36 100644 --- a/products.d/leap_160.yaml +++ b/products.d/leap_160.yaml @@ -26,15 +26,8 @@ translations: zh_Hans: Leap 16.0 是基于 SUSE Linux Enterprise Server 构建的社区发行版的最新版本。 software: installation_repositories: - - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/standard - archs: x86_64 - - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/standard - archs: aarch64 - - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/product/repo/Leap-Packages-16.0-x86_64 - archs: x86_64 - - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/product/repo/Leap-Packages-16.0-aarch64 - archs: aarch64 - + - url: https://download.opensuse.org/distribution/leap/16.0/oss + mandatory_patterns: - enhanced_base # only pattern that is shared among all roles on Leap optional_patterns: null # no optional pattern shared From 7a937cb99f6ec2e1f67fdc24457cd5934915c50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 16:34:16 +0100 Subject: [PATCH 174/430] fix(web): avoid types complaints in SpacePolicySelection Either, adding a temporary type definition or using @ts-ignore by now. --- web/src/components/storage/SpacePolicySelection.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/components/storage/SpacePolicySelection.jsx b/web/src/components/storage/SpacePolicySelection.jsx index e5a7648206..602ad6d510 100644 --- a/web/src/components/storage/SpacePolicySelection.jsx +++ b/web/src/components/storage/SpacePolicySelection.jsx @@ -85,6 +85,7 @@ const SpacePolicyPicker = ({ currentPolicy, onChange = noop }) => { */ export default function SpacePolicySelection() { const [state, setState] = useState({ load: false, settings: {} }); + /** @type ReturnType> */ const [policy, setPolicy] = useState(); const [actions, setActions] = useState([]); const [expandedDevices, setExpandedDevices] = useState([]); @@ -154,6 +155,7 @@ export default function SpacePolicySelection() { const onSubmit = (e) => { e.preventDefault(); + // @ts-ignore client.proposal.calculate({ ...state.settings, spacePolicy: policy.id, From 4bb07fd9f45e20e85134d09270720a979e353b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 16:45:08 +0100 Subject: [PATCH 175/430] fix(web): please TypeScript for ProposalResultSection By adding the missing `resize` property for actions in tests and by adapting the JSX of the component used for rendering its content. --- web/src/components/core/CardField.jsx | 16 +++++++++------- .../storage/ProposalResultSection.test.jsx | 2 +- .../storage/test-data/full-result-example.js | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/web/src/components/core/CardField.jsx b/web/src/components/core/CardField.jsx index 5695cb4c16..43a6082600 100644 --- a/web/src/components/core/CardField.jsx +++ b/web/src/components/core/CardField.jsx @@ -44,10 +44,10 @@ import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; */ const CardField = ({ label, - value, - description, - actions, - children = [], + value = undefined, + description = undefined, + actions = undefined, + children, cardProps = {}, cardHeaderProps = {}, cardDescriptionProps = {}, @@ -61,9 +61,11 @@ const CardField = ({

      {label}

      - - {value} - + {value && ( + + {value} + + )}
      diff --git a/web/src/components/storage/ProposalResultSection.test.jsx b/web/src/components/storage/ProposalResultSection.test.jsx index 0e3db28354..cc6839c1c9 100644 --- a/web/src/components/storage/ProposalResultSection.test.jsx +++ b/web/src/components/storage/ProposalResultSection.test.jsx @@ -81,7 +81,7 @@ describe.skip("ProposalResultSection", () => { // affected systems are rendered in the warning summary const props = { ...defaultProps, - actions: [{ device: 79, subvol: false, delete: true, text: "" }], + actions: [{ device: 79, subvol: false, delete: true, resize: false, text: "" }], }; plainRender(); diff --git a/web/src/components/storage/test-data/full-result-example.js b/web/src/components/storage/test-data/full-result-example.js index f3587215e4..43419f309d 100644 --- a/web/src/components/storage/test-data/full-result-example.js +++ b/web/src/components/storage/test-data/full-result-example.js @@ -1181,101 +1181,118 @@ export const actions = [ text: "Delete partition /dev/md0p1 (2.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 72, text: "Delete RAID0 /dev/md0 (10.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 80, text: "Delete partition /dev/vdc3 (1.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 78, text: "Delete partition /dev/vdc1 (5.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 81, text: "Shrink partition /dev/vdc4 from 2.00 GiB to 1.50 GiB", subvol: false, delete: false, + resize: true, }, { device: 459, text: "Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition", subvol: false, delete: false, + resize: false, }, { device: 460, text: "Create partition /dev/vdc3 (1.50 GiB) for swap", subvol: false, delete: false, + resize: false, }, { device: 463, text: "Create partition /dev/vdc5 (17.50 GiB) for / with btrfs", subvol: false, delete: false, + resize: false, }, { device: 467, text: "Create subvolume @ on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 482, text: "Create subvolume @/boot/grub2/x86_64-efi on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 480, text: "Create subvolume @/boot/grub2/i386-pc on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 478, text: "Create subvolume @/var on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 476, text: "Create subvolume @/usr/local on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 474, text: "Create subvolume @/srv on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 472, text: "Create subvolume @/root on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 470, text: "Create subvolume @/opt on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 468, text: "Create subvolume @/home on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, ]; From 74ae0575768789a750046c06e4fdd0a6cb3763f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 16:54:59 +0100 Subject: [PATCH 176/430] fix(web): add missing properties in ProposalActionsSummary.test.jsx --- web/src/components/storage/ProposalActionsSummary.test.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/components/storage/ProposalActionsSummary.test.jsx b/web/src/components/storage/ProposalActionsSummary.test.jsx index 3cb7aa52bb..af66162ba2 100644 --- a/web/src/components/storage/ProposalActionsSummary.test.jsx +++ b/web/src/components/storage/ProposalActionsSummary.test.jsx @@ -60,6 +60,7 @@ const defaultProps = { onActionsClick: jest.fn(), system: devices.system, staging: devices.staging, + devices: [sda], actions, }; @@ -75,7 +76,7 @@ describe("ProposalActionsSummary", () => { const props = { ...defaultProps, policy: deletePolicy, - actions: [{ device: 79, subvol: false, delete: true, text: "" }], + actions: [{ device: 79, subvol: false, delete: true, resize: false, text: "" }], }; installerRender(); From 253554ec35b0ebbd4a9c0070a69288ece34ca8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:03:23 +0100 Subject: [PATCH 177/430] fix(web): add missing inputRefs for PasswordAndConfirmationInput --- web/src/components/storage/EncryptionSettingsDialog.jsx | 4 +++- web/src/components/users/RootPasswordPopup.jsx | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/src/components/storage/EncryptionSettingsDialog.jsx b/web/src/components/storage/EncryptionSettingsDialog.jsx index 0801c45298..efbeb39e40 100644 --- a/web/src/components/storage/EncryptionSettingsDialog.jsx +++ b/web/src/components/storage/EncryptionSettingsDialog.jsx @@ -21,7 +21,7 @@ // @ts-check -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { Checkbox, Form, Switch, Stack } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { PasswordAndConfirmationInput, Popup } from "~/components/core"; @@ -78,6 +78,7 @@ export default function EncryptionSettingsDialog({ const [passwordsMatch, setPasswordsMatch] = useState(true); const [validSettings, setValidSettings] = useState(true); const [wasLoading, setWasLoading] = useState(isLoading); + const passwordRef = useRef(); const formId = "encryptionSettingsForm"; // reset the settings only after loading is finished @@ -131,6 +132,7 @@ export default function EncryptionSettingsDialog({ />
      { setPassword(""); @@ -63,6 +66,7 @@ export default function RootPasswordPopup({ title = _("Root password"), isOpen, Date: Fri, 12 Jul 2024 17:15:45 +0100 Subject: [PATCH 178/430] fix(web): add type definition for DeviceSelection state --- web/src/components/storage/DeviceSelection.jsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/web/src/components/storage/DeviceSelection.jsx b/web/src/components/storage/DeviceSelection.jsx index 653ef377b7..d417bb6c18 100644 --- a/web/src/components/storage/DeviceSelection.jsx +++ b/web/src/components/storage/DeviceSelection.jsx @@ -47,8 +47,6 @@ import { compact, useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; /** - * @typedef {import ("~/client/storage").ProposalTarget} ProposalTarget - * @typedef {import ("~/client/storage").ProposalSettings} ProposalSettings * @typedef {import ("~/client/storage").StorageDevice} StorageDevice */ @@ -56,16 +54,24 @@ const SELECT_DISK_ID = "select-disk"; const CREATE_LVM_ID = "create-lvm"; const SELECT_DISK_PANEL_ID = "panel-for-disk-selection"; const CREATE_LVM_PANEL_ID = "panel-for-lvm-creation"; -const OPTIONS_NAME = "selection-mode"; /** * Allows the user to select a target device for installation. * @component */ export default function DeviceSelection() { - const [state, setState] = useState({}); + /** + * @typedef {object} DeviceSelectionState + * @property {boolean} load + * @property {string} [target] + * @property {StorageDevice} [targetDevice] + * @property {StorageDevice[]} [targetPVDevices] + * @property {StorageDevice[]} [availableDevices] + */ const navigate = useNavigate(); const { cancellablePromise } = useCancellablePromise(); + /** @type ReturnType> */ + const [state, setState] = useState({ load: false }); const isTargetDisk = state.target === "DISK"; const isTargetNewLvmVg = state.target === "NEW_LVM_VG"; From 4525546648b4cf9914a97e82571fb0b5267cccf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:20:34 +0100 Subject: [PATCH 179/430] fix(web): add type definition for BootSelection state --- web/src/components/storage/BootSelection.jsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/src/components/storage/BootSelection.jsx b/web/src/components/storage/BootSelection.jsx index 0241f51a32..61e68440e6 100644 --- a/web/src/components/storage/BootSelection.jsx +++ b/web/src/components/storage/BootSelection.jsx @@ -49,9 +49,19 @@ const BOOT_DISABLED_ID = "boot-disabled"; * Allows the user to select the boot configuration. */ export default function BootSelectionDialog() { + /** + * @typedef {object} BootSelectionState + * @property {boolean} load + * @property {string} [selectedOption] + * @property {boolean} [configureBoot] + * @property {StorageDevice} [bootDevice] + * @property {StorageDevice} [defaultBootDevice] + * @property {StorageDevice[]} [availableDevices] + */ const { cancellablePromise } = useCancellablePromise(); const { storage: client } = useInstallerClient(); - const [state, setState] = useState({}); + /** @type ReturnType> */ + const [state, setState] = useState({ load: false }); const navigate = useNavigate(); // FIXME: Repeated code, see DeviceSelection. Use a context/hook or whatever From 9cc6a10e169a17d1c99142492484ecc209f499af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:22:58 +0100 Subject: [PATCH 180/430] fix(web): make core/CardField#label optional Because there are places that does not require such a label. Anyway, core/CardField is a component that must be rewritten/replaced. --- web/src/components/core/CardField.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/src/components/core/CardField.jsx b/web/src/components/core/CardField.jsx index 43a6082600..c11862e78e 100644 --- a/web/src/components/core/CardField.jsx +++ b/web/src/components/core/CardField.jsx @@ -43,7 +43,7 @@ import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; * @todo write documentation */ const CardField = ({ - label, + label = undefined, value = undefined, description = undefined, actions = undefined, @@ -58,9 +58,11 @@ const CardField = ({ - -

      {label}

      -
      + {label && ( + +

      {label}

      +
      + )} {value && ( {value} From 934a8ec5a28a4041d130b349778562347e61539c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:27:32 +0100 Subject: [PATCH 181/430] fix(web): make core/EmptyState#color prop optional By giving the "color-100" default value. --- web/src/components/core/EmptyState.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx index e147766895..92d4b79d70 100644 --- a/web/src/components/core/EmptyState.jsx +++ b/web/src/components/core/EmptyState.jsx @@ -41,7 +41,7 @@ import { Icon } from "~/components/layout"; * @param {object} props * @param {string} props.title * @param {IconName} props.icon - * @param {string} props.color + * @param {string} [props.color="color-100"] * @param {EmptyStateHeaderProps["headingLevel"]} [props.headingLevel="h4"] * @param {boolean} [props.noPadding=false] * @param {React.ReactNode} props.children @@ -64,6 +64,7 @@ export default function EmptyStateWrapper({ } /> From a41fd290c7bde7b70424add6c91754e1a6e0ee26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:33:14 +0100 Subject: [PATCH 182/430] fix(web): make core/EmptyState#children prop optional --- web/src/components/core/EmptyState.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx index 92d4b79d70..d007f0c4b8 100644 --- a/web/src/components/core/EmptyState.jsx +++ b/web/src/components/core/EmptyState.jsx @@ -44,7 +44,7 @@ import { Icon } from "~/components/layout"; * @param {string} [props.color="color-100"] * @param {EmptyStateHeaderProps["headingLevel"]} [props.headingLevel="h4"] * @param {boolean} [props.noPadding=false] - * @param {React.ReactNode} props.children + * @param {React.ReactNode} [props.children] * @param {EmptyStateProps} [props.rest] * @todo write documentation */ @@ -68,9 +68,11 @@ export default function EmptyStateWrapper({ titleClassName={`pf-v5-u-${color}`} icon={} /> - - {children} - + {children && ( + + {children} + + )} ); } From 712f873d4e4ef43283eb58346f707f00be3715db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:35:41 +0100 Subject: [PATCH 183/430] fix(web): improve type definitions of NetworkPage But also adds some @ts-ignore because this page is being already refactored in the context of adding a "global state manager" and a log changes / conflicts are expected. --- web/src/components/network/ConnectionsTable.jsx | 5 +++-- web/src/components/network/NetworkPage.jsx | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/src/components/network/ConnectionsTable.jsx b/web/src/components/network/ConnectionsTable.jsx index 48b5b04210..e925e3350c 100644 --- a/web/src/components/network/ConnectionsTable.jsx +++ b/web/src/components/network/ConnectionsTable.jsx @@ -30,6 +30,7 @@ import { formatIp } from "~/client/network/utils"; import { _ } from "~/i18n"; /** + * @typedef {import("~/client/network/model").Device} Device * @typedef {import("~/client/network/model").Connection} Connection */ @@ -40,8 +41,8 @@ import { _ } from "~/i18n"; * * @param {object} props * @param {Connection[]} props.connections - Connections to be shown - * @param {function} props.onEdit - function to be called for editing a connection - * @param {function} props.onForget - function to be called for forgetting a connection + * @param {Device[]} props.devices - Connections to be shown + * @param {function} [props.onForget] - function to be called for forgetting a connection */ export default function ConnectionsTable({ connections, devices, onForget }) { const navigate = useNavigate(); diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index cdf974028b..dc99f2d296 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -38,6 +38,7 @@ import { sprintf } from "sprintf-js"; */ export default function NetworkPage() { const { network: client } = useInstallerClient(); + // @ts-ignore const { connections: initialConnections, devices: initialDevices, settings } = useLoaderData(); const [connections, setConnections] = useState(initialConnections); const [devices, setDevices] = useState(initialDevices); @@ -64,6 +65,7 @@ export default function NetworkPage() { NetworkEventTypes.DEVICE_ADDED, NetworkEventTypes.DEVICE_UPDATED, NetworkEventTypes.DEVICE_REMOVED, + // @ts-ignore ].includes(type) ) { setUpdateState(true); From bf5266205587b1ae933997412ca19dba1b0f44c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:39:47 +0100 Subject: [PATCH 184/430] fix(web): disable type checking for core/Section It is a component that must be refactored or replace. Let's not invest too much time fixing types there meanwhile. --- web/src/components/core/Section.jsx | 2 +- web/src/components/core/Section.test.jsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/components/core/Section.jsx b/web/src/components/core/Section.jsx index 1d8c6c891d..8fc92c2098 100644 --- a/web/src/components/core/Section.jsx +++ b/web/src/components/core/Section.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -// @ts-check +// FIXME: Refactor or replace import React from "react"; import { Link } from "react-router-dom"; diff --git a/web/src/components/core/Section.test.jsx b/web/src/components/core/Section.test.jsx index 5d7c5cbb11..2104cac640 100644 --- a/web/src/components/core/Section.test.jsx +++ b/web/src/components/core/Section.test.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -// @ts-check +// FIXME: Refactor or replace import React from "react"; import { screen, within } from "@testing-library/react"; @@ -65,7 +65,6 @@ describe.skip("Section", () => { }); it("does not render an icon if not valid icon name is given", () => { - // @ts-expect-error: Creating the icon name dynamically is unlikely, but let's be safe. const { container } = plainRender(
      , ); From 29086923d057daeeea673c432291a4ea62972958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:41:40 +0100 Subject: [PATCH 185/430] fix(web): remove leftovers in core/Page --- web/src/components/core/Page.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.jsx index b972c65273..b71bcdbbcd 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.jsx @@ -22,7 +22,7 @@ // @ts-check import React from "react"; -import { NavLink, Outlet, useNavigate, useMatches, useLocation } from "react-router-dom"; +import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { Button, Card, @@ -187,11 +187,6 @@ const CardSection = ({ title, children, ...props }) => { * @param {React.ReactNode} [props.children] - The page content. */ const Page = () => { - const location = useLocation(); - const matches = useMatches(); - const currentRoute = matches.find((r) => r.pathname === location.pathname); - const titleFromRoute = currentRoute?.handle?.name; - return ( From 91f44cfecf2f72a57fa68e7c4ed548b60326faa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:42:19 +0100 Subject: [PATCH 186/430] fix(web): temporary ignore a rest typecheck at core/EmptyState --- web/src/components/core/EmptyState.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx index d007f0c4b8..490226433a 100644 --- a/web/src/components/core/EmptyState.jsx +++ b/web/src/components/core/EmptyState.jsx @@ -57,6 +57,7 @@ export default function EmptyStateWrapper({ children, ...rest }) { + // @ts-ignore if (noPadding) rest.className = [rest.className, "no-padding"].join(" ").trim(); return ( From 0648ee269c46fd38ea3fb22741230019a75e2504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:43:31 +0100 Subject: [PATCH 187/430] fix(web) drop no longer used prop at core/Center The component is going to be refactored or replaced soon and the prop can be easily re-introduced if really needed. --- web/src/components/layout/Center.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/components/layout/Center.jsx b/web/src/components/layout/Center.jsx index 52bd24f759..a7df6e8a14 100644 --- a/web/src/components/layout/Center.jsx +++ b/web/src/components/layout/Center.jsx @@ -46,10 +46,9 @@ import React from "react"; * * @param {object} props * @param {React.ReactNode} props.children - * @param {React.HTMLAttributes} props.htmlProps */ -const Center = ({ children, ...htmlProps }) => ( -
      +const Center = ({ children }) => ( +
      {children}
      ); From d536cef6e5d478cf39689e92a0604f2f5619567a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:46:22 +0100 Subject: [PATCH 188/430] fix(web) avoid a type definition complaint at LoginPage Related to the fact that core/EmptyState#rest prop is not well defined. Of course, it should be properly fixed in the short term. --- web/src/components/core/LoginPage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/core/LoginPage.jsx b/web/src/components/core/LoginPage.jsx index 1ff500b7c1..5ef18ed4e4 100644 --- a/web/src/components/core/LoginPage.jsx +++ b/web/src/components/core/LoginPage.jsx @@ -87,6 +87,7 @@ user privileges.", + {/** @ts-ignore */}

      {rootExplanationStart} {rootUser} {rootExplanationEnd} From 2f463e4e7fbbca5e1fc6742ad54de1dd1f18a037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:48:43 +0100 Subject: [PATCH 189/430] fix(web): disable typechecking for core/Drawer Since it is a kind of experimental wrapper component that must be finished and, ideally, ported to .tsx now that writing TypeScript code is possible in Agama. --- web/src/components/core/Drawer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/core/Drawer.jsx b/web/src/components/core/Drawer.jsx index 504f09afa6..97b23dfee0 100644 --- a/web/src/components/core/Drawer.jsx +++ b/web/src/components/core/Drawer.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -// @ts-check +// FIXME: rewrite to .tsx import React, { forwardRef, useImperativeHandle, useState } from "react"; import { From 98d3183f833645076a1e6b3af7cc73c9a4d36754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:51:12 +0100 Subject: [PATCH 190/430] fix(web): drop leftover property in a network test --- web/src/client/network/model.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/client/network/model.test.js b/web/src/client/network/model.test.js index c25d998242..0ab12d642b 100644 --- a/web/src/client/network/model.test.js +++ b/web/src/client/network/model.test.js @@ -43,7 +43,7 @@ describe("createConnection", () => { it("merges given properties", () => { const addresses = [{ address: "192.168.0.1", prefix: 24 }]; - const connection = createConnection({ addresses, testing: 1 }); + const connection = createConnection({ addresses }); expect(connection.method4).toEqual("auto"); expect(connection.gateway4).toEqual(""); expect(connection.addresses).toEqual(addresses); From e6631c1294c3e2520e28b1b7ed410ad076a517b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 17:52:11 +0100 Subject: [PATCH 191/430] fix(web): avoid typecheck errors in storage client Basically bypassing them. Such a client needs a refactor in which types thingy must be addressed. --- web/src/client/storage.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index ab58fd31d7..16f20b3b71 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -43,7 +43,32 @@ const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk"; /** @fixme Adapt code depending on D-Bus */ class DBusClient { - proxy() { + /** + * @param {string} service + * @param {string|undefined} address + */ + constructor(service, address) { + console.warn(`FIXME: Adapt code depending on D-Bus ${service} ${address}`); + } + + /** + * @param {string} iface + * @param {string} [path] + * @return {Promise} + */ + async proxy(iface, path) { + console.warn(`FIXME: Adapt code depending on D-Bus ${iface} ${path}`); + return Promise.resolve(undefined); + } + + /** + * @param {string|undefined} iface + * @param {string|undefined} path_namespace + * @param {object|undefined} options + * @return {Promise} + */ + async proxies(iface, path_namespace, options) { + console.warn(`FIXME: Adapt code depending on D-Bus ${iface} ${path_namespace} ${options}`); return Promise.resolve(undefined); } } @@ -1576,7 +1601,9 @@ class StorageBaseClient { this.staging = new DevicesManager(this.client, "result"); this.proposal = new ProposalManager(this.client, this.system); this.iscsi = new ISCSIManager(this.client); + // @ts-ignore this.dasd = new DASDManager(StorageBaseClient.SERVICE, client); + // @ts-ignore this.zfcp = new ZFCPManager(StorageBaseClient.SERVICE, client); } From 5e6e1e8da61b1b38480c0f596522892feb2d7d4c Mon Sep 17 00:00:00 2001 From: Natasha Ament-Teusink Date: Fri, 12 Jul 2024 19:31:09 +0200 Subject: [PATCH 192/430] changed leap_160.yaml due to type in url --- products.d/agama-products-opensuse.changes | 5 +++++ products.d/leap_160.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/products.d/agama-products-opensuse.changes b/products.d/agama-products-opensuse.changes index 286f805583..6bd4e35180 100644 --- a/products.d/agama-products-opensuse.changes +++ b/products.d/agama-products-opensuse.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Tue Jul 12 17:29:00 UTC 2024 - Natasha Ament + +- change url for leap_160.yaml due to typo + ------------------------------------------------------------------- Tue Jul 12 13:39:15 UTC 2024 - Natasha Ament diff --git a/products.d/leap_160.yaml b/products.d/leap_160.yaml index 1cde6e6e36..ab1cdb22ab 100644 --- a/products.d/leap_160.yaml +++ b/products.d/leap_160.yaml @@ -26,7 +26,7 @@ translations: zh_Hans: Leap 16.0 是基于 SUSE Linux Enterprise Server 构建的社区发行版的最新版本。 software: installation_repositories: - - url: https://download.opensuse.org/distribution/leap/16.0/oss + - url: https://download.opensuse.org/distribution/leap/16.0/repo/oss mandatory_patterns: - enhanced_base # only pattern that is shared among all roles on Leap From 22fbaef37c84a59e4d7af486a1392551c3b5a430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 19:49:45 +0100 Subject: [PATCH 193/430] fix(web): fine-tuning Webpack and TypeScript config Removing the noEmit compiler option from tsconfig file and altering the order of babel and ts loaders to make them work without errors. Most probably that configuration has room for improvements yet. --- web/tsconfig.json | 1 - web/webpack.config.js | 18 ++++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/web/tsconfig.json b/web/tsconfig.json index fc69f759e1..c8a6d2fa16 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "baseUrl": "./", "isolatedModules": true, - "noEmit": true, "target": "esnext", "moduleResolution": "node", "resolveJsonModule": true, diff --git a/web/webpack.config.js b/web/webpack.config.js index 1ea7895157..fd82d21e93 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -154,6 +154,12 @@ module.exports = { test: /\.[jt]sx?$/, exclude: /node_modules/, use: [ + { + loader: "babel-loader", + options: { + plugins: [development && require.resolve("react-refresh/babel")].filter(Boolean), + }, + }, { loader: require.resolve("ts-loader"), options: { @@ -165,18 +171,6 @@ module.exports = { }, ], }, - { - test: /\.(js|jsx)$/, - exclude: /node_modules/, - use: [ - { - loader: "babel-loader", - options: { - plugins: [development && require.resolve("react-refresh/babel")].filter(Boolean), - }, - }, - ], - }, { test: /\.s?css$/, use: [ From 2ed84910117a5e07cec3d03eee345350d4203613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 12 Jul 2024 19:52:45 +0100 Subject: [PATCH 194/430] fix(web): ensure proper code format --- web/.eslintrc.json | 18 +++--------------- web/.stylelintrc.json | 4 +--- web/src/components/storage/VolumeFields.jsx | 2 +- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/web/.eslintrc.json b/web/.eslintrc.json index 4b23faa524..76fbfa654e 100644 --- a/web/.eslintrc.json +++ b/web/.eslintrc.json @@ -21,14 +21,7 @@ }, "sourceType": "module" }, - "plugins": [ - "agama-i18n", - "flowtype", - "i18next", - "react", - "react-hooks", - "@typescript-eslint" - ], + "plugins": ["agama-i18n", "flowtype", "i18next", "react", "react-hooks", "@typescript-eslint"], "rules": { "agama-i18n/string-literals": "error", "i18next/no-literal-string": "error", @@ -69,19 +62,14 @@ "overrides": [ { // do not check translations in the testing or development files - "files": [ - "*.test.*", - "test-utils.js" - ], + "files": ["*.test.*", "test-utils.js"], "rules": { "i18next/no-literal-string": "off" } }, { // do not check translation arguments in the test, it checks some internals by passing variables - "files": [ - "i18n.test.js" - ], + "files": ["i18n.test.js"], "rules": { "agama-i18n/string-literals": "off" } diff --git a/web/.stylelintrc.json b/web/.stylelintrc.json index a7365de1cd..ad22c45498 100644 --- a/web/.stylelintrc.json +++ b/web/.stylelintrc.json @@ -1,7 +1,5 @@ { - "plugins": [ - "stylelint-prettier" - ], + "plugins": ["stylelint-prettier"], "extends": [ "stylelint-config-standard", "stylelint-config-standard-scss", diff --git a/web/src/components/storage/VolumeFields.jsx b/web/src/components/storage/VolumeFields.jsx index 66f4e1e995..f8047f93e5 100644 --- a/web/src/components/storage/VolumeFields.jsx +++ b/web/src/components/storage/VolumeFields.jsx @@ -102,7 +102,7 @@ const SizeUnitFormSelect = ({ units, ...formSelectProps }) => { {units.map((unit) => { // unit values are marked for translation in the utils.js file // eslint-disable-next-line agama-i18n/string-literals - return + return ; })} ); From 177f5cc160a83050cf0e5b1da2d59cd26f869f9e Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 12 Jul 2024 21:07:45 +0200 Subject: [PATCH 195/430] changes from review --- rust/agama-cli/src/main.rs | 6 +- rust/agama-lib/src/base_http_client.rs | 81 +++++++++++++++++++++ rust/agama-lib/src/http_client.rs | 61 ---------------- rust/agama-lib/src/lib.rs | 2 +- rust/agama-lib/src/questions/http_client.rs | 9 +-- 5 files changed, 90 insertions(+), 69 deletions(-) create mode 100644 rust/agama-lib/src/base_http_client.rs delete mode 100644 rust/agama-lib/src/http_client.rs diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index b7891df3de..01afb47235 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -119,7 +119,7 @@ async fn build_manager<'a>() -> anyhow::Result> { } async fn run_command(cli: Cli) -> Result<(), ServiceError> { - Ok(match cli.command { + match cli.command { Commands::Config(subcommand) => { let manager = build_manager().await?; wait_for_services(&manager).await?; @@ -139,7 +139,9 @@ async fn run_command(cli: Cli) -> Result<(), ServiceError> { Commands::Logs(subcommand) => run_logs_cmd(subcommand).await?, Commands::Auth(subcommand) => run_auth_cmd(subcommand).await?, Commands::Download { url } => crate::profile::download(&url, std::io::stdout())?, - }) + }; + + Ok(()) } /// Represents the result of execution. diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs new file mode 100644 index 0000000000..1405162420 --- /dev/null +++ b/rust/agama-lib/src/base_http_client.rs @@ -0,0 +1,81 @@ +use anyhow::Context; +use reqwest::{header, Client, Response}; +use serde::de::DeserializeOwned; + +use crate::{auth::AuthToken, error::ServiceError}; + +/// Base that all http clients should use. +/// +/// It provides several features including automatic base url switching, +/// websocket events listening or object constructions. +/// +/// Usage should be just thin layer in domain specific client. +/// +/// ```no_run +/// use agama_lib::questions::model::Question; +/// use agama_lib::base_http_client::BaseHTTPClient; +/// use agama_lib::error::ServiceError; +/// +/// async fn get_questions() -> Result, ServiceError> { +/// let client = BaseHTTPClient::new()?; +/// client.get("/questions").await +/// } +/// ``` +pub struct BaseHTTPClient { + client: Client, + pub base_url: String, +} + +const API_URL: &str = "http://localhost/api"; + +impl BaseHTTPClient { + // if there is need for client without authorization, create new constructor for it + pub fn new() -> Result { + let token = AuthToken::find().context("You are not logged in")?; + + let mut headers = header::HeaderMap::new(); + // just use generic anyhow error here as Bearer format is constructed by us, so failures can come only from token + let value = header::HeaderValue::from_str(format!("Bearer {}", token).as_str()) + .map_err(|e| anyhow::Error::new(e))?; + + headers.insert(header::AUTHORIZATION, value); + + let client = Client::builder().default_headers(headers).build()?; + + Ok(Self { + client, + base_url: API_URL.to_string(), // TODO: add support for remote server + }) + } + + const NO_TEXT: &'static str = "No text"; + /// Simple wrapper around Response to get object from response. + /// If complete Response is needed use get_response method. + pub async fn get(&self, path: &str) -> Result { + let response = self.get_response(path).await?; + if response.status().is_success() { + response.json::().await.map_err(|e| e.into()) + } else { + let code = response.status().as_u16(); + let text = response + .text() + .await + .unwrap_or_else(|_| Self::NO_TEXT.to_string()); + Err(ServiceError::BackendError(code, text)) + } + } + + /// Calls GET method on given path and return Response that can be further + /// processed. If only simple object from json is required, use method get. + pub async fn get_response(&self, path: &str) -> Result { + self.client + .get(self.url(path)) + .send() + .await + .map_err(|e| e.into()) + } + + fn url(&self, path: &str) -> String { + self.base_url.clone() + path + } +} diff --git a/rust/agama-lib/src/http_client.rs b/rust/agama-lib/src/http_client.rs deleted file mode 100644 index a538c52029..0000000000 --- a/rust/agama-lib/src/http_client.rs +++ /dev/null @@ -1,61 +0,0 @@ -use anyhow::Context; -use reqwest::{header, Client, Response}; -use serde::de::DeserializeOwned; - -use crate::{auth::AuthToken, error::ServiceError}; - -pub struct HTTPClient { - client: Client, - pub base_url: String, -} - -const API_URL: &str = "http://localhost/api"; - -impl HTTPClient { - // if there is need for client without authorization, create new constructor for it - pub async fn new() -> Result { - let token = AuthToken::find().context("You are not logged in")?; - - let mut headers = header::HeaderMap::new(); - let value = header::HeaderValue::from_str(format!("Bearer {}", token).as_str()) - .map_err(|e| ServiceError::NetworkClientError(e.to_string()))?; - - headers.insert(header::AUTHORIZATION, value); - - let client = Client::builder().default_headers(headers).build()?; - - Ok(Self { - client, - base_url: API_URL.to_string(), // TODO: add support for remote server - }) - } - - const NO_TEXT: &'static str = "No text"; - // Simple wrapper around Response to get target type. - // For more advanced usage use directly get method - pub async fn get_type(&self, path: &str) -> Result { - let response = self.get(path).await?; - if !response.status().is_success() { - let code = response.status().as_u16(); - let text = response - .text() - .await - .unwrap_or_else(|_| Self::NO_TEXT.to_string()); - return Err(ServiceError::BackendError(code, text)); - } - - response.json::().await.map_err(|e| e.into()) - } - - pub async fn get(&self, path: &str) -> Result { - self.client - .get(self.target_path(path)) - .send() - .await - .map_err(|e| e.into()) - } - - fn target_path(&self, path: &str) -> String { - self.base_url.clone() + path - } -} diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 1fe1d7766b..e91fbdc770 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -25,7 +25,7 @@ pub mod auth; pub mod error; -mod http_client; +pub mod base_http_client; pub mod install_settings; pub mod localization; pub mod manager; diff --git a/rust/agama-lib/src/questions/http_client.rs b/rust/agama-lib/src/questions/http_client.rs index 8cbe53fc41..f12f4554f9 100644 --- a/rust/agama-lib/src/questions/http_client.rs +++ b/rust/agama-lib/src/questions/http_client.rs @@ -1,20 +1,19 @@ -use crate::error::ServiceError; +use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; use super::model; pub struct HTTPClient { - client: crate::http_client::HTTPClient, + client: BaseHTTPClient, } impl HTTPClient { pub async fn new() -> Result { Ok(Self { - client: crate::http_client::HTTPClient::new().await?, + client: BaseHTTPClient::new()?, }) } pub async fn list_questions(&self) -> Result, ServiceError> { - let questions = self.client.get_type("/questions").await?; - Ok(questions) + self.client.get("/questions").await } } From 5ba3b2fc17e887f669321ab6ca7f52090798d745 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 12 Jul 2024 23:00:05 +0200 Subject: [PATCH 196/430] add post method to base client and use it in creating question call --- rust/agama-lib/src/base_http_client.rs | 40 +++++++++++++++++---- rust/agama-lib/src/questions/http_client.rs | 13 ++++++- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 1405162420..edc9e1c29a 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -1,6 +1,6 @@ use anyhow::Context; use reqwest::{header, Client, Response}; -use serde::de::DeserializeOwned; +use serde::{de::DeserializeOwned, Serialize}; use crate::{auth::AuthToken, error::ServiceError}; @@ -56,12 +56,7 @@ impl BaseHTTPClient { if response.status().is_success() { response.json::().await.map_err(|e| e.into()) } else { - let code = response.status().as_u16(); - let text = response - .text() - .await - .unwrap_or_else(|_| Self::NO_TEXT.to_string()); - Err(ServiceError::BackendError(code, text)) + Err(self.build_backend_error(response).await) } } @@ -78,4 +73,35 @@ impl BaseHTTPClient { fn url(&self, path: &str) -> String { self.base_url.clone() + path } + + /// post object to given path and report error if post + pub async fn post(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { + let response = self.post_response(path, object).await?; + if response.status().is_success() { + Ok(()) + } else { + Err(self.build_backend_error(response).await) + } + } + + /// post object to given path and returns server response. Reports error only if failed to send + /// request, but if server returns e.g. 500, it will be in Ok result. + /// In general unless specific response handling is needed, simple post should be used. + pub async fn post_response(&self, path: &str, object: &impl Serialize) -> Result { + self.client + .post(self.url(path)) + .json(object) + .send() + .await + .map_err(|e| e.into()) + } + + pub async fn build_backend_error(&self, response: Response) -> ServiceError { + let code = response.status().as_u16(); + let text = response + .text() + .await + .unwrap_or_else(|_| Self::NO_TEXT.to_string()); + ServiceError::BackendError(code, text) + } } diff --git a/rust/agama-lib/src/questions/http_client.rs b/rust/agama-lib/src/questions/http_client.rs index f12f4554f9..aa054a8a7a 100644 --- a/rust/agama-lib/src/questions/http_client.rs +++ b/rust/agama-lib/src/questions/http_client.rs @@ -1,6 +1,6 @@ use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; -use super::model; +use super::model::{self, Question}; pub struct HTTPClient { client: BaseHTTPClient, @@ -16,4 +16,15 @@ impl HTTPClient { pub async fn list_questions(&self) -> Result, ServiceError> { self.client.get("/questions").await } + + /// Creates question and return newly created question including id + pub async fn create_question(&self, question: &Question) -> Result { + let response = self.client.post_response("/questions", question).await?; + if response.status().is_success() { + let question = response.json().await?; + Ok(question) + } else { + Err(self.client.build_backend_error(response).await) + } + } } From 59faba97d02aea99d144aad9f2bbd3cdf0aadc02 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 12 Jul 2024 23:21:08 +0200 Subject: [PATCH 197/430] format rust --- rust/agama-lib/src/base_http_client.rs | 16 ++++++++++------ rust/agama-lib/src/lib.rs | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index edc9e1c29a..02dd866b10 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -5,17 +5,17 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::{auth::AuthToken, error::ServiceError}; /// Base that all http clients should use. -/// +/// /// It provides several features including automatic base url switching, /// websocket events listening or object constructions. -/// +/// /// Usage should be just thin layer in domain specific client. -/// +/// /// ```no_run /// use agama_lib::questions::model::Question; /// use agama_lib::base_http_client::BaseHTTPClient; /// use agama_lib::error::ServiceError; -/// +/// /// async fn get_questions() -> Result, ServiceError> { /// let client = BaseHTTPClient::new()?; /// client.get("/questions").await @@ -57,7 +57,7 @@ impl BaseHTTPClient { response.json::().await.map_err(|e| e.into()) } else { Err(self.build_backend_error(response).await) - } + } } /// Calls GET method on given path and return Response that can be further @@ -87,7 +87,11 @@ impl BaseHTTPClient { /// post object to given path and returns server response. Reports error only if failed to send /// request, but if server returns e.g. 500, it will be in Ok result. /// In general unless specific response handling is needed, simple post should be used. - pub async fn post_response(&self, path: &str, object: &impl Serialize) -> Result { + pub async fn post_response( + &self, + path: &str, + object: &impl Serialize, + ) -> Result { self.client .post(self.url(path)) .json(object) diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index e91fbdc770..3dfffd3e07 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -24,8 +24,8 @@ //! As said, those modules might implement additional stuff, like specific types, clients, etc. pub mod auth; -pub mod error; pub mod base_http_client; +pub mod error; pub mod install_settings; pub mod localization; pub mod manager; From 17dc31677c2b1afb99a428bb6c4289c1b2c41b55 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 14 Jul 2024 02:55:36 +0000 Subject: [PATCH 198/430] Update web PO files Agama-weblate commit: 58a59d54a14eadfaa04a59b46e262be26cff5c7d --- web/po/ca.po | 1192 +++++++++++++++++++-------------------- web/po/cs.po | 1153 ++++++++++++++++++-------------------- web/po/de.po | 1201 +++++++++++++++++++-------------------- web/po/es.po | 1193 +++++++++++++++++++-------------------- web/po/fr.po | 1193 +++++++++++++++++++-------------------- web/po/id.po | 1168 ++++++++++++++++++-------------------- web/po/ja.po | 1194 +++++++++++++++++++-------------------- web/po/ka.po | 1165 ++++++++++++++++++-------------------- web/po/mk.po | 1152 ++++++++++++++++++-------------------- web/po/nb_NO.po | 1185 +++++++++++++++++++-------------------- web/po/nl.po | 1162 ++++++++++++++++++-------------------- web/po/pt_BR.po | 1185 +++++++++++++++++++-------------------- web/po/ru.po | 1187 +++++++++++++++++++-------------------- web/po/sv.po | 1192 +++++++++++++++++++-------------------- web/po/tr.po | 1152 ++++++++++++++++++-------------------- web/po/uk.po | 1153 ++++++++++++++++++-------------------- web/po/zh_Hans.po | 1207 +++++++++++++++++++--------------------- web/src/languages.json | 22 +- 18 files changed, 9577 insertions(+), 10479 deletions(-) diff --git a/web/po/ca.po b/web/po/ca.po index 87b43cece1..b70ee9ae12 100644 --- a/web/po/ca.po +++ b/web/po/ca.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-06-27 09:46+0000\n" +"POT-Creation-Date: 2024-07-14 02:32+0000\n" +"PO-Revision-Date: 2024-07-12 09:47+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan \n" @@ -17,13 +17,13 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6\n" +"X-Generator: Weblate 5.6.2\n" -#: src/MainLayout.jsx:40 +#: src/MainLayout.jsx:52 msgid "Agama" msgstr "Agama" -#: src/MainLayout.jsx:82 +#: src/MainLayout.jsx:94 msgid "Change product" msgstr "Canvia el producte" @@ -31,12 +31,11 @@ msgstr "Canvia el producte" msgid "About" msgstr "Quant a" -#: src/components/core/About.jsx:71 +#: src/components/core/About.jsx:69 msgid "About Agama" msgstr "Quant a Agama" -#. TRANSLATORS: content of the "About" popup (1/2) -#: src/components/core/About.jsx:76 +#: src/components/core/About.jsx:74 msgid "" "Agama is an experimental installer for (open)SUSE systems. It is still under " "development so, please, do not use it in production environments. If you " @@ -50,26 +49,17 @@ msgstr "" #. TRANSLATORS: content of the "About" popup (2/2) #. %s is replaced by the project URL -#: src/components/core/About.jsx:88 +#: src/components/core/About.jsx:86 #, c-format msgid "For more information, please visit the project's repository at %s." msgstr "" "Per obtenir-ne més informació, visiteu el repositori del projecte a %s." -#: src/components/core/About.jsx:94 src/components/core/FileViewer.jsx:81 -#: src/components/core/LogsButton.jsx:123 -#: src/components/software/SoftwarePatternsSelection.jsx:260 +#: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 +#: src/components/software/SoftwarePatternsSelection.jsx:268 msgid "Close" msgstr "Tanca" -#: src/components/core/FileViewer.jsx:66 -msgid "Reading file..." -msgstr "Llegint el fitxer..." - -#: src/components/core/FileViewer.jsx:72 -msgid "Cannot read the file" -msgstr "No es pot llegir el fitxer." - #: src/components/core/InstallButton.jsx:32 msgid "Confirm Installation" msgstr "Confirmeu la instal·lació" @@ -93,9 +83,9 @@ msgid "Continue" msgstr "Continua" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:97 -#: src/components/core/Popup.jsx:136 -#: src/components/network/WifiConnectionForm.jsx:131 +#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 +#: src/components/core/Popup.jsx:132 +#: src/components/network/WifiConnectionForm.jsx:134 msgid "Cancel" msgstr "Cancel·la" @@ -104,11 +94,11 @@ msgstr "Cancel·la" msgid "Install" msgstr "Instal·la" -#: src/components/core/InstallationFinished.jsx:42 +#: src/components/core/InstallationFinished.jsx:48 msgid "TPM sealing requires the new system to be booted directly." msgstr "El segellament TPM requereix que el sistema nou s'iniciï directament." -#: src/components/core/InstallationFinished.jsx:47 +#: src/components/core/InstallationFinished.jsx:53 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." @@ -116,16 +106,15 @@ msgstr "" "Si s'ha usat un mitjà local per executar aquest instal·lador, traieu-lo " "abans de la propera arrencada." -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "Hide details" msgstr "Amaga els detalls" -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "See more details" msgstr "Mostra'n més detalls" -#. TRANSLATORS: "Trusted Platform Module" is the name of the technology and "TPM" its abbreviation -#: src/components/core/InstallationFinished.jsx:55 +#: src/components/core/InstallationFinished.jsx:62 msgid "" "The final step to configure the Trusted Platform Module (TPM) to " "automatically open encrypted devices will take place during the first boot " @@ -137,28 +126,28 @@ msgstr "" "arrencada del nou sistema. Perquè això funcioni, la màquina ha d'arrencar " "directament amb el carregador d'arrencada nou." -#: src/components/core/InstallationFinished.jsx:97 +#: src/components/core/InstallationFinished.jsx:107 msgid "Congratulations!" msgstr "Enhorabona!" -#: src/components/core/InstallationFinished.jsx:102 +#: src/components/core/InstallationFinished.jsx:116 msgid "The installation on your machine is complete." msgstr "La instal·lació a la màquina s'ha completat." -#: src/components/core/InstallationFinished.jsx:105 +#: src/components/core/InstallationFinished.jsx:119 msgid "At this point you can power off the machine." msgstr "En aquest punt, podeu aturar la màquina." -#: src/components/core/InstallationFinished.jsx:106 +#: src/components/core/InstallationFinished.jsx:121 msgid "At this point you can reboot the machine to log in to the new system." msgstr "" "En aquest punt, podeu reiniciar la màquina per iniciar sessió al sistema nou." -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Finish" msgstr "Acaba" -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Reboot" msgstr "Reinicia" @@ -166,40 +155,40 @@ msgstr "Reinicia" msgid "Installing the system, please wait ..." msgstr "Instal·lant el sistema. Espereu, si us plau..." -#: src/components/core/InstallerOptions.jsx:83 +#: src/components/core/InstallerOptions.jsx:92 msgid "Show installer options" msgstr "Mostra les opcions de l'instal·lador" -#: src/components/core/InstallerOptions.jsx:88 +#: src/components/core/InstallerOptions.jsx:95 msgid "Installer options" msgstr "Opcions de l'instal·lador" -#: src/components/core/InstallerOptions.jsx:94 -#: src/components/core/InstallerOptions.jsx:99 -#: src/components/core/InstallerOptions.jsx:100 -#: src/components/l10n/L10nPage.jsx:67 +#: src/components/core/InstallerOptions.jsx:98 +#: src/components/core/InstallerOptions.jsx:102 +#: src/components/core/InstallerOptions.jsx:103 +#: src/components/l10n/L10nPage.jsx:60 msgid "Language" msgstr "Llengua" -#: src/components/core/InstallerOptions.jsx:114 -#: src/components/core/InstallerOptions.jsx:121 +#: src/components/core/InstallerOptions.jsx:115 +#: src/components/core/InstallerOptions.jsx:120 msgid "Keyboard layout" msgstr "Disposició del teclat" -#: src/components/core/InstallerOptions.jsx:130 +#: src/components/core/InstallerOptions.jsx:129 msgid "Cannot be changed in remote installation" msgstr "No es pot canviar a la instal·lació remota." -#: src/components/core/InstallerOptions.jsx:135 -#: src/components/network/IpSettingsForm.jsx:210 -#: src/components/product/ProductRegistrationPage.jsx:89 -#: src/components/storage/BootSelection.jsx:228 -#: src/components/storage/DeviceSelection.jsx:240 -#: src/components/storage/EncryptionSettingsDialog.jsx:138 -#: src/components/storage/SpacePolicySelection.jsx:198 -#: src/components/storage/VolumeDialog.jsx:781 -#: src/components/storage/ZFCPPage.jsx:503 -#: src/components/users/FirstUserForm.jsx:285 +#: src/components/core/InstallerOptions.jsx:142 +#: src/components/network/IpSettingsForm.jsx:228 +#: src/components/product/ProductRegistrationPage.jsx:85 +#: src/components/storage/BootSelection.jsx:250 +#: src/components/storage/DeviceSelection.jsx:254 +#: src/components/storage/EncryptionSettingsDialog.jsx:155 +#: src/components/storage/SpacePolicySelection.jsx:200 +#: src/components/storage/VolumeDialog.jsx:794 +#: src/components/storage/ZFCPPage.jsx:528 +#: src/components/users/FirstUserForm.jsx:303 msgid "Accept" msgstr "Accepta-ho" @@ -209,31 +198,28 @@ msgid "" msgstr "" "Abans de començar la instal·lació, heu de resoldre els problemes següents:" -#: src/components/core/ListSearch.jsx:51 +#: src/components/core/ListSearch.jsx:48 msgid "Search" msgstr "Cerca" -#: src/components/core/LoginPage.jsx:61 +#: src/components/core/LoginPage.jsx:64 msgid "Could not log in. Please, make sure that the password is correct." msgstr "" "No s'ha pogut iniciar la sessió. Si us plau, assegureu-vos que la " "contrasenya sigui correcta." -#: src/components/core/LoginPage.jsx:63 +#: src/components/core/LoginPage.jsx:66 msgid "Could not authenticate against the server, please check it." msgstr "No s'ha pogut autenticar amb el servidor. Si us plau, reviseu-ho." #. TRANSLATORS: Title for a form to provide the password for the root user. %s #. will be replaced by "root" -#: src/components/core/LoginPage.jsx:71 +#: src/components/core/LoginPage.jsx:74 #, c-format msgid "Log in as %s" msgstr "Inicieu sessió com a %s" -#. TRANSLATORS: description why root password is needed. The text in the -#. square brackets [] is displayed in bold, use only please, do not translate -#. it and keep the brackets. -#: src/components/core/LoginPage.jsx:76 +#: src/components/core/LoginPage.jsx:80 msgid "The installer requires [root] user privileges." msgstr "L'instal·lador requereix privilegis de l'usuari [root]." @@ -242,32 +228,32 @@ msgid "Please, provide its password to log in to the system." msgstr "" "Si us plau, proporcioneu-ne la contrasenya per iniciar sessió al sistema." -#: src/components/core/LoginPage.jsx:98 +#: src/components/core/LoginPage.jsx:96 msgid "Login form" msgstr "Forma d'entrada" -#: src/components/core/LoginPage.jsx:104 +#: src/components/core/LoginPage.jsx:102 msgid "Password input" msgstr "Introducció de contrasenya" -#: src/components/core/LoginPage.jsx:113 +#: src/components/core/LoginPage.jsx:111 msgid "Log in" msgstr "Inicia la sessió" -#: src/components/core/LoginPage.jsx:124 +#: src/components/core/LoginPage.jsx:121 msgid "More about this" msgstr "Més sobre això" -#: src/components/core/LogsButton.jsx:103 +#: src/components/core/LogsButton.jsx:101 msgid "Collecting logs..." msgstr "Recopilant registres..." -#: src/components/core/LogsButton.jsx:103 -#: src/components/core/LogsButton.jsx:106 +#: src/components/core/LogsButton.jsx:101 +#: src/components/core/LogsButton.jsx:104 msgid "Download logs" msgstr "Baixa els registres" -#: src/components/core/LogsButton.jsx:112 +#: src/components/core/LogsButton.jsx:111 msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." @@ -275,32 +261,32 @@ msgstr "" "El navegador executarà la baixada de registres així que estiguin a punt. Si " "us plau, tingueu paciència." -#: src/components/core/LogsButton.jsx:120 +#: src/components/core/LogsButton.jsx:121 msgid "Something went wrong while collecting logs. Please, try again." msgstr "" "Hi ha hagut un error durant la recopilació de registres. Torneu-ho a provar." -#: src/components/core/PasswordAndConfirmationInput.jsx:48 +#: src/components/core/PasswordAndConfirmationInput.jsx:55 msgid "Passwords do not match" msgstr "Les contrasenyes no coincideixen." -#: src/components/core/PasswordAndConfirmationInput.jsx:72 -#: src/components/network/WifiConnectionForm.jsx:120 -#: src/components/storage/iscsi/AuthFields.jsx:95 -#: src/components/storage/iscsi/AuthFields.jsx:100 -#: src/components/users/RootAuthMethods.jsx:163 +#: src/components/core/PasswordAndConfirmationInput.jsx:79 +#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/storage/iscsi/AuthFields.jsx:90 +#: src/components/storage/iscsi/AuthFields.jsx:94 +#: src/components/users/RootAuthMethods.jsx:165 msgid "Password" msgstr "Contrasenya" -#: src/components/core/PasswordAndConfirmationInput.jsx:85 +#: src/components/core/PasswordAndConfirmationInput.jsx:90 msgid "Password confirmation" msgstr "Confirmació de la contrasenya" -#: src/components/core/PasswordInput.jsx:64 +#: src/components/core/PasswordInput.jsx:61 msgid "Password visibility button" msgstr "Botó de visibilitat de la contrasenya" -#: src/components/core/Popup.jsx:100 +#: src/components/core/Popup.jsx:92 msgid "Confirm" msgstr "Confirmeu-ho" @@ -317,117 +303,106 @@ msgstr "Acabada" msgid "In progress" msgstr "En curs" -#: src/components/core/ProgressReport.jsx:70 +#: src/components/core/ProgressReport.jsx:74 msgid "Pending" msgstr "Pendent" -#: src/components/core/ProgressReport.jsx:134 +#: src/components/core/ProgressReport.jsx:138 msgid "Waiting for progress status..." msgstr "Esperant l'informe de progrés..." #: src/components/core/RowActions.jsx:64 -#: src/components/storage/PartitionsField.jsx:454 -#: src/components/storage/ProposalActionsSummary.jsx:226 +#: src/components/storage/PartitionsField.jsx:491 +#: src/components/storage/ProposalActionsSummary.jsx:233 msgid "Actions" msgstr "Accions" -#: src/components/core/SectionSkeleton.jsx:29 +#: src/components/core/SectionSkeleton.jsx:27 msgid "Waiting" msgstr "Escrivint" -#: src/components/core/Selector.jsx:126 -#: src/components/software/SoftwarePatternsSelection.jsx:212 -msgid "auto selected" -msgstr "seleccionat automàticament" - -#. TRANSLATORS: page title -#: src/components/core/ServerError.jsx:34 -msgid "Agama Error" -msgstr "Error de l'Agama" - -#: src/components/core/ServerError.jsx:38 +#: src/components/core/ServerError.jsx:47 msgid "Cannot connect to Agama server" msgstr "No es pot connectar amb el servidor d'Agama." -#: src/components/core/ServerError.jsx:43 +#: src/components/core/ServerError.jsx:51 msgid "Please, check whether it is running." msgstr "Si us plau, comproveu si s'executa." -#. TRANSLATORS: button label -#: src/components/core/ServerError.jsx:51 +#: src/components/core/ServerError.jsx:56 msgid "Reload" msgstr "Torna a carregar" -#: src/components/l10n/KeyboardSelection.jsx:45 +#: src/components/l10n/KeyboardSelection.jsx:41 msgid "Filter by description or keymap code" msgstr "Filtra per descripció o codi de mapa de tecles" -#: src/components/l10n/KeyboardSelection.jsx:85 +#: src/components/l10n/KeyboardSelection.jsx:71 msgid "None of the keymaps match the filter." msgstr "Cap dels mapes de tecles coincideix amb el filtre." -#: src/components/l10n/KeyboardSelection.jsx:92 +#: src/components/l10n/KeyboardSelection.jsx:77 msgid "Keyboard selection" msgstr "Selecció del teclat" -#: src/components/l10n/KeyboardSelection.jsx:107 -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/l10n/LocaleSelection.jsx:107 -#: src/components/l10n/TimezoneSelection.jsx:145 -#: src/components/product/ProductSelectionPage.jsx:101 +#: src/components/l10n/KeyboardSelection.jsx:90 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/l10n/LocaleSelection.jsx:92 +#: src/components/l10n/TimezoneSelection.jsx:125 +#: src/components/product/ProductSelectionPage.jsx:90 msgid "Select" msgstr "Selecciona" -#: src/components/l10n/L10nPage.jsx:60 src/components/l10n/routes.js:34 -#: src/components/overview/L10nSection.jsx:42 +#: src/components/l10n/L10nPage.jsx:53 +#: src/components/overview/L10nSection.jsx:37 src/routes/l10n.js:38 msgid "Localization" msgstr "Localització" -#: src/components/l10n/L10nPage.jsx:68 src/components/l10n/L10nPage.jsx:79 -#: src/components/l10n/L10nPage.jsx:90 +#: src/components/l10n/L10nPage.jsx:61 src/components/l10n/L10nPage.jsx:70 +#: src/components/l10n/L10nPage.jsx:80 msgid "Not selected yet" msgstr "Encara no s'ha seleccionat." -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/network/NetworkPage.jsx:102 -#: src/components/storage/InstallationDeviceField.jsx:105 -#: src/components/storage/ProposalActionsSummary.jsx:228 -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/storage/InstallationDeviceField.jsx:108 +#: src/components/storage/ProposalActionsSummary.jsx:238 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Change" msgstr "Canvia" -#: src/components/l10n/L10nPage.jsx:78 +#: src/components/l10n/L10nPage.jsx:70 msgid "Keyboard" msgstr "Teclat" -#: src/components/l10n/L10nPage.jsx:89 +#: src/components/l10n/L10nPage.jsx:79 msgid "Time zone" msgstr "Zona horària" -#: src/components/l10n/LocaleSelection.jsx:44 +#: src/components/l10n/LocaleSelection.jsx:39 msgid "Filter by language, territory or locale code" msgstr "Filtra per llengua, territori o codi local" -#: src/components/l10n/LocaleSelection.jsx:84 +#: src/components/l10n/LocaleSelection.jsx:72 msgid "None of the locales match the filter." msgstr "Cap de les llengües coincideix amb el filtre." -#: src/components/l10n/LocaleSelection.jsx:91 +#: src/components/l10n/LocaleSelection.jsx:78 msgid "Locale selection" msgstr "Selecció de la llengua" -#: src/components/l10n/TimezoneSelection.jsx:71 +#: src/components/l10n/TimezoneSelection.jsx:64 msgid "Filter by territory, time zone code or UTC offset" msgstr "Filtra per territori, codi de zona horària o desplaçament d'UTC" -#: src/components/l10n/TimezoneSelection.jsx:122 +#: src/components/l10n/TimezoneSelection.jsx:101 msgid "None of the time zones match the filter." msgstr "Cap de les zones horàries coincideix amb el filtre." -#: src/components/l10n/TimezoneSelection.jsx:129 +#: src/components/l10n/TimezoneSelection.jsx:107 msgid " Timezone selection" msgstr " Selecció de la zona horària" @@ -436,111 +411,111 @@ msgid "Loading installation environment, please wait." msgstr "Carregant l'entorn d'instal·lació. Espereu, si us plau." #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:78 -#: src/components/network/DnsDataList.jsx:84 +#: src/components/network/AddressesDataList.jsx:88 +#: src/components/network/DnsDataList.jsx:95 msgid "Remove" msgstr "Suprimeix" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:90 -#: src/components/network/AddressesDataList.jsx:91 +#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:101 #: src/components/network/IpAddressInput.jsx:33 msgid "IP Address" msgstr "Adreça IP" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:99 -#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:109 +#: src/components/network/AddressesDataList.jsx:110 msgid "Prefix length or netmask" msgstr "Longitud del prefix o màscara de xarxa" -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add an address" msgstr "Afegeix-hi una adreça" #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add another address" msgstr "Afegeix-hi una altra adreça" -#: src/components/network/AddressesDataList.jsx:121 +#: src/components/network/AddressesDataList.jsx:131 msgid "Addresses" msgstr "Adreces" -#: src/components/network/AddressesDataList.jsx:123 +#: src/components/network/AddressesDataList.jsx:133 msgid "Addresses data list" msgstr "Llista de dades d'adreces" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:67 -#: src/components/network/ConnectionsTable.jsx:95 -#: src/components/storage/ZFCPPage.jsx:361 +#: src/components/network/ConnectionsTable.jsx:64 +#: src/components/network/ConnectionsTable.jsx:92 +#: src/components/storage/ZFCPPage.jsx:381 #: src/components/storage/iscsi/InitiatorForm.jsx:52 #: src/components/storage/iscsi/InitiatorPresenter.jsx:68 #: src/components/storage/iscsi/InitiatorPresenter.jsx:85 -#: src/components/storage/iscsi/NodesPresenter.jsx:100 -#: src/components/storage/iscsi/NodesPresenter.jsx:121 +#: src/components/storage/iscsi/NodesPresenter.jsx:98 +#: src/components/storage/iscsi/NodesPresenter.jsx:119 msgid "Name" msgstr "Nom" #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:69 -#: src/components/network/ConnectionsTable.jsx:96 +#: src/components/network/ConnectionsTable.jsx:66 +#: src/components/network/ConnectionsTable.jsx:93 msgid "IP addresses" msgstr "Adreces IP" -#: src/components/network/ConnectionsTable.jsx:77 -#: src/components/network/WifiNetworksListPage.jsx:100 -#: src/components/network/WifiNetworksListPage.jsx:124 -#: src/components/storage/PartitionsField.jsx:320 +#: src/components/network/ConnectionsTable.jsx:74 +#: src/components/network/WifiNetworksListPage.jsx:107 +#: src/components/network/WifiNetworksListPage.jsx:130 +#: src/components/storage/PartitionsField.jsx:347 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 -#: src/components/users/FirstUser.jsx:116 +#: src/components/users/FirstUser.jsx:120 msgid "Edit" msgstr "Edita" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:80 -#: src/components/network/IpSettingsForm.jsx:136 +#: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/IpSettingsForm.jsx:151 #, c-format msgid "Edit connection %s" msgstr "Edita la connexió %s" -#: src/components/network/ConnectionsTable.jsx:84 -#: src/components/network/WifiNetworksListPage.jsx:103 -#: src/components/network/WifiNetworksListPage.jsx:127 +#: src/components/network/ConnectionsTable.jsx:81 +#: src/components/network/WifiNetworksListPage.jsx:109 +#: src/components/network/WifiNetworksListPage.jsx:137 msgid "Forget" msgstr "Oblida-la" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:86 +#: src/components/network/ConnectionsTable.jsx:83 #, c-format msgid "Forget connection %s" msgstr "Oblida la connexió %s" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:101 +#: src/components/network/ConnectionsTable.jsx:98 #, c-format msgid "Actions for connection %s" msgstr "Accions per a la connexió %s" #. TRANSLATORS: input field name -#: src/components/network/DnsDataList.jsx:75 -#: src/components/network/DnsDataList.jsx:76 +#: src/components/network/DnsDataList.jsx:81 +#: src/components/network/DnsDataList.jsx:82 msgid "Server IP" msgstr "IP del servidor" -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add DNS" msgstr "Afegeix-hi un DNS" #. TRANSLATORS: button label -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add another DNS" msgstr "Afegeix-hi un altre DNS" -#: src/components/network/DnsDataList.jsx:98 +#: src/components/network/DnsDataList.jsx:109 msgid "DNS" msgstr "DNS" @@ -550,42 +525,42 @@ msgid "IP prefix or netmask" msgstr "Prefix IP o màscara de xarxa" #. TRANSLATORS: error message -#: src/components/network/IpSettingsForm.jsx:90 +#: src/components/network/IpSettingsForm.jsx:104 msgid "At least one address must be provided for selected mode" msgstr "S'ha de proporcionar almenys una adreça per al mode seleccionat." #. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) -#: src/components/network/IpSettingsForm.jsx:145 -#: src/components/network/IpSettingsForm.jsx:150 -#: src/components/network/IpSettingsForm.jsx:152 +#: src/components/network/IpSettingsForm.jsx:160 +#: src/components/network/IpSettingsForm.jsx:165 +#: src/components/network/IpSettingsForm.jsx:167 msgid "Mode" msgstr "Mode" -#: src/components/network/IpSettingsForm.jsx:156 +#: src/components/network/IpSettingsForm.jsx:174 msgid "Automatic (DHCP)" msgstr "Automàtic (DHCP)" #. TRANSLATORS: manual network configuration mode with a static IP address -#: src/components/network/IpSettingsForm.jsx:158 +#: src/components/network/IpSettingsForm.jsx:177 #: src/components/storage/iscsi/NodeStartupOptions.js:25 msgid "Manual" msgstr "Manual" #. TRANSLATORS: network gateway configuration -#: src/components/network/IpSettingsForm.jsx:166 -#: src/components/network/IpSettingsForm.jsx:169 +#: src/components/network/IpSettingsForm.jsx:185 +#: src/components/network/IpSettingsForm.jsx:188 msgid "Gateway" msgstr "Passarel·la" -#: src/components/network/IpSettingsForm.jsx:178 +#: src/components/network/IpSettingsForm.jsx:196 msgid "Gateway can be defined only in 'Manual' mode" msgstr "La passarel·la només es pot definir en mode manual." -#: src/components/network/NetworkPage.jsx:85 +#: src/components/network/NetworkPage.jsx:93 msgid "No Wi-Fi supported" msgstr "No és compatible amb Wi-Fi." -#: src/components/network/NetworkPage.jsx:86 +#: src/components/network/NetworkPage.jsx:95 msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." @@ -593,41 +568,41 @@ msgstr "" "El sistema no admet connexions de wifi, probablement a causa de maquinari " "que manca o que està inhabilitat." -#: src/components/network/NetworkPage.jsx:99 +#: src/components/network/NetworkPage.jsx:109 msgid "Wi-Fi" msgstr "Wifi" #. TRANSLATORS: button label, connect to a WiFi network -#: src/components/network/NetworkPage.jsx:102 -#: src/components/network/WifiConnectionForm.jsx:128 -#: src/components/network/WifiNetworksListPage.jsx:97 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:130 +#: src/components/network/WifiNetworksListPage.jsx:105 msgid "Connect" msgstr "Connecta't" -#: src/components/network/NetworkPage.jsx:109 +#: src/components/network/NetworkPage.jsx:119 #, c-format msgid "Conected to %s" msgstr "Connectat amb %s" -#: src/components/network/NetworkPage.jsx:114 +#: src/components/network/NetworkPage.jsx:126 msgid "No connected yet" msgstr "Encara no s'ha connetat." -#: src/components/network/NetworkPage.jsx:115 +#: src/components/network/NetworkPage.jsx:127 msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." msgstr "" "El sistema encara no s'ha configurat per connectar-se a una xarxa de wifi." -#: src/components/network/NetworkPage.jsx:136 +#: src/components/network/NetworkPage.jsx:156 msgid "Wired" msgstr "Amb fil" -#: src/components/network/NetworkPage.jsx:139 +#: src/components/network/NetworkPage.jsx:160 msgid "No wired connections found" msgstr "No s'ha trobat cap connexió amb fil." -#: src/components/network/NetworkPage.jsx:149 +#: src/components/network/NetworkPage.jsx:173 #: src/components/network/routes.js:59 msgid "Network" msgstr "Xarxa" @@ -643,16 +618,16 @@ msgstr "Cap" msgid "WPA & WPA2 Personal" msgstr "WPA i WPA2 personal" -#: src/components/network/WifiConnectionForm.jsx:86 -#: src/components/product/ProductRegistrationPage.jsx:69 -#: src/components/storage/ZFCPDiskForm.jsx:108 -#: src/components/storage/iscsi/DiscoverForm.jsx:110 -#: src/components/storage/iscsi/LoginForm.jsx:72 -#: src/components/users/FirstUserForm.jsx:203 +#: src/components/network/WifiConnectionForm.jsx:85 +#: src/components/product/ProductRegistrationPage.jsx:68 +#: src/components/storage/ZFCPDiskForm.jsx:105 +#: src/components/storage/iscsi/DiscoverForm.jsx:98 +#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/users/FirstUserForm.jsx:217 msgid "Something went wrong" msgstr "Alguna cosa ha anat malament." -#: src/components/network/WifiConnectionForm.jsx:87 +#: src/components/network/WifiConnectionForm.jsx:86 msgid "Please, review provided settings and try again." msgstr "" "Si us plau, reviseu la configuració proporcionada i torneu-ho a provar." @@ -664,97 +639,97 @@ msgid "SSID" msgstr "SSID" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:104 -#: src/components/network/WifiConnectionForm.jsx:107 +#: src/components/network/WifiConnectionForm.jsx:105 +#: src/components/network/WifiConnectionForm.jsx:108 msgid "Security" msgstr "Seguretat" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:116 +#: src/components/network/WifiConnectionForm.jsx:117 msgid "WPA Password" msgstr "Contrasenya de WPA" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:51 -#: src/components/network/WifiNetworksListPage.jsx:111 +#: src/components/network/WifiNetworksListPage.jsx:63 +#: src/components/network/WifiNetworksListPage.jsx:117 msgid "Connecting" msgstr "Connectant" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:54 -#: src/components/network/WifiNetworksListPage.jsx:115 -#: src/components/network/WifiNetworksListPage.jsx:154 +#: src/components/network/WifiNetworksListPage.jsx:66 +#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:164 msgid "Connected" msgstr "Connectat" #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:59 -#: src/components/network/WifiNetworksListPage.jsx:113 +#: src/components/network/WifiNetworksListPage.jsx:71 +#: src/components/network/WifiNetworksListPage.jsx:119 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" msgstr "Desconnectat" -#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:127 msgid "Disconnect" msgstr "Desconnecta" -#: src/components/network/WifiNetworksListPage.jsx:142 +#: src/components/network/WifiNetworksListPage.jsx:150 msgid "Connect to a hidden network" msgstr "Connecta't a una xarxa oculta" -#: src/components/network/WifiNetworksListPage.jsx:153 +#: src/components/network/WifiNetworksListPage.jsx:161 msgid "configured" msgstr "configurat" -#: src/components/network/WifiNetworksListPage.jsx:244 +#: src/components/network/WifiNetworksListPage.jsx:265 msgid "Connect to hidden network" msgstr "Connecta't a una xarxa oculta" -#: src/components/network/WifiSelectorPage.jsx:131 +#: src/components/network/WifiSelectorPage.jsx:136 msgid "Connect to a Wi-Fi network" msgstr "Connecteu-vos a una xarxa Wi-Fi" #. TRANSLATORS: %s will be replaced by a language name and territory, example: #. "English (United States)". -#: src/components/overview/L10nSection.jsx:38 +#: src/components/overview/L10nSection.jsx:33 #, c-format msgid "The system will use %s as its default language." msgstr "El sistema usarà el %s com a llengua per defecte." -#: src/components/overview/OverviewPage.jsx:45 +#: src/components/overview/OverviewPage.jsx:47 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "Usuaris" -#: src/components/overview/OverviewPage.jsx:46 +#: src/components/overview/OverviewPage.jsx:48 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:290 +#: src/components/storage/ProposalPage.jsx:307 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "Emmagatzematge" -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/overview/SoftwareSection.jsx:86 #: src/components/software/SoftwarePage.jsx:155 #: src/components/software/routes.js:32 msgid "Software" msgstr "Programari" -#: src/components/overview/OverviewPage.jsx:52 +#: src/components/overview/OverviewPage.jsx:54 msgid "Ready for installation" msgstr "A punt per a la instal·lació" -#: src/components/overview/OverviewPage.jsx:102 +#: src/components/overview/OverviewPage.jsx:104 msgid "Installation" msgstr "Instal·lació" -#: src/components/overview/OverviewPage.jsx:103 +#: src/components/overview/OverviewPage.jsx:105 msgid "Before installing, please check the following problems." msgstr "Abans d'instal·lar, comproveu els problemes següents." -#: src/components/overview/OverviewPage.jsx:114 +#: src/components/overview/OverviewPage.jsx:116 msgid "" "Take your time to check your configuration before starting the installation " "process." @@ -762,7 +737,7 @@ msgstr "" "Dediqueu el temps que calgui a comprovar la configuració abans de començar " "el procés d'instal·lació." -#: src/components/overview/OverviewPage.jsx:123 +#: src/components/overview/OverviewPage.jsx:125 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -849,14 +824,14 @@ msgstr "" "Instal·la en un grup de volums nou del Gestor de Volums Lògics (LVM) a %s " "amb una estratègia personalitzada per trobar l'espai necessari." -#: src/components/overview/StorageSection.jsx:175 -#: src/components/storage/InstallationDeviceField.jsx:63 +#: src/components/overview/StorageSection.jsx:179 +#: src/components/storage/InstallationDeviceField.jsx:66 msgid "No device selected yet" msgstr "Encara no s'ha seleccionat cap dispositiu." #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:182 +#: src/components/overview/StorageSection.jsx:186 #, c-format msgid "Install using device %s shrinking existing partitions as needed" msgstr "" @@ -865,21 +840,21 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:186 +#: src/components/overview/StorageSection.jsx:190 #, c-format msgid "Install using device %s without modifying existing partitions" msgstr "Instal·la al dispositiu %s sense modificar-ne les particions existents" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:190 +#: src/components/overview/StorageSection.jsx:194 #, c-format msgid "Install using device %s and deleting all its content" msgstr "Instal·la al dispositiu %s suprimint-ne tot el contingut" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:195 +#: src/components/overview/StorageSection.jsx:199 #, c-format msgid "Install using device %s with a custom strategy to find the needed space" msgstr "" @@ -895,19 +870,15 @@ msgstr "Resum" msgid "Register %s" msgstr "Registra %s" -#: src/components/product/ProductRegistrationPage.jsx:74 +#: src/components/product/ProductRegistrationPage.jsx:73 msgid "Registration code" msgstr "Codi de registre" -#: src/components/product/ProductRegistrationPage.jsx:77 +#: src/components/product/ProductRegistrationPage.jsx:76 msgid "Email" msgstr "Adreça electrònica" -#: src/components/product/ProductSelectionPage.jsx:58 -msgid "Loading available products, please wait..." -msgstr "Carregant els productes disponibles. Espereu, si us plau..." - -#: src/components/product/ProductSelectionProgress.jsx:53 +#: src/components/product/ProductSelectionProgress.jsx:49 msgid "Configuring the product, please wait ..." msgstr "Configurant el producte. Espereu, si us plau..." @@ -926,7 +897,7 @@ msgid "Encrypted Device" msgstr "Dispositiu encriptat" #. TRANSLATORS: field label -#: src/components/questions/LuksActivationQuestion.jsx:69 +#: src/components/questions/LuksActivationQuestion.jsx:67 msgid "Encryption Password" msgstr "Contrasenya d'encriptació" @@ -947,17 +918,21 @@ msgstr "Patrons seleccionats" msgid "Change selection" msgstr "Canvia la selecció" -#: src/components/software/SoftwarePatternsSelection.jsx:230 +#: src/components/software/SoftwarePatternsSelection.jsx:223 +msgid "auto selected" +msgstr "seleccionat automàticament" + +#: src/components/software/SoftwarePatternsSelection.jsx:241 msgid "None of the patterns match the filter." msgstr "Cap dels patrons coincideix amb el filtre." -#: src/components/software/SoftwarePatternsSelection.jsx:238 +#: src/components/software/SoftwarePatternsSelection.jsx:248 msgid "Software selection" msgstr "Selecció de programari" #. TRANSLATORS: search field placeholder text -#: src/components/software/SoftwarePatternsSelection.jsx:241 -#: src/components/software/SoftwarePatternsSelection.jsx:242 +#: src/components/software/SoftwarePatternsSelection.jsx:251 +#: src/components/software/SoftwarePatternsSelection.jsx:252 msgid "Filter by pattern title or description" msgstr "Filtra per títol o descripció del patró" @@ -968,7 +943,7 @@ msgstr "Filtra per títol o descripció del patró" msgid "Installation will take %s." msgstr "La instal·lació necessitarà %s." -#: src/components/software/UsedSize.jsx:38 +#: src/components/software/UsedSize.jsx:37 msgid "This space includes the base system and the selected software patterns." msgstr "" "Aquest espai inclou el sistema de base i els patrons de programari " @@ -978,24 +953,23 @@ msgstr "" msgid "Change boot options" msgstr "Canvia les opcions d'arrencada" -#: src/components/storage/BootConfigField.jsx:87 +#: src/components/storage/BootConfigField.jsx:81 msgid "Installation will not configure partitions for booting." msgstr "La instal·lació no configurarà les particions per a l'arrencada." -#: src/components/storage/BootConfigField.jsx:89 +#: src/components/storage/BootConfigField.jsx:85 msgid "" "Installation will configure partitions for booting at the installation disk." msgstr "" "La instal·lació configurarà les particions per arrencar al disc " "d'instal·lació." -#. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/BootConfigField.jsx:92 +#: src/components/storage/BootConfigField.jsx:89 #, c-format msgid "Installation will configure partitions for booting at %s." msgstr "La instal·lació configurarà les particions per arrencar a %s." -#: src/components/storage/BootSelection.jsx:127 +#: src/components/storage/BootSelection.jsx:132 msgid "" "To ensure the new system is able to boot, the installer may need to create " "or configure some partitions in the appropriate disk." @@ -1003,43 +977,43 @@ msgstr "" "Per garantir que el sistema nou pugui arrencar, és possible que " "l'instal·lador hagi de crear o configurar algunes particions al disc adequat." -#: src/components/storage/BootSelection.jsx:133 +#: src/components/storage/BootSelection.jsx:138 msgid "Partitions to boot will be allocated at the installation disk." msgstr "Les particions per a l'arrencada s'assignaran al disc d'instal·lació." #. TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") -#: src/components/storage/BootSelection.jsx:138 +#: src/components/storage/BootSelection.jsx:143 #, c-format msgid "Partitions to boot will be allocated at the installation disk (%s)." msgstr "" "Les particions per a l'arrencada s'assignaran al disc d'instal·lació (%s)." -#: src/components/storage/BootSelection.jsx:154 +#: src/components/storage/BootSelection.jsx:159 msgid "Select booting partition" msgstr "Seleccioneu la partició d'arrencada" -#: src/components/storage/BootSelection.jsx:170 +#: src/components/storage/BootSelection.jsx:180 #: src/components/storage/iscsi/NodeStartupOptions.js:27 msgid "Automatic" msgstr "Automàtica" -#: src/components/storage/BootSelection.jsx:183 +#: src/components/storage/BootSelection.jsx:198 msgid "Select a disk" msgstr "Seleccioneu un disc" -#: src/components/storage/BootSelection.jsx:189 +#: src/components/storage/BootSelection.jsx:204 msgid "Partitions to boot will be allocated at the following device." msgstr "Les particions per a l'arrencada s'assignaran al dispositiu següent." -#: src/components/storage/BootSelection.jsx:192 +#: src/components/storage/BootSelection.jsx:207 msgid "Choose a disk for placing the boot loader" msgstr "Trieu un disc per posar-hi el carregador d'arrencada" -#: src/components/storage/BootSelection.jsx:210 +#: src/components/storage/BootSelection.jsx:230 msgid "Do not configure" msgstr "No ho configuris" -#: src/components/storage/BootSelection.jsx:215 +#: src/components/storage/BootSelection.jsx:236 msgid "" "No partitions will be automatically configured for booting. Use with caution." msgstr "" @@ -1050,125 +1024,117 @@ msgstr "" msgid "Waiting for progress report" msgstr "Esperant l'informe de progrés" -#: src/components/storage/DASDFormatProgress.jsx:68 +#: src/components/storage/DASDFormatProgress.jsx:67 msgid "Formatting DASD devices" msgstr "Formatatge de dispositius DASD" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "No" msgstr "No" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "Yes" msgstr "Sí" -#: src/components/storage/DASDTable.jsx:63 -#: src/components/storage/ZFCPDiskForm.jsx:112 -#: src/components/storage/ZFCPPage.jsx:302 -#: src/components/storage/ZFCPPage.jsx:362 +#: src/components/storage/DASDTable.jsx:69 +#: src/components/storage/ZFCPDiskForm.jsx:110 +#: src/components/storage/ZFCPPage.jsx:324 +#: src/components/storage/ZFCPPage.jsx:382 msgid "Channel ID" msgstr "Identificador del canal" #. TRANSLATORS: table header -#: src/components/storage/DASDTable.jsx:64 -#: src/components/storage/ZFCPPage.jsx:303 -#: src/components/storage/iscsi/NodesPresenter.jsx:104 -#: src/components/storage/iscsi/NodesPresenter.jsx:125 -#: src/components/users/RootAuthMethods.jsx:157 +#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/ZFCPPage.jsx:325 +#: src/components/storage/iscsi/NodesPresenter.jsx:102 +#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/users/RootAuthMethods.jsx:159 msgid "Status" msgstr "Estat" -#: src/components/storage/DASDTable.jsx:65 -#: src/components/storage/DeviceSelectorTable.jsx:186 -#: src/components/storage/ProposalResultTable.jsx:120 -#: src/components/storage/SpaceActionsTable.jsx:141 -#: src/components/storage/VolumeLocationSelectorTable.jsx:100 +#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DeviceSelectorTable.jsx:197 +#: src/components/storage/ProposalResultTable.jsx:130 +#: src/components/storage/SpaceActionsTable.jsx:200 +#: src/components/storage/VolumeLocationSelectorTable.jsx:107 msgid "Device" msgstr "Dispositiu" -#: src/components/storage/DASDTable.jsx:66 +#: src/components/storage/DASDTable.jsx:72 msgid "Type" msgstr "Tipus" #. TRANSLATORS: table header, the column contains "Yes"/"No" values #. for the DIAG access mode (special disk access mode on IBM mainframes), #. usually keep untranslated -#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/DASDTable.jsx:76 msgid "DIAG" msgstr "DIAG" -#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DASDTable.jsx:77 msgid "Formatted" msgstr "Formatat" -#: src/components/storage/DASDTable.jsx:72 +#: src/components/storage/DASDTable.jsx:78 msgid "Partition Info" msgstr "Informació de la partició" #. TRANSLATORS: drop down menu label -#: src/components/storage/DASDTable.jsx:107 +#: src/components/storage/DASDTable.jsx:115 msgid "Perform an action" msgstr "Fes una acció" -#. TRANSLATORS: drop down menu action, activate the device -#: src/components/storage/DASDTable.jsx:113 -#: src/components/storage/ZFCPPage.jsx:333 +#: src/components/storage/DASDTable.jsx:122 +#: src/components/storage/ZFCPPage.jsx:353 msgid "Activate" msgstr "Activa" -#. TRANSLATORS: drop down menu action, deactivate the device -#: src/components/storage/DASDTable.jsx:115 -#: src/components/storage/ZFCPPage.jsx:375 +#: src/components/storage/DASDTable.jsx:126 +#: src/components/storage/ZFCPPage.jsx:395 msgid "Deactivate" msgstr "Desactiva" -#. TRANSLATORS: drop down menu action, enable DIAG access method -#: src/components/storage/DASDTable.jsx:118 +#: src/components/storage/DASDTable.jsx:131 msgid "Set DIAG On" msgstr "Activa la diagnosi" -#. TRANSLATORS: drop down menu action, disable DIAG access method -#: src/components/storage/DASDTable.jsx:120 +#: src/components/storage/DASDTable.jsx:135 msgid "Set DIAG Off" msgstr "Desactiva la diagnosi" -#. TRANSLATORS: drop down menu action, format the disk -#: src/components/storage/DASDTable.jsx:123 +#: src/components/storage/DASDTable.jsx:140 msgid "Format" msgstr "Formata" -#: src/components/storage/DASDTable.jsx:223 -#: src/components/storage/DASDTable.jsx:224 +#: src/components/storage/DASDTable.jsx:261 +#: src/components/storage/DASDTable.jsx:262 msgid "Filter by min channel" msgstr "Filtra per canal mínim" -#: src/components/storage/DASDTable.jsx:231 +#: src/components/storage/DASDTable.jsx:269 msgid "Remove min channel filter" msgstr "Suprimeix el filtre del canal mínim" -#: src/components/storage/DASDTable.jsx:244 -#: src/components/storage/DASDTable.jsx:245 +#: src/components/storage/DASDTable.jsx:283 +#: src/components/storage/DASDTable.jsx:284 msgid "Filter by max channel" msgstr "Filtra per canal màxim" -#: src/components/storage/DASDTable.jsx:252 +#: src/components/storage/DASDTable.jsx:291 msgid "Remove max channel filter" msgstr "Suprimeix el filtre de canal màxim" -#: src/components/storage/DeviceSelection.jsx:101 +#: src/components/storage/DeviceSelection.jsx:108 msgid "Loading data, please wait a second..." msgstr "Carregant dades. Espereu, si us plau..." -#. TRANSLATORS: description for using plain partitions for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:136 +#: src/components/storage/DeviceSelection.jsx:144 msgid "" "The file systems will be allocated by default as [new partitions in the " "selected device]." @@ -1176,10 +1142,7 @@ msgstr "" "Els sistemes de fitxers s'assignaran per defecte com a [particions noves al " "dispositiu seleccionat]." -#. TRANSLATORS: description for using logical volumes for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:141 +#: src/components/storage/DeviceSelection.jsx:151 msgid "" "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 " @@ -1189,135 +1152,135 @@ msgstr "" "nou grup de volums d'LVM]. Els volums físics corresponents es crearan segons " "demanda com a particions noves als dispositius seleccionats." -#: src/components/storage/DeviceSelection.jsx:149 +#: src/components/storage/DeviceSelection.jsx:160 msgid "Select installation device" msgstr "Seleccioneu el dispositiu d'instal·lació" -#: src/components/storage/DeviceSelection.jsx:155 +#: src/components/storage/DeviceSelection.jsx:166 msgid "Install new system on" msgstr "Instal·la el sistema nou" -#: src/components/storage/DeviceSelection.jsx:158 +#: src/components/storage/DeviceSelection.jsx:169 msgid "An existing disk" msgstr "un disc existent" -#: src/components/storage/DeviceSelection.jsx:167 +#: src/components/storage/DeviceSelection.jsx:178 msgid "A new LVM Volume Group" msgstr "un grup de volums d'LVM nou" -#: src/components/storage/DeviceSelection.jsx:192 +#: src/components/storage/DeviceSelection.jsx:203 msgid "Device selector for target disk" msgstr "Selector de dispositiu per al disc de destinació" -#: src/components/storage/DeviceSelection.jsx:215 +#: src/components/storage/DeviceSelection.jsx:226 msgid "Device selector for new LVM volume group" msgstr "Selector de dispositius per al grup de volums LVM nou" -#: src/components/storage/DeviceSelection.jsx:228 +#: src/components/storage/DeviceSelection.jsx:242 msgid "Prepare more devices by configuring advanced" msgstr "Prepareu més dispositius mitjançant la configuració avançada" -#: src/components/storage/DeviceSelection.jsx:229 +#: src/components/storage/DeviceSelection.jsx:243 msgid "storage techs" msgstr "tecnologies d'emmagatzematge" #. TRANSLATORS: multipath device type -#: src/components/storage/DeviceSelectorTable.jsx:57 +#: src/components/storage/DeviceSelectorTable.jsx:61 msgid "Multipath" msgstr "Multicamí" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/DeviceSelectorTable.jsx:62 +#: src/components/storage/DeviceSelectorTable.jsx:66 #, c-format msgid "DASD %s" msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/DeviceSelectorTable.jsx:67 +#: src/components/storage/DeviceSelectorTable.jsx:71 #, c-format msgid "Software %s" msgstr "Programari %s" -#: src/components/storage/DeviceSelectorTable.jsx:72 +#: src/components/storage/DeviceSelectorTable.jsx:76 msgid "SD Card" msgstr "Targeta SD" #. TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" -#: src/components/storage/DeviceSelectorTable.jsx:77 +#: src/components/storage/DeviceSelectorTable.jsx:81 #, c-format msgid "%s disk" msgstr "Disc %s" -#: src/components/storage/DeviceSelectorTable.jsx:78 +#: src/components/storage/DeviceSelectorTable.jsx:82 msgid "Disk" msgstr "Disc" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:98 +#: src/components/storage/DeviceSelectorTable.jsx:102 #, c-format msgid "Members: %s" msgstr "Membres: %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:107 +#: src/components/storage/DeviceSelectorTable.jsx:111 #, c-format msgid "Devices: %s" msgstr "Dispositius: %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/DeviceSelectorTable.jsx:116 +#: src/components/storage/DeviceSelectorTable.jsx:120 #, c-format msgid "Wires: %s" msgstr "Cables: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/DeviceSelectorTable.jsx:152 +#: src/components/storage/DeviceSelectorTable.jsx:155 #, c-format msgid "%s with %d partitions" msgstr "%s amb %d particions" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/DeviceSelectorTable.jsx:158 -#: src/components/storage/SpaceActionsTable.jsx:114 +#: src/components/storage/DeviceSelectorTable.jsx:161 +#: src/components/storage/SpaceActionsTable.jsx:175 msgid "No content found" msgstr "No s'ha trobat contingut." -#: src/components/storage/DeviceSelectorTable.jsx:187 -#: src/components/storage/PartitionsField.jsx:450 -#: src/components/storage/ProposalResultTable.jsx:122 -#: src/components/storage/SpaceActionsTable.jsx:142 -#: src/components/storage/VolumeLocationSelectorTable.jsx:101 +#: src/components/storage/DeviceSelectorTable.jsx:198 +#: src/components/storage/PartitionsField.jsx:487 +#: src/components/storage/ProposalResultTable.jsx:132 +#: src/components/storage/SpaceActionsTable.jsx:201 +#: src/components/storage/VolumeLocationSelectorTable.jsx:108 msgid "Details" msgstr "Detalls" -#: src/components/storage/DeviceSelectorTable.jsx:188 -#: src/components/storage/PartitionsField.jsx:451 -#: src/components/storage/ProposalResultTable.jsx:123 -#: src/components/storage/SpaceActionsTable.jsx:143 -#: src/components/storage/VolumeFields.jsx:474 -#: src/components/storage/VolumeLocationSelectorTable.jsx:103 +#: src/components/storage/DeviceSelectorTable.jsx:199 +#: src/components/storage/PartitionsField.jsx:488 +#: src/components/storage/ProposalResultTable.jsx:133 +#: src/components/storage/SpaceActionsTable.jsx:202 +#: src/components/storage/VolumeFields.jsx:488 +#: src/components/storage/VolumeLocationSelectorTable.jsx:113 msgid "Size" msgstr "Mida" -#: src/components/storage/DevicesTechMenu.jsx:44 +#: src/components/storage/DevicesTechMenu.jsx:38 msgid "Manage and format" msgstr "Gestió i formatatge" -#: src/components/storage/DevicesTechMenu.jsx:62 +#: src/components/storage/DevicesTechMenu.jsx:52 msgid "Activate disks" msgstr "Activa els discs" -#: src/components/storage/DevicesTechMenu.jsx:64 +#: src/components/storage/DevicesTechMenu.jsx:53 msgid "zFCP" msgstr "zFCP" -#: src/components/storage/DevicesTechMenu.jsx:80 +#: src/components/storage/DevicesTechMenu.jsx:66 msgid "Connect to iSCSI targets" msgstr "Connecta amb objectius iSCSI" -#: src/components/storage/DevicesTechMenu.jsx:82 +#: src/components/storage/DevicesTechMenu.jsx:67 #: src/components/storage/routes.js:37 msgid "iSCSI" msgstr "iSCSI" @@ -1327,7 +1290,7 @@ msgstr "iSCSI" msgid "Encryption" msgstr "Encriptació" -#: src/components/storage/EncryptionField.jsx:39 +#: src/components/storage/EncryptionField.jsx:40 msgid "" "Protection for the information stored at the device, including data, " "programs, and system files." @@ -1335,27 +1298,27 @@ msgstr "" "Protecció per a la informació emmagatzemada al dispositiu, incloses les " "dades, els programes i els fitxers del sistema." -#: src/components/storage/EncryptionField.jsx:42 +#: src/components/storage/EncryptionField.jsx:44 msgid "disabled" msgstr "inhabilitada" -#: src/components/storage/EncryptionField.jsx:43 +#: src/components/storage/EncryptionField.jsx:45 msgid "enabled" msgstr "habilitada" -#: src/components/storage/EncryptionField.jsx:44 +#: src/components/storage/EncryptionField.jsx:46 msgid "using TPM unlocking" msgstr "usant el desblocatge de TPM" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Enable" msgstr "Habilita" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Modify" msgstr "Modifica" -#: src/components/storage/EncryptionSettingsDialog.jsx:37 +#: src/components/storage/EncryptionSettingsDialog.jsx:38 msgid "" "Full Disk Encryption (FDE) allows to protect the information stored at the " "device, including data, programs, and system files." @@ -1364,16 +1327,14 @@ msgstr "" "emmagatzemada al dispositiu, incloses dades, programes i fitxers del sistema." #. TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation -#: src/components/storage/EncryptionSettingsDialog.jsx:40 +#: src/components/storage/EncryptionSettingsDialog.jsx:42 msgid "" "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot" msgstr "" "Useu el mòdul de plataforma de confiança (TPM) per fer-ne la desencriptació " "automàticament a cada arrencada." -#. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing -#. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. -#: src/components/storage/EncryptionSettingsDialog.jsx:43 +#: src/components/storage/EncryptionSettingsDialog.jsx:46 msgid "" "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 " @@ -1383,12 +1344,12 @@ msgstr "" "verificar la integritat del sistema. El segellat de TPM requereix que el nou " "sistema s'iniciï directament a la primera execució." -#: src/components/storage/EncryptionSettingsDialog.jsx:114 +#: src/components/storage/EncryptionSettingsDialog.jsx:129 msgid "Encrypt the system" msgstr "Encripta el sistema" #: src/components/storage/InstallationDeviceField.jsx:36 -#: src/components/storage/VolumeLocationSelectorTable.jsx:58 +#: src/components/storage/VolumeLocationSelectorTable.jsx:61 msgid "Installation device" msgstr "Dispositiu d'instal·lació" @@ -1407,71 +1368,70 @@ msgstr "Sistemes de fitxers creats com a particions noves a %s" msgid "File systems created at a new LVM volume group" msgstr "Sistemes de fitxers creats en un nou grup de volums d'LVM" -#. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) -#: src/components/storage/InstallationDeviceField.jsx:59 +#: src/components/storage/InstallationDeviceField.jsx:60 #, c-format msgid "File systems created at a new LVM volume group on %s" msgstr "Sistemes de fitxers creats en un nou grup de volums d'LVM a %s" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:73 +#: src/components/storage/PartitionsField.jsx:84 #, c-format msgid "at least %s" msgstr "almenys %s" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:97 +#: src/components/storage/PartitionsField.jsx:108 #, c-format msgid "Transactional Btrfs root volume (%s)" msgstr "Volum d'arrel de Btrfs transaccional (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:99 +#: src/components/storage/PartitionsField.jsx:110 #, c-format msgid "Transactional Btrfs root partition (%s)" msgstr "Partició d'arrel Btrfs transaccional (%s)" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:104 +#: src/components/storage/PartitionsField.jsx:115 #, c-format msgid "Btrfs root volume with snapshots (%s)" msgstr "Volum d'arrel Btrfs amb instantànies (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:106 +#: src/components/storage/PartitionsField.jsx:117 #, c-format msgid "Btrfs root partition with snapshots (%s)" msgstr "Partició d'arrel Btrfs amb instantànies (%s)" #. TRANSLATORS: This results in something like "Mount /dev/sda3 at /home (25 GiB)" since #. %1$s is replaced by the device name, %2$s by the mount point and %3$s by the size -#: src/components/storage/PartitionsField.jsx:115 +#: src/components/storage/PartitionsField.jsx:126 #, c-format msgid "Mount %1$s at %2$s (%3$s)" msgstr "Munta %1$s a %2$s (%3$s)" #. TRANSLATORS: This results in something like "Swap at /dev/sda3 (2 GiB)" since #. %1$s is replaced by the device name, and %2$s by the size -#: src/components/storage/PartitionsField.jsx:121 +#: src/components/storage/PartitionsField.jsx:132 #, c-format msgid "Swap at %1$s (%2$s)" msgstr "Intercanvi a %1$s (%2$s)" #. TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:125 +#: src/components/storage/PartitionsField.jsx:136 #, c-format msgid "Swap volume (%s)" msgstr "Volum d'intercanvi (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:127 +#: src/components/storage/PartitionsField.jsx:138 #, c-format msgid "Swap partition (%s)" msgstr "Partició d'intercanvi (%s)" #. TRANSLATORS: This results in something like "Btrfs root at /dev/sda3 (20 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size -#: src/components/storage/PartitionsField.jsx:136 +#: src/components/storage/PartitionsField.jsx:147 #, c-format msgid "%1$s root at %2$s (%3$s)" msgstr "Arrel %1$s a %2$s (%3$s)" @@ -1479,21 +1439,21 @@ msgstr "Arrel %1$s a %2$s (%3$s)" #. TRANSLATORS: "/" is in an LVM logical volume. #. Results in something like "Btrfs root volume (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:142 +#: src/components/storage/PartitionsField.jsx:153 #, c-format msgid "%1$s root volume (%2$s)" msgstr "Volum d'arrel de %1$s (%2$s)" #. TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:145 +#: src/components/storage/PartitionsField.jsx:156 #, c-format msgid "%1$s root partition (%2$s)" msgstr "Partició d'arrel %1$s (%2$s)" #. TRANSLATORS: This results in something like "Ext4 /home at /dev/sda3 (20 GiB)" since #. %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size -#: src/components/storage/PartitionsField.jsx:151 +#: src/components/storage/PartitionsField.jsx:162 #, c-format msgid "%1$s %2$s at %3$s (%4$s)" msgstr "%1$s %2$s a %3$s (%4$s)" @@ -1501,143 +1461,139 @@ msgstr "%1$s %2$s a %3$s (%4$s)" #. TRANSLATORS: The filesystem is in an LVM logical volume. #. Results in something like "Ext4 /home volume (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:157 +#: src/components/storage/PartitionsField.jsx:168 #, c-format msgid "%1$s %2$s volume (%3$s)" msgstr "Volum %1$s per a %2$s (%3$s)" #. TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:160 +#: src/components/storage/PartitionsField.jsx:171 #, c-format msgid "%1$s %2$s partition (%3$s)" msgstr "Partició %1$s per a %2$s (%3$s)" -#: src/components/storage/PartitionsField.jsx:172 +#: src/components/storage/PartitionsField.jsx:182 msgid "Do not configure partitions for booting" msgstr "No configuris particions per a l'arrencada." -#: src/components/storage/PartitionsField.jsx:175 +#: src/components/storage/PartitionsField.jsx:184 msgid "Boot partitions at installation disk" msgstr "Particions per a l'arrencada al disc d'instal·lació" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/PartitionsField.jsx:178 +#: src/components/storage/PartitionsField.jsx:187 #, c-format msgid "Boot partitions at %s" msgstr "Particions per l'arrencada a %s" #. TRANSLATORS: header for a list of items referring to size limits for file systems -#: src/components/storage/PartitionsField.jsx:200 +#: src/components/storage/PartitionsField.jsx:209 msgid "These limits are affected by:" msgstr "Aquests límits estan afectats pel següent:" #. TRANSLATORS: list item, this affects the computed partition size limits -#: src/components/storage/PartitionsField.jsx:204 +#: src/components/storage/PartitionsField.jsx:213 msgid "The configuration of snapshots" msgstr "La configuració de les instantànies" -#. TRANSLATORS: list item, this affects the computed partition size limits -#. %s is replaced by a list of the volumes (like "/home, /boot") -#: src/components/storage/PartitionsField.jsx:208 +#: src/components/storage/PartitionsField.jsx:219 #, c-format msgid "Presence of other volumes (%s)" msgstr "La presència d'altres volums (%s)" #. TRANSLATORS: list item, describes a factor that affects the computed size of a #. file system; eg. adjusting the size of the swap -#: src/components/storage/PartitionsField.jsx:212 +#: src/components/storage/PartitionsField.jsx:225 msgid "The amount of RAM in the system" msgstr "La quantitat de RAM del sistema" -#. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/PartitionsField.jsx:263 +#: src/components/storage/PartitionsField.jsx:292 msgid "auto" msgstr "automàtica" #. TRANSLATORS: %s will be replaced by a file-system type like "Btrfs" or "Ext4" -#: src/components/storage/PartitionsField.jsx:279 +#: src/components/storage/PartitionsField.jsx:309 #, c-format msgid "Reused %s" msgstr "%s reutilitzat" -#: src/components/storage/PartitionsField.jsx:281 +#: src/components/storage/PartitionsField.jsx:310 msgid "Transactional Btrfs" msgstr "Btrfs transaccional" -#: src/components/storage/PartitionsField.jsx:283 +#: src/components/storage/PartitionsField.jsx:311 msgid "Btrfs with snapshots" msgstr "Btrfs amb instantànies" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:297 +#: src/components/storage/PartitionsField.jsx:325 #, c-format msgid "Partition at %s" msgstr "Partició a %s" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:300 +#: src/components/storage/PartitionsField.jsx:328 #, c-format msgid "Separate LVM at %s" msgstr "LVM separat a %s" -#: src/components/storage/PartitionsField.jsx:304 +#: src/components/storage/PartitionsField.jsx:331 msgid "Logical volume at system LVM" msgstr "Volum lògic al sistema LVM" -#: src/components/storage/PartitionsField.jsx:306 +#: src/components/storage/PartitionsField.jsx:333 msgid "Partition at installation disk" msgstr "Partició al disc d'instal·lació" -#: src/components/storage/PartitionsField.jsx:321 +#: src/components/storage/PartitionsField.jsx:348 msgid "Reset location" msgstr "Restableix la ubicació" -#: src/components/storage/PartitionsField.jsx:322 +#: src/components/storage/PartitionsField.jsx:349 msgid "Change location" msgstr "Canvia la ubicació" -#: src/components/storage/PartitionsField.jsx:323 -#: src/components/storage/SpaceActionsTable.jsx:78 +#: src/components/storage/PartitionsField.jsx:350 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Suprimeix" -#: src/components/storage/PartitionsField.jsx:449 -#: src/components/storage/VolumeFields.jsx:61 -#: src/components/storage/VolumeFields.jsx:70 +#: src/components/storage/PartitionsField.jsx:486 +#: src/components/storage/VolumeFields.jsx:66 #: src/components/storage/VolumeFields.jsx:75 +#: src/components/storage/VolumeFields.jsx:80 msgid "Mount point" msgstr "Punt de muntatge" #. TRANSLATORS: where (and how) the file-system is going to be created -#: src/components/storage/PartitionsField.jsx:453 +#: src/components/storage/PartitionsField.jsx:490 msgid "Location" msgstr "Ubicació" -#: src/components/storage/PartitionsField.jsx:495 +#: src/components/storage/PartitionsField.jsx:532 msgid "Table with mount points" msgstr "Taula amb punts de muntatge" -#: src/components/storage/PartitionsField.jsx:566 -#: src/components/storage/PartitionsField.jsx:585 -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/PartitionsField.jsx:604 +#: src/components/storage/PartitionsField.jsx:624 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Add file system" msgstr "Afegeix-hi un sistema de fitxers" -#: src/components/storage/PartitionsField.jsx:596 +#: src/components/storage/PartitionsField.jsx:636 msgid "Other" msgstr "Una altra" -#: src/components/storage/PartitionsField.jsx:731 +#: src/components/storage/PartitionsField.jsx:777 msgid "Reset to defaults" msgstr "Restableix els valors predeterminats" -#: src/components/storage/PartitionsField.jsx:801 +#: src/components/storage/PartitionsField.jsx:849 msgid "Partitions and file systems" msgstr "Particions i sistemes de fitxers" -#: src/components/storage/PartitionsField.jsx:802 +#: src/components/storage/PartitionsField.jsx:851 msgid "" "Structure of the new system, including any additional partition needed for " "booting" @@ -1645,20 +1601,18 @@ msgstr "" "Estructura del sistema nou, inclosa qualsevol partició addicional necessària " "per a arrencar" -#: src/components/storage/PartitionsField.jsx:808 +#: src/components/storage/PartitionsField.jsx:858 msgid "Show partitions and file-systems actions" msgstr "Mostra les particions i les accions dels sistemes de fitxers" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:62 +#: src/components/storage/ProposalActionsDialog.jsx:65 #, c-format msgid "Hide %d subvolume action" msgid_plural "Hide %d subvolume actions" msgstr[0] "Amaga %d acció de subvolum" msgstr[1] "Amaga %d accions de subvolum" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:64 +#: src/components/storage/ProposalActionsDialog.jsx:70 #, c-format msgid "Show %d subvolume action" msgid_plural "Show %d subvolume actions" @@ -1673,86 +1627,79 @@ msgstr "No es permeten accions destructives." msgid "Destructive actions are allowed" msgstr "Es permeten accions destructives." -#: src/components/storage/ProposalActionsSummary.jsx:66 -#, c-format -msgid "There is %d destructive action planned" -msgid_plural "There are %d destructive actions planned" -msgstr[0] "Hi ha %d acció destructiva planificada." -msgstr[1] "Hi ha %d accions destructives planificades." - -#: src/components/storage/ProposalActionsSummary.jsx:79 -#: src/components/storage/ProposalActionsSummary.jsx:126 +#: src/components/storage/ProposalActionsSummary.jsx:82 +#: src/components/storage/ProposalActionsSummary.jsx:132 msgid "affecting" msgstr "Això afecta" -#: src/components/storage/ProposalActionsSummary.jsx:107 +#: src/components/storage/ProposalActionsSummary.jsx:112 msgid "Shrinking partitions is not allowed" msgstr "No es permet encongir particions." -#: src/components/storage/ProposalActionsSummary.jsx:111 +#: src/components/storage/ProposalActionsSummary.jsx:116 msgid "Shrinking partitions is allowed" msgstr "Es permet encongir particions." -#: src/components/storage/ProposalActionsSummary.jsx:113 +#: src/components/storage/ProposalActionsSummary.jsx:118 msgid "Shrinking some partitions is allowed but not needed" msgstr "Es permet encongir algunes particions, però no cal." -#: src/components/storage/ProposalActionsSummary.jsx:116 +#: src/components/storage/ProposalActionsSummary.jsx:121 #, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" msgstr[0] "S'encongirà %d partició." msgstr[1] "S'encongiran %d particions." -#: src/components/storage/ProposalActionsSummary.jsx:151 +#: src/components/storage/ProposalActionsSummary.jsx:159 msgid "Cannot accommodate the required file systems for installation" msgstr "" "No es poden acomodar els sistemes de fitxers necessaris per a la " "instal·lació." -#: src/components/storage/ProposalActionsSummary.jsx:160 +#: src/components/storage/ProposalActionsSummary.jsx:167 #, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" msgstr[0] "Marca l'acció planificada" msgstr[1] "Marca les %d accions planificades" -#: src/components/storage/ProposalActionsSummary.jsx:179 +#: src/components/storage/ProposalActionsSummary.jsx:182 msgid "Waiting for actions information..." msgstr "Esperant la informació de les accions..." -#: src/components/storage/ProposalPage.jsx:314 +#: src/components/storage/ProposalPage.jsx:329 msgid "Planned Actions" msgstr "Accions planificades" -#: src/components/storage/ProposalResultSection.jsx:42 +#: src/components/storage/ProposalResultSection.jsx:43 msgid "Waiting for information about storage configuration" msgstr "Esperant informació sobre la configuració de l'emmagatzematge" -#: src/components/storage/ProposalResultSection.jsx:70 +#: src/components/storage/ProposalResultSection.jsx:73 msgid "Final layout" msgstr "Disposició final" -#: src/components/storage/ProposalResultSection.jsx:71 +#: src/components/storage/ProposalResultSection.jsx:74 msgid "The systems will be configured as displayed below." msgstr "Els sistemes es configuraran tal com es mostra a continuació." -#: src/components/storage/ProposalResultSection.jsx:78 +#: src/components/storage/ProposalResultSection.jsx:83 msgid "Storage proposal not possible" msgstr "La proposta d'emmagatzematge no és possible." -#: src/components/storage/ProposalResultTable.jsx:74 +#: src/components/storage/ProposalResultTable.jsx:79 msgid "New" msgstr "Nova" #. TRANSLATORS: Label to indicate the device size before resizing, where %s is #. replaced by the original size (e.g., 3.00 GiB). -#: src/components/storage/ProposalResultTable.jsx:98 +#: src/components/storage/ProposalResultTable.jsx:105 #, c-format msgid "Before %s" msgstr "Abans: %s" -#: src/components/storage/ProposalResultTable.jsx:121 +#: src/components/storage/ProposalResultTable.jsx:131 msgid "Mount Point" msgstr "Punt de muntatge" @@ -1760,7 +1707,7 @@ msgstr "Punt de muntatge" msgid "Transactional root file system" msgstr "Sistema de fitxers d'arrel transaccional" -#: src/components/storage/ProposalTransactionalInfo.jsx:48 +#: src/components/storage/ProposalTransactionalInfo.jsx:49 #, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " @@ -1773,7 +1720,7 @@ msgstr "" msgid "Use Btrfs snapshots for the root file system" msgstr "Usa instantànies de Btrfs per al sistema de fitxers d'arrel." -#: src/components/storage/SnapshotsField.jsx:37 +#: src/components/storage/SnapshotsField.jsx:38 msgid "" "Allows to boot to a previous version of the system after configuration " "changes or software upgrades." @@ -1781,117 +1728,110 @@ msgstr "" "Permet arrencar amb una versió anterior del sistema després de canvis de " "configuració o actualitzacions de programari." -#. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) -#: src/components/storage/SpaceActionsTable.jsx:75 +#: src/components/storage/SpaceActionsTable.jsx:68 #, c-format -msgid "Space action selector for %s" -msgstr "Selector d'acció espacial per a %s" +msgid "Up to %s can be recovered by shrinking the device." +msgstr "Es poden recuperar fins a %s encongint el dispositiu." -#: src/components/storage/SpaceActionsTable.jsx:79 -msgid "Allow resize" -msgstr "Permet-ne el canvi de mida" +#: src/components/storage/SpaceActionsTable.jsx:77 +msgid "The device cannot be shrunk:" +msgstr "El dispositiu no es pot encongir:" -#: src/components/storage/SpaceActionsTable.jsx:80 -msgid "Do not modify" -msgstr "No la modifiquis" +#: src/components/storage/SpaceActionsTable.jsx:98 +#, c-format +msgid "Show information about %s" +msgstr "Mostra informació quant a %s" -#: src/components/storage/SpaceActionsTable.jsx:111 +#: src/components/storage/SpaceActionsTable.jsx:172 msgid "The content may be deleted" msgstr "El contingut pot suprimir-se" -#: src/components/storage/SpaceActionsTable.jsx:144 -msgid "Shrinkable" -msgstr "Encongible" - -#: src/components/storage/SpaceActionsTable.jsx:146 +#: src/components/storage/SpaceActionsTable.jsx:204 msgid "Action" msgstr "Acció" -#: src/components/storage/SpaceActionsTable.jsx:162 +#: src/components/storage/SpaceActionsTable.jsx:215 msgid "Actions to find space" msgstr "Accions per aconseguir espai" -#: src/components/storage/SpacePolicySelection.jsx:170 +#: src/components/storage/SpacePolicySelection.jsx:172 msgid "Space policy" msgstr "Política espacial" -#: src/components/storage/VolumeDialog.jsx:78 +#: src/components/storage/VolumeDialog.jsx:83 #, c-format msgid "Add %s file system" msgstr "Afegeix-hi un sistema de fitxers %s" -#: src/components/storage/VolumeDialog.jsx:79 +#: src/components/storage/VolumeDialog.jsx:84 #, c-format msgid "Edit %s file system" msgstr "Edita el sistema de fitxers %s" -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Edit file system" msgstr "Edita el sistema de fitxers" #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:96 +#: src/components/storage/VolumeDialog.jsx:101 msgid "The type and size of the file system cannot be edited." msgstr "El tipus i la mida del sistema de fitxers no es poden editar." -#. TRANSLATORS: Description of a warning. The first %s is replaced by a device name (e.g., -#. /dev/vda) and the second %s is replaced by a mount path (e.g., /home). -#: src/components/storage/VolumeDialog.jsx:99 +#: src/components/storage/VolumeDialog.jsx:105 #, c-format msgid "The current file system on %s is selected to be mounted at %s." msgstr "El sistema de fitxers actual a %s està seleccionat per muntar-lo a %s." #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:105 +#: src/components/storage/VolumeDialog.jsx:113 msgid "The size of the file system cannot be edited" msgstr "La mida del sistema de fitxers no es pot editar." #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). -#: src/components/storage/VolumeDialog.jsx:107 +#: src/components/storage/VolumeDialog.jsx:115 #, c-format msgid "The file system is allocated at the device %s." msgstr "El sistema de fitxers s'assigna al dispositiu %s." -#: src/components/storage/VolumeDialog.jsx:152 +#: src/components/storage/VolumeDialog.jsx:163 msgid "A mount point is required" msgstr "Cal un punt de muntatge." -#: src/components/storage/VolumeDialog.jsx:179 +#: src/components/storage/VolumeDialog.jsx:190 msgid "The mount point is invalid" msgstr "El punt de muntatge no és vàlid." -#: src/components/storage/VolumeDialog.jsx:207 +#: src/components/storage/VolumeDialog.jsx:218 msgid "A size value is required" msgstr "Cal un valor de mida" -#: src/components/storage/VolumeDialog.jsx:235 +#: src/components/storage/VolumeDialog.jsx:246 msgid "Minimum size is required" msgstr "Cal una mida mínima" -#: src/components/storage/VolumeDialog.jsx:267 +#: src/components/storage/VolumeDialog.jsx:278 msgid "Maximum must be greater than minimum" msgstr "El màxim ha de ser superior al mínim." -#: src/components/storage/VolumeDialog.jsx:309 +#: src/components/storage/VolumeDialog.jsx:320 #, c-format msgid "There is already a file system for %s." msgstr "Ja hi ha un sistema de fitxers per a %s." -#: src/components/storage/VolumeDialog.jsx:311 +#: src/components/storage/VolumeDialog.jsx:322 msgid "Do you want to edit it?" msgstr "El voleu editar?" -#: src/components/storage/VolumeDialog.jsx:356 +#: src/components/storage/VolumeDialog.jsx:367 #, c-format msgid "There is a predefined file system for %s." msgstr "Hi ha un sistema de fitxers predefinit per a %s." -#: src/components/storage/VolumeDialog.jsx:358 +#: src/components/storage/VolumeDialog.jsx:369 msgid "Do you want to add it?" msgstr "L'hi voleu afegir?" -#. TRANSLATORS: info about possible file system types. -#: src/components/storage/VolumeFields.jsx:217 +#: src/components/storage/VolumeFields.jsx:225 msgid "" "The options for the file system type depends on the product and the mount " "point." @@ -1899,68 +1839,64 @@ msgstr "" "Les opcions per al tipus de sistema de fitxers depenen del producte i del " "punt de muntatge." -#: src/components/storage/VolumeFields.jsx:223 +#: src/components/storage/VolumeFields.jsx:232 msgid "More info for file system types" msgstr "Més informació sobre els tipus de sistemes de fitxers" #. TRANSLATORS: label for the file system selector. -#: src/components/storage/VolumeFields.jsx:234 +#: src/components/storage/VolumeFields.jsx:243 msgid "File system type" msgstr "Tipus de sistema de fitxers" #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:265 +#: src/components/storage/VolumeFields.jsx:274 msgid "the configuration of snapshots" msgstr "la configuració de les instantànies" -#. TRANSLATORS: item which affects the final computed partition size -#. %s is replaced by a list of mount points like "/home, /boot" -#: src/components/storage/VolumeFields.jsx:270 +#: src/components/storage/VolumeFields.jsx:281 #, c-format msgid "the presence of the file system for %s" msgstr "la presència del sistema de fitxers per a %s" #. TRANSLATORS: conjunction for merging two list items -#: src/components/storage/VolumeFields.jsx:272 +#: src/components/storage/VolumeFields.jsx:283 msgid ", " msgstr ", " #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:276 +#: src/components/storage/VolumeFields.jsx:289 msgid "the amount of RAM in the system" msgstr "la quantitat de RAM del sistema" -#. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeFields.jsx:279 +#: src/components/storage/VolumeFields.jsx:293 #, c-format msgid "The final size depends on %s." msgstr "La mida final depèn de %s." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeFields.jsx:281 +#: src/components/storage/VolumeFields.jsx:295 msgid " and " msgstr " i " -#. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeFields.jsx:286 +#: src/components/storage/VolumeFields.jsx:302 msgid "Automatically calculated size according to the selected product." msgstr "Mida calculada automàticament segons el producte seleccionat." -#: src/components/storage/VolumeFields.jsx:305 +#: src/components/storage/VolumeFields.jsx:321 msgid "Exact size for the file system." msgstr "Mida exacta per al sistema de fitxers." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeFields.jsx:318 +#: src/components/storage/VolumeFields.jsx:330 msgid "Exact size" msgstr "Mida exacta" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeFields.jsx:335 +#: src/components/storage/VolumeFields.jsx:347 msgid "Size unit" msgstr "Unitat de mida" -#: src/components/storage/VolumeFields.jsx:363 +#: src/components/storage/VolumeFields.jsx:376 msgid "" "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 " @@ -1971,50 +1907,49 @@ msgstr "" "de fitxers serà el més gros possible." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeFields.jsx:370 +#: src/components/storage/VolumeFields.jsx:384 msgid "Minimum" msgstr "Mínim" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeFields.jsx:381 +#: src/components/storage/VolumeFields.jsx:395 msgid "Minimum desired size" msgstr "Mida mínima desitjada" -#: src/components/storage/VolumeFields.jsx:392 +#: src/components/storage/VolumeFields.jsx:406 msgid "Unit for the minimum size" msgstr "Unitat per a la mida mínima" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:404 +#: src/components/storage/VolumeFields.jsx:418 msgid "Maximum" msgstr "Màxim" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:416 +#: src/components/storage/VolumeFields.jsx:430 msgid "Maximum desired size" msgstr "Mida màxima desitjada" -#: src/components/storage/VolumeFields.jsx:426 +#: src/components/storage/VolumeFields.jsx:440 msgid "Unit for the maximum size" msgstr "Unitat per a la mida màxima" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeFields.jsx:444 +#: src/components/storage/VolumeFields.jsx:458 msgid "Auto" msgstr "Automàtica" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeFields.jsx:446 +#: src/components/storage/VolumeFields.jsx:460 msgid "Fixed" msgstr "Fixa" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeFields.jsx:448 +#: src/components/storage/VolumeFields.jsx:462 msgid "Range" msgstr "Interval" -#. TRANSLATORS: Description of the dialog for changing the location of a file system. -#: src/components/storage/VolumeLocationDialog.jsx:40 +#: src/components/storage/VolumeLocationDialog.jsx:41 msgid "" "The file systems are allocated at the installation device by default. " "Indicate a custom location to create the file system at a specific device." @@ -2025,38 +1960,38 @@ msgstr "" #. TRANSLATORS: Title of the dialog for changing the location of a file system. %s is replaced #. by a mount path (e.g., /home). -#: src/components/storage/VolumeLocationDialog.jsx:135 +#: src/components/storage/VolumeLocationDialog.jsx:137 #, c-format msgid "Location for %s file system" msgstr "Ubicació per al sistema de fitxers %s" -#: src/components/storage/VolumeLocationDialog.jsx:145 +#: src/components/storage/VolumeLocationDialog.jsx:147 msgid "Select in which device to allocate the file system" msgstr "Seleccioneu en quin dispositiu assignar el sistema de fitxers." -#: src/components/storage/VolumeLocationDialog.jsx:148 +#: src/components/storage/VolumeLocationDialog.jsx:150 msgid "Select a location" msgstr "Seleccioneu una ubicació." -#: src/components/storage/VolumeLocationDialog.jsx:160 +#: src/components/storage/VolumeLocationDialog.jsx:162 msgid "Select how to allocate the file system" msgstr "Seleccioneu com assignar el sistema de fitxers." -#: src/components/storage/VolumeLocationDialog.jsx:165 +#: src/components/storage/VolumeLocationDialog.jsx:167 msgid "Create a new partition" msgstr "Crea una partició nova" -#: src/components/storage/VolumeLocationDialog.jsx:166 +#: src/components/storage/VolumeLocationDialog.jsx:169 msgid "" "The file system will be allocated as a new partition at the selected disk." msgstr "" "El sistema de fitxers s'assignarà com a partició nova al disc seleccionat." -#: src/components/storage/VolumeLocationDialog.jsx:175 +#: src/components/storage/VolumeLocationDialog.jsx:179 msgid "Create a dedicated LVM volume group" msgstr "Crea un grup de volums LVM dedicat" -#: src/components/storage/VolumeLocationDialog.jsx:176 +#: src/components/storage/VolumeLocationDialog.jsx:181 msgid "" "A new volume group will be allocated in the selected disk and the file " "system will be created as a logical volume." @@ -2064,21 +1999,20 @@ msgstr "" "S'assignarà un grup de volums nou al disc seleccionat i el sistema de " "fitxers es crearà com a volum lògic." -#: src/components/storage/VolumeLocationDialog.jsx:185 +#: src/components/storage/VolumeLocationDialog.jsx:191 msgid "Format the device" msgstr "Formata el dispositiu" -#. TRANSLATORS: %s is replaced by a file system type (e.g., Ext4). -#: src/components/storage/VolumeLocationDialog.jsx:188 +#: src/components/storage/VolumeLocationDialog.jsx:195 #, c-format msgid "The selected device will be formatted as %s file system." msgstr "El dispositiu seleccionat es formatarà com a sistema de fitxers %s." -#: src/components/storage/VolumeLocationDialog.jsx:198 +#: src/components/storage/VolumeLocationDialog.jsx:206 msgid "Mount the file system" msgstr "Munta el sistema de fitxers" -#: src/components/storage/VolumeLocationDialog.jsx:199 +#: src/components/storage/VolumeLocationDialog.jsx:208 msgid "" "The current file system on the selected device will be mounted without " "formatting the device." @@ -2086,53 +2020,51 @@ msgstr "" "El sistema de fitxers actual del dispositiu seleccionat es muntarà sense " "formatar el dispositiu." -#: src/components/storage/VolumeLocationSelectorTable.jsx:102 +#: src/components/storage/VolumeLocationSelectorTable.jsx:110 msgid "Usage" msgstr "Ús" -#: src/components/storage/ZFCPDiskForm.jsx:109 +#: src/components/storage/ZFCPDiskForm.jsx:106 msgid "The zFCP disk was not activated." msgstr "El disc zFCP no s'ha activat." #. TRANSLATORS: abbrev. World Wide Port Name #: src/components/storage/ZFCPDiskForm.jsx:123 -#: src/components/storage/ZFCPPage.jsx:363 +#: src/components/storage/ZFCPPage.jsx:383 msgid "WWPN" msgstr "WWPN" #. TRANSLATORS: abbrev. Logical Unit Number -#: src/components/storage/ZFCPDiskForm.jsx:134 -#: src/components/storage/ZFCPPage.jsx:364 +#: src/components/storage/ZFCPDiskForm.jsx:131 +#: src/components/storage/ZFCPPage.jsx:384 msgid "LUN" msgstr "LUN" -#: src/components/storage/ZFCPPage.jsx:304 +#: src/components/storage/ZFCPPage.jsx:326 msgid "Auto LUNs Scan" msgstr "Escaneig automàtic de LUN" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Activated" msgstr "Activat" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Deactivated" msgstr "Desactivat" -#: src/components/storage/ZFCPPage.jsx:418 +#: src/components/storage/ZFCPPage.jsx:437 msgid "No zFCP controllers found." msgstr "No s'ha trobat cap controlador de zFCP." -#: src/components/storage/ZFCPPage.jsx:419 +#: src/components/storage/ZFCPPage.jsx:438 msgid "Please, try to read the zFCP devices again." msgstr "Si us plau, intenteu tornar a llegir els dispositius zFCP." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:421 +#: src/components/storage/ZFCPPage.jsx:441 msgid "Read zFCP devices" msgstr "Llegeix els dispositius zFCP" -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:430 +#: src/components/storage/ZFCPPage.jsx:452 msgid "" "Automatic LUN scan is [enabled]. Activating a controller which is running in " "NPIV mode will automatically configures all its LUNs." @@ -2140,8 +2072,7 @@ msgstr "" "L'exploració automàtica de LUN està [activada]. L'activació d'un controlador " "que s'executa en mode NPIV configurarà automàticament tots els seus LUN." -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:433 +#: src/components/storage/ZFCPPage.jsx:457 msgid "" "Automatic LUN scan is [disabled]. LUNs have to be manually configured after " "activating a controller." @@ -2149,38 +2080,36 @@ msgstr "" "L'exploració automàtica de LUN està [desactivada]. Els LUN s'han de " "configurar manualment després d'activar un controlador." -#: src/components/storage/ZFCPPage.jsx:490 +#: src/components/storage/ZFCPPage.jsx:519 msgid "Activate a zFCP disk" msgstr "Activa un disc zFCP" -#: src/components/storage/ZFCPPage.jsx:529 +#: src/components/storage/ZFCPPage.jsx:553 msgid "Please, try to activate a zFCP controller." msgstr "Si us plau, proveu d'activar un controlador de zFCP." -#: src/components/storage/ZFCPPage.jsx:536 +#: src/components/storage/ZFCPPage.jsx:559 msgid "Please, try to activate a zFCP disk." msgstr "Si us plau, proveu d'activar un disc zFCP." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:538 +#: src/components/storage/ZFCPPage.jsx:562 msgid "Activate zFCP disk" msgstr "Activa el disc zFCP" -#: src/components/storage/ZFCPPage.jsx:545 +#: src/components/storage/ZFCPPage.jsx:570 msgid "No zFCP disks found." msgstr "No s'ha trobat cap disc zFCP." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:560 +#: src/components/storage/ZFCPPage.jsx:586 msgid "Activate new disk" msgstr "Activa el disc nou" #. TRANSLATORS: section title -#: src/components/storage/ZFCPPage.jsx:572 +#: src/components/storage/ZFCPPage.jsx:599 msgid "Disks" msgstr "Discs" -#: src/components/storage/device-utils.jsx:88 +#: src/components/storage/device-utils.jsx:92 msgid "Unused space" msgstr "Espai sense ús" @@ -2192,70 +2121,70 @@ msgstr "Només està disponible si es proporciona l'autenticació per destinaci msgid "Authentication by target" msgstr "Autenticació per destinació" -#: src/components/storage/iscsi/AuthFields.jsx:80 -#: src/components/storage/iscsi/AuthFields.jsx:85 -#: src/components/storage/iscsi/AuthFields.jsx:87 -#: src/components/storage/iscsi/AuthFields.jsx:112 -#: src/components/storage/iscsi/AuthFields.jsx:117 -#: src/components/storage/iscsi/AuthFields.jsx:119 +#: src/components/storage/iscsi/AuthFields.jsx:78 +#: src/components/storage/iscsi/AuthFields.jsx:82 +#: src/components/storage/iscsi/AuthFields.jsx:84 +#: src/components/storage/iscsi/AuthFields.jsx:104 +#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:110 msgid "User name" msgstr "Nom d'usuari" -#: src/components/storage/iscsi/AuthFields.jsx:91 -#: src/components/storage/iscsi/AuthFields.jsx:124 +#: src/components/storage/iscsi/AuthFields.jsx:88 +#: src/components/storage/iscsi/AuthFields.jsx:116 msgid "Incorrect user name" msgstr "Nom d'usuari incorrecte" -#: src/components/storage/iscsi/AuthFields.jsx:105 -#: src/components/storage/iscsi/AuthFields.jsx:139 +#: src/components/storage/iscsi/AuthFields.jsx:99 +#: src/components/storage/iscsi/AuthFields.jsx:130 msgid "Incorrect password" msgstr "Contrasenya incorrecta" -#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:102 msgid "Authentication by initiator" msgstr "Autenticació per iniciador" -#: src/components/storage/iscsi/AuthFields.jsx:133 +#: src/components/storage/iscsi/AuthFields.jsx:123 msgid "Target Password" msgstr "Contrasenya de destinació" #. TRANSLATORS: popup title -#: src/components/storage/iscsi/DiscoverForm.jsx:102 +#: src/components/storage/iscsi/DiscoverForm.jsx:94 msgid "Discover iSCSI Targets" msgstr "Descobreix les destinacions iSCSI" -#: src/components/storage/iscsi/DiscoverForm.jsx:112 -#: src/components/storage/iscsi/LoginForm.jsx:73 +#: src/components/storage/iscsi/DiscoverForm.jsx:99 +#: src/components/storage/iscsi/LoginForm.jsx:70 msgid "Make sure you provide the correct values" msgstr "Assegureu-vos que proporcioneu els valors correctes" -#: src/components/storage/iscsi/DiscoverForm.jsx:118 +#: src/components/storage/iscsi/DiscoverForm.jsx:103 msgid "IP address" msgstr "Adreça IP" #. TRANSLATORS: network address -#: src/components/storage/iscsi/DiscoverForm.jsx:125 -#: src/components/storage/iscsi/DiscoverForm.jsx:127 +#: src/components/storage/iscsi/DiscoverForm.jsx:108 +#: src/components/storage/iscsi/DiscoverForm.jsx:110 msgid "Address" msgstr "Adreça" -#: src/components/storage/iscsi/DiscoverForm.jsx:132 +#: src/components/storage/iscsi/DiscoverForm.jsx:115 msgid "Incorrect IP address" msgstr "Adreça IP incorrecta" #. TRANSLATORS: network port number -#: src/components/storage/iscsi/DiscoverForm.jsx:136 -#: src/components/storage/iscsi/DiscoverForm.jsx:143 -#: src/components/storage/iscsi/DiscoverForm.jsx:145 +#: src/components/storage/iscsi/DiscoverForm.jsx:117 +#: src/components/storage/iscsi/DiscoverForm.jsx:122 +#: src/components/storage/iscsi/DiscoverForm.jsx:124 msgid "Port" msgstr "Port" -#: src/components/storage/iscsi/DiscoverForm.jsx:150 +#: src/components/storage/iscsi/DiscoverForm.jsx:129 msgid "Incorrect port" msgstr "Port incorrecte" #. TRANSLATORS: %s is replaced by the iSCSI target node name -#: src/components/storage/iscsi/EditNodeForm.jsx:50 +#: src/components/storage/iscsi/EditNodeForm.jsx:48 #, c-format msgid "Edit %s" msgstr "Edita %s" @@ -2273,8 +2202,8 @@ msgstr "Nom de l'iniciador" #. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 #: src/components/storage/iscsi/InitiatorPresenter.jsx:86 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 -#: src/components/storage/iscsi/NodesPresenter.jsx:124 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 +#: src/components/storage/iscsi/NodesPresenter.jsx:122 msgid "iBFT" msgstr "iBFT" @@ -2289,14 +2218,14 @@ msgid "Initiator" msgstr "Iniciador" #. TRANSLATORS: %s is replaced by the iSCSI target name -#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/storage/iscsi/LoginForm.jsx:66 #, c-format msgid "Login %s" msgstr "Entrada per a %s" #. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) -#: src/components/storage/iscsi/LoginForm.jsx:76 -#: src/components/storage/iscsi/LoginForm.jsx:79 +#: src/components/storage/iscsi/LoginForm.jsx:74 +#: src/components/storage/iscsi/LoginForm.jsx:77 msgid "Startup" msgstr "Inici" @@ -2318,29 +2247,28 @@ msgstr "Entrada" msgid "Logout" msgstr "Sortida" -#: src/components/storage/iscsi/NodesPresenter.jsx:101 -#: src/components/storage/iscsi/NodesPresenter.jsx:122 +#: src/components/storage/iscsi/NodesPresenter.jsx:99 +#: src/components/storage/iscsi/NodesPresenter.jsx:120 msgid "Portal" msgstr "Portal" -#: src/components/storage/iscsi/NodesPresenter.jsx:102 -#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/storage/iscsi/NodesPresenter.jsx:100 +#: src/components/storage/iscsi/NodesPresenter.jsx:121 msgid "Interface" msgstr "Interfície" -#: src/components/storage/iscsi/TargetsSection.jsx:142 +#: src/components/storage/iscsi/TargetsSection.jsx:138 msgid "No iSCSI targets found." msgstr "No s'ha trobat cap destinació iSCSI." -#: src/components/storage/iscsi/TargetsSection.jsx:143 +#: src/components/storage/iscsi/TargetsSection.jsx:140 msgid "" "Please, perform an iSCSI discovery in order to find available iSCSI targets." msgstr "" "Si us plau, executeu un descobriment d'iSCSI per trobar destinacions iSCSI " "disponibles." -#. TRANSLATORS: button label, starts iSCSI discovery -#: src/components/storage/iscsi/TargetsSection.jsx:145 +#: src/components/storage/iscsi/TargetsSection.jsx:144 msgid "Discover iSCSI targets" msgstr "Descobreix destinacions iSCSI" @@ -2350,7 +2278,7 @@ msgid "Discover" msgstr "Descobreix" #. TRANSLATORS: iSCSI targets section title -#: src/components/storage/iscsi/TargetsSection.jsx:170 +#: src/components/storage/iscsi/TargetsSection.jsx:167 msgid "Targets" msgstr "Destinacions" @@ -2442,7 +2370,7 @@ msgstr "amb accions personalitzades." msgid "No user defined yet." msgstr "Encara no s'ha definit cap usuari." -#: src/components/users/FirstUser.jsx:38 +#: src/components/users/FirstUser.jsx:39 msgid "" "Please, be aware that a user must be defined before installing the system to " "be able to log into it." @@ -2450,68 +2378,68 @@ msgstr "" "Si us plau, tingueu en compte que cal definir un usuari abans d'instal·lar " "el sistema per poder-hi iniciar sessió." -#: src/components/users/FirstUser.jsx:42 +#: src/components/users/FirstUser.jsx:45 msgid "Define a user now" msgstr "Definiu un usuari ara" -#: src/components/users/FirstUser.jsx:54 -#: src/components/users/FirstUserForm.jsx:210 +#: src/components/users/FirstUser.jsx:58 +#: src/components/users/FirstUserForm.jsx:227 msgid "Full name" msgstr "Nom complet" -#: src/components/users/FirstUser.jsx:55 -#: src/components/users/FirstUserForm.jsx:224 -#: src/components/users/FirstUserForm.jsx:229 -#: src/components/users/FirstUserForm.jsx:232 +#: src/components/users/FirstUser.jsx:59 +#: src/components/users/FirstUserForm.jsx:241 +#: src/components/users/FirstUserForm.jsx:246 +#: src/components/users/FirstUserForm.jsx:249 msgid "Username" msgstr "Nom d'usuari" -#: src/components/users/FirstUser.jsx:120 -#: src/components/users/RootAuthMethods.jsx:99 -#: src/components/users/RootAuthMethods.jsx:111 +#: src/components/users/FirstUser.jsx:124 +#: src/components/users/RootAuthMethods.jsx:104 +#: src/components/users/RootAuthMethods.jsx:116 msgid "Discard" msgstr "Descarta'l" -#: src/components/users/FirstUserForm.jsx:46 +#: src/components/users/FirstUserForm.jsx:57 msgid "Username suggestion dropdown" msgstr "Menú desplegable de suggeriments de nom d'usuari" #. TRANSLATORS: dropdown username suggestions -#: src/components/users/FirstUserForm.jsx:61 +#: src/components/users/FirstUserForm.jsx:72 msgid "Use suggested username" msgstr "Usa el nom d'usuari suggerit" -#: src/components/users/FirstUserForm.jsx:140 +#: src/components/users/FirstUserForm.jsx:151 msgid "All fields are required" msgstr "Tots els camps són obligatoris." -#: src/components/users/FirstUserForm.jsx:147 +#: src/components/users/FirstUserForm.jsx:158 msgid "Please, try again." msgstr "Si us plau, torneu-ho a provar." -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Create user" msgstr "Crea un usuari" -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Edit user" msgstr "Edita l'usuari" -#: src/components/users/FirstUserForm.jsx:214 -#: src/components/users/FirstUserForm.jsx:216 +#: src/components/users/FirstUserForm.jsx:231 +#: src/components/users/FirstUserForm.jsx:233 msgid "User full name" msgstr "Nom complet de l'usuari" -#: src/components/users/FirstUserForm.jsx:254 +#: src/components/users/FirstUserForm.jsx:271 msgid "Edit password too" msgstr "Edita també la contrasenya" -#: src/components/users/FirstUserForm.jsx:269 +#: src/components/users/FirstUserForm.jsx:287 msgid "user autologin" msgstr "entrada de sessió automàtica de l'usuari" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:273 +#: src/components/users/FirstUserForm.jsx:291 msgid "Auto-login" msgstr "Entrada automàtica" @@ -2519,7 +2447,7 @@ msgstr "Entrada automàtica" msgid "No root authentication method defined yet." msgstr "Encara no s'ha definit cap mètode d'autenticació d'arrel." -#: src/components/users/RootAuthMethods.jsx:38 +#: src/components/users/RootAuthMethods.jsx:39 msgid "" "Please, define at least one authentication method for logging into the " "system as root." @@ -2527,56 +2455,54 @@ msgstr "" "Si us plau, definiu almenys un mètode d'autenticació per iniciar sessió al " "sistema com a arrel." -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:43 +#: src/components/users/RootAuthMethods.jsx:46 msgid "Set a password" msgstr "Establiu una contrasenya" -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:45 +#: src/components/users/RootAuthMethods.jsx:50 msgid "Upload a SSH Public Key" msgstr "Carrega una clau pública SSH" -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Set" msgstr "Estableix" -#: src/components/users/RootAuthMethods.jsx:129 +#: src/components/users/RootAuthMethods.jsx:132 msgid "Already set" msgstr "Ja s'ha establert" -#: src/components/users/RootAuthMethods.jsx:130 -#: src/components/users/RootAuthMethods.jsx:134 +#: src/components/users/RootAuthMethods.jsx:132 +#: src/components/users/RootAuthMethods.jsx:136 msgid "Not set" msgstr "No s'ha establert" #. TRANSLATORS: table header, user authentication method -#: src/components/users/RootAuthMethods.jsx:155 +#: src/components/users/RootAuthMethods.jsx:157 msgid "Method" msgstr "Mètode" -#: src/components/users/RootAuthMethods.jsx:170 +#: src/components/users/RootAuthMethods.jsx:174 msgid "SSH Key" msgstr "Clau SSH" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Change the root password" msgstr "Canvia la contrasenya d'arrel" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Set a root password" msgstr "Establiu una contrasenya d'arrel" -#: src/components/users/RootAuthMethods.jsx:194 -msgid "Add a SSH Public Key for root" -msgstr "Afegiu una clau pública SSH per a l'arrel" - -#: src/components/users/RootAuthMethods.jsx:194 +#: src/components/users/RootAuthMethods.jsx:203 msgid "Edit the SSH Public Key for root" msgstr "Edita la clau pública SSH per a l'arrel" -#: src/components/users/RootPasswordPopup.jsx:42 +#: src/components/users/RootAuthMethods.jsx:204 +msgid "Add a SSH Public Key for root" +msgstr "Afegiu una clau pública SSH per a l'arrel" + +#: src/components/users/RootPasswordPopup.jsx:43 msgid "Root password" msgstr "Contrasenya d'arrel" @@ -2610,6 +2536,37 @@ msgstr "Usuari primer" msgid "Root authentication" msgstr "Autenticació d'arrel" +#~ msgid "Reading file..." +#~ msgstr "Llegint el fitxer..." + +#~ msgid "Cannot read the file" +#~ msgstr "No es pot llegir el fitxer." + +#~ msgid "Agama Error" +#~ msgstr "Error de l'Agama" + +#~ msgid "Loading available products, please wait..." +#~ msgstr "Carregant els productes disponibles. Espereu, si us plau..." + +#, c-format +#~ msgid "There is %d destructive action planned" +#~ msgid_plural "There are %d destructive actions planned" +#~ msgstr[0] "Hi ha %d acció destructiva planificada." +#~ msgstr[1] "Hi ha %d accions destructives planificades." + +#, c-format +#~ msgid "Space action selector for %s" +#~ msgstr "Selector d'acció espacial per a %s" + +#~ msgid "Allow resize" +#~ msgstr "Permet-ne el canvi de mida" + +#~ msgid "Do not modify" +#~ msgstr "No la modifiquis" + +#~ msgid "Shrinkable" +#~ msgstr "Encongible" + #~ msgid "Choose a language" #~ msgstr "Trieu la llengua" @@ -2968,9 +2925,6 @@ msgstr "Autenticació d'arrel" #~ msgid "Waiting for information about selected device" #~ msgstr "Esperant informació sobre el dispositiu seleccionat" -#~ msgid "Waiting for information about space policy" -#~ msgstr "Esperant informació sobre la política d'espai" - #, c-format #~ msgid "" #~ "The filesystem will be allocated as a new partition at the installation " diff --git a/web/po/cs.po b/web/po/cs.po index 81a120ed48..18235aa011 100644 --- a/web/po/cs.po +++ b/web/po/cs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-30 02:27+0000\n" +"POT-Creation-Date: 2024-07-14 02:32+0000\n" "PO-Revision-Date: 2023-12-31 20:39+0000\n" "Last-Translator: Ladislav Slezák \n" "Language-Team: Czech =2 && n<=4) ? 1 : 2;\n" "X-Generator: Weblate 4.9.1\n" -#: src/MainLayout.jsx:40 +#: src/MainLayout.jsx:52 msgid "Agama" msgstr "" -#: src/MainLayout.jsx:82 +#: src/MainLayout.jsx:94 msgid "Change product" msgstr "" @@ -32,12 +32,11 @@ msgstr "" msgid "About" msgstr "" -#: src/components/core/About.jsx:71 +#: src/components/core/About.jsx:69 msgid "About Agama" msgstr "" -#. TRANSLATORS: content of the "About" popup (1/2) -#: src/components/core/About.jsx:76 +#: src/components/core/About.jsx:74 msgid "" "Agama is an experimental installer for (open)SUSE systems. It is still under " "development so, please, do not use it in production environments. If you " @@ -47,25 +46,16 @@ msgstr "" #. TRANSLATORS: content of the "About" popup (2/2) #. %s is replaced by the project URL -#: src/components/core/About.jsx:88 +#: src/components/core/About.jsx:86 #, c-format msgid "For more information, please visit the project's repository at %s." msgstr "" -#: src/components/core/About.jsx:94 src/components/core/FileViewer.jsx:81 -#: src/components/core/LogsButton.jsx:123 -#: src/components/software/SoftwarePatternsSelection.jsx:260 +#: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 +#: src/components/software/SoftwarePatternsSelection.jsx:268 msgid "Close" msgstr "Zavřít" -#: src/components/core/FileViewer.jsx:66 -msgid "Reading file..." -msgstr "Soubor se načítá…" - -#: src/components/core/FileViewer.jsx:72 -msgid "Cannot read the file" -msgstr "Soubor nelze přečíst" - #: src/components/core/InstallButton.jsx:32 msgid "Confirm Installation" msgstr "" @@ -86,9 +76,9 @@ msgid "Continue" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:97 -#: src/components/core/Popup.jsx:136 -#: src/components/network/WifiConnectionForm.jsx:131 +#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 +#: src/components/core/Popup.jsx:132 +#: src/components/network/WifiConnectionForm.jsx:134 msgid "Cancel" msgstr "" @@ -97,26 +87,25 @@ msgstr "" msgid "Install" msgstr "" -#: src/components/core/InstallationFinished.jsx:42 +#: src/components/core/InstallationFinished.jsx:48 msgid "TPM sealing requires the new system to be booted directly." msgstr "" -#: src/components/core/InstallationFinished.jsx:47 +#: src/components/core/InstallationFinished.jsx:53 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." msgstr "" -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "Hide details" msgstr "" -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "See more details" msgstr "" -#. TRANSLATORS: "Trusted Platform Module" is the name of the technology and "TPM" its abbreviation -#: src/components/core/InstallationFinished.jsx:55 +#: src/components/core/InstallationFinished.jsx:62 msgid "" "The final step to configure the Trusted Platform Module (TPM) to " "automatically open encrypted devices will take place during the first boot " @@ -124,27 +113,27 @@ msgid "" "the new boot loader." msgstr "" -#: src/components/core/InstallationFinished.jsx:97 +#: src/components/core/InstallationFinished.jsx:107 msgid "Congratulations!" msgstr "" -#: src/components/core/InstallationFinished.jsx:102 +#: src/components/core/InstallationFinished.jsx:116 msgid "The installation on your machine is complete." msgstr "" -#: src/components/core/InstallationFinished.jsx:105 +#: src/components/core/InstallationFinished.jsx:119 msgid "At this point you can power off the machine." msgstr "" -#: src/components/core/InstallationFinished.jsx:106 +#: src/components/core/InstallationFinished.jsx:121 msgid "At this point you can reboot the machine to log in to the new system." msgstr "" -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Finish" msgstr "" -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Reboot" msgstr "" @@ -152,40 +141,40 @@ msgstr "" msgid "Installing the system, please wait ..." msgstr "" -#: src/components/core/InstallerOptions.jsx:83 +#: src/components/core/InstallerOptions.jsx:92 msgid "Show installer options" msgstr "" -#: src/components/core/InstallerOptions.jsx:88 +#: src/components/core/InstallerOptions.jsx:95 msgid "Installer options" msgstr "" -#: src/components/core/InstallerOptions.jsx:94 -#: src/components/core/InstallerOptions.jsx:99 -#: src/components/core/InstallerOptions.jsx:100 -#: src/components/l10n/L10nPage.jsx:67 +#: src/components/core/InstallerOptions.jsx:98 +#: src/components/core/InstallerOptions.jsx:102 +#: src/components/core/InstallerOptions.jsx:103 +#: src/components/l10n/L10nPage.jsx:60 msgid "Language" msgstr "" -#: src/components/core/InstallerOptions.jsx:114 -#: src/components/core/InstallerOptions.jsx:121 +#: src/components/core/InstallerOptions.jsx:115 +#: src/components/core/InstallerOptions.jsx:120 msgid "Keyboard layout" msgstr "" -#: src/components/core/InstallerOptions.jsx:130 +#: src/components/core/InstallerOptions.jsx:129 msgid "Cannot be changed in remote installation" msgstr "" -#: src/components/core/InstallerOptions.jsx:135 -#: src/components/network/IpSettingsForm.jsx:210 -#: src/components/product/ProductRegistrationPage.jsx:89 -#: src/components/storage/BootSelection.jsx:228 -#: src/components/storage/DeviceSelection.jsx:240 -#: src/components/storage/EncryptionSettingsDialog.jsx:138 -#: src/components/storage/SpacePolicySelection.jsx:198 -#: src/components/storage/VolumeDialog.jsx:781 -#: src/components/storage/ZFCPPage.jsx:503 -#: src/components/users/FirstUserForm.jsx:285 +#: src/components/core/InstallerOptions.jsx:142 +#: src/components/network/IpSettingsForm.jsx:228 +#: src/components/product/ProductRegistrationPage.jsx:85 +#: src/components/storage/BootSelection.jsx:250 +#: src/components/storage/DeviceSelection.jsx:254 +#: src/components/storage/EncryptionSettingsDialog.jsx:155 +#: src/components/storage/SpacePolicySelection.jsx:200 +#: src/components/storage/VolumeDialog.jsx:794 +#: src/components/storage/ZFCPPage.jsx:528 +#: src/components/users/FirstUserForm.jsx:303 msgid "Accept" msgstr "" @@ -194,29 +183,26 @@ msgid "" "Before starting the installation, you need to address the following problems:" msgstr "" -#: src/components/core/ListSearch.jsx:51 +#: src/components/core/ListSearch.jsx:48 msgid "Search" msgstr "" -#: src/components/core/LoginPage.jsx:61 +#: src/components/core/LoginPage.jsx:64 msgid "Could not log in. Please, make sure that the password is correct." msgstr "" -#: src/components/core/LoginPage.jsx:63 +#: src/components/core/LoginPage.jsx:66 msgid "Could not authenticate against the server, please check it." msgstr "" #. TRANSLATORS: Title for a form to provide the password for the root user. %s #. will be replaced by "root" -#: src/components/core/LoginPage.jsx:71 +#: src/components/core/LoginPage.jsx:74 #, c-format msgid "Log in as %s" msgstr "" -#. TRANSLATORS: description why root password is needed. The text in the -#. square brackets [] is displayed in bold, use only please, do not translate -#. it and keep the brackets. -#: src/components/core/LoginPage.jsx:76 +#: src/components/core/LoginPage.jsx:80 msgid "The installer requires [root] user privileges." msgstr "" @@ -224,62 +210,62 @@ msgstr "" msgid "Please, provide its password to log in to the system." msgstr "" -#: src/components/core/LoginPage.jsx:98 +#: src/components/core/LoginPage.jsx:96 msgid "Login form" msgstr "" -#: src/components/core/LoginPage.jsx:104 +#: src/components/core/LoginPage.jsx:102 msgid "Password input" msgstr "" -#: src/components/core/LoginPage.jsx:113 +#: src/components/core/LoginPage.jsx:111 msgid "Log in" msgstr "" -#: src/components/core/LoginPage.jsx:124 +#: src/components/core/LoginPage.jsx:121 msgid "More about this" msgstr "" -#: src/components/core/LogsButton.jsx:103 +#: src/components/core/LogsButton.jsx:101 msgid "Collecting logs..." msgstr "" -#: src/components/core/LogsButton.jsx:103 -#: src/components/core/LogsButton.jsx:106 +#: src/components/core/LogsButton.jsx:101 +#: src/components/core/LogsButton.jsx:104 msgid "Download logs" msgstr "" -#: src/components/core/LogsButton.jsx:112 +#: src/components/core/LogsButton.jsx:111 msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." msgstr "" -#: src/components/core/LogsButton.jsx:120 +#: src/components/core/LogsButton.jsx:121 msgid "Something went wrong while collecting logs. Please, try again." msgstr "" -#: src/components/core/PasswordAndConfirmationInput.jsx:48 +#: src/components/core/PasswordAndConfirmationInput.jsx:55 msgid "Passwords do not match" msgstr "" -#: src/components/core/PasswordAndConfirmationInput.jsx:72 -#: src/components/network/WifiConnectionForm.jsx:120 -#: src/components/storage/iscsi/AuthFields.jsx:95 -#: src/components/storage/iscsi/AuthFields.jsx:100 -#: src/components/users/RootAuthMethods.jsx:163 +#: src/components/core/PasswordAndConfirmationInput.jsx:79 +#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/storage/iscsi/AuthFields.jsx:90 +#: src/components/storage/iscsi/AuthFields.jsx:94 +#: src/components/users/RootAuthMethods.jsx:165 msgid "Password" msgstr "" -#: src/components/core/PasswordAndConfirmationInput.jsx:85 +#: src/components/core/PasswordAndConfirmationInput.jsx:90 msgid "Password confirmation" msgstr "" -#: src/components/core/PasswordInput.jsx:64 +#: src/components/core/PasswordInput.jsx:61 msgid "Password visibility button" msgstr "" -#: src/components/core/Popup.jsx:100 +#: src/components/core/Popup.jsx:92 msgid "Confirm" msgstr "" @@ -297,117 +283,106 @@ msgstr "" msgid "In progress" msgstr "" -#: src/components/core/ProgressReport.jsx:70 +#: src/components/core/ProgressReport.jsx:74 msgid "Pending" msgstr "" -#: src/components/core/ProgressReport.jsx:134 +#: src/components/core/ProgressReport.jsx:138 msgid "Waiting for progress status..." msgstr "" #: src/components/core/RowActions.jsx:64 -#: src/components/storage/PartitionsField.jsx:454 -#: src/components/storage/ProposalActionsSummary.jsx:226 +#: src/components/storage/PartitionsField.jsx:491 +#: src/components/storage/ProposalActionsSummary.jsx:233 msgid "Actions" msgstr "" -#: src/components/core/SectionSkeleton.jsx:29 +#: src/components/core/SectionSkeleton.jsx:27 msgid "Waiting" msgstr "" -#: src/components/core/Selector.jsx:126 -#: src/components/software/SoftwarePatternsSelection.jsx:212 -msgid "auto selected" -msgstr "" - -#. TRANSLATORS: page title -#: src/components/core/ServerError.jsx:34 -msgid "Agama Error" -msgstr "" - -#: src/components/core/ServerError.jsx:38 +#: src/components/core/ServerError.jsx:47 msgid "Cannot connect to Agama server" msgstr "" -#: src/components/core/ServerError.jsx:43 +#: src/components/core/ServerError.jsx:51 msgid "Please, check whether it is running." msgstr "" -#. TRANSLATORS: button label -#: src/components/core/ServerError.jsx:51 +#: src/components/core/ServerError.jsx:56 msgid "Reload" msgstr "" -#: src/components/l10n/KeyboardSelection.jsx:45 +#: src/components/l10n/KeyboardSelection.jsx:41 msgid "Filter by description or keymap code" msgstr "" -#: src/components/l10n/KeyboardSelection.jsx:85 +#: src/components/l10n/KeyboardSelection.jsx:71 msgid "None of the keymaps match the filter." msgstr "" -#: src/components/l10n/KeyboardSelection.jsx:92 +#: src/components/l10n/KeyboardSelection.jsx:77 msgid "Keyboard selection" msgstr "" -#: src/components/l10n/KeyboardSelection.jsx:107 -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/l10n/LocaleSelection.jsx:107 -#: src/components/l10n/TimezoneSelection.jsx:145 -#: src/components/product/ProductSelectionPage.jsx:101 +#: src/components/l10n/KeyboardSelection.jsx:90 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/l10n/LocaleSelection.jsx:92 +#: src/components/l10n/TimezoneSelection.jsx:125 +#: src/components/product/ProductSelectionPage.jsx:90 msgid "Select" msgstr "" -#: src/components/l10n/L10nPage.jsx:60 src/components/l10n/routes.js:34 -#: src/components/overview/L10nSection.jsx:42 +#: src/components/l10n/L10nPage.jsx:53 +#: src/components/overview/L10nSection.jsx:37 src/routes/l10n.js:38 msgid "Localization" msgstr "" -#: src/components/l10n/L10nPage.jsx:68 src/components/l10n/L10nPage.jsx:79 -#: src/components/l10n/L10nPage.jsx:90 +#: src/components/l10n/L10nPage.jsx:61 src/components/l10n/L10nPage.jsx:70 +#: src/components/l10n/L10nPage.jsx:80 msgid "Not selected yet" msgstr "" -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/network/NetworkPage.jsx:102 -#: src/components/storage/InstallationDeviceField.jsx:105 -#: src/components/storage/ProposalActionsSummary.jsx:228 -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/storage/InstallationDeviceField.jsx:108 +#: src/components/storage/ProposalActionsSummary.jsx:238 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Change" msgstr "" -#: src/components/l10n/L10nPage.jsx:78 +#: src/components/l10n/L10nPage.jsx:70 msgid "Keyboard" msgstr "" -#: src/components/l10n/L10nPage.jsx:89 +#: src/components/l10n/L10nPage.jsx:79 msgid "Time zone" msgstr "" -#: src/components/l10n/LocaleSelection.jsx:44 +#: src/components/l10n/LocaleSelection.jsx:39 msgid "Filter by language, territory or locale code" msgstr "" -#: src/components/l10n/LocaleSelection.jsx:84 +#: src/components/l10n/LocaleSelection.jsx:72 msgid "None of the locales match the filter." msgstr "" -#: src/components/l10n/LocaleSelection.jsx:91 +#: src/components/l10n/LocaleSelection.jsx:78 msgid "Locale selection" msgstr "" -#: src/components/l10n/TimezoneSelection.jsx:71 +#: src/components/l10n/TimezoneSelection.jsx:64 msgid "Filter by territory, time zone code or UTC offset" msgstr "" -#: src/components/l10n/TimezoneSelection.jsx:122 +#: src/components/l10n/TimezoneSelection.jsx:101 msgid "None of the time zones match the filter." msgstr "" -#: src/components/l10n/TimezoneSelection.jsx:129 +#: src/components/l10n/TimezoneSelection.jsx:107 msgid " Timezone selection" msgstr "" @@ -416,111 +391,111 @@ msgid "Loading installation environment, please wait." msgstr "" #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:78 -#: src/components/network/DnsDataList.jsx:84 +#: src/components/network/AddressesDataList.jsx:88 +#: src/components/network/DnsDataList.jsx:95 msgid "Remove" msgstr "" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:90 -#: src/components/network/AddressesDataList.jsx:91 +#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:101 #: src/components/network/IpAddressInput.jsx:33 msgid "IP Address" msgstr "" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:99 -#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:109 +#: src/components/network/AddressesDataList.jsx:110 msgid "Prefix length or netmask" msgstr "" -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add an address" msgstr "" #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add another address" msgstr "" -#: src/components/network/AddressesDataList.jsx:121 +#: src/components/network/AddressesDataList.jsx:131 msgid "Addresses" msgstr "" -#: src/components/network/AddressesDataList.jsx:123 +#: src/components/network/AddressesDataList.jsx:133 msgid "Addresses data list" msgstr "" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:67 -#: src/components/network/ConnectionsTable.jsx:95 -#: src/components/storage/ZFCPPage.jsx:361 +#: src/components/network/ConnectionsTable.jsx:64 +#: src/components/network/ConnectionsTable.jsx:92 +#: src/components/storage/ZFCPPage.jsx:381 #: src/components/storage/iscsi/InitiatorForm.jsx:52 #: src/components/storage/iscsi/InitiatorPresenter.jsx:68 #: src/components/storage/iscsi/InitiatorPresenter.jsx:85 -#: src/components/storage/iscsi/NodesPresenter.jsx:100 -#: src/components/storage/iscsi/NodesPresenter.jsx:121 +#: src/components/storage/iscsi/NodesPresenter.jsx:98 +#: src/components/storage/iscsi/NodesPresenter.jsx:119 msgid "Name" msgstr "" #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:69 -#: src/components/network/ConnectionsTable.jsx:96 +#: src/components/network/ConnectionsTable.jsx:66 +#: src/components/network/ConnectionsTable.jsx:93 msgid "IP addresses" msgstr "" -#: src/components/network/ConnectionsTable.jsx:77 -#: src/components/network/WifiNetworksListPage.jsx:100 -#: src/components/network/WifiNetworksListPage.jsx:124 -#: src/components/storage/PartitionsField.jsx:320 +#: src/components/network/ConnectionsTable.jsx:74 +#: src/components/network/WifiNetworksListPage.jsx:107 +#: src/components/network/WifiNetworksListPage.jsx:130 +#: src/components/storage/PartitionsField.jsx:347 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 -#: src/components/users/FirstUser.jsx:116 +#: src/components/users/FirstUser.jsx:120 msgid "Edit" msgstr "" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:80 -#: src/components/network/IpSettingsForm.jsx:136 +#: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/IpSettingsForm.jsx:151 #, c-format msgid "Edit connection %s" msgstr "" -#: src/components/network/ConnectionsTable.jsx:84 -#: src/components/network/WifiNetworksListPage.jsx:103 -#: src/components/network/WifiNetworksListPage.jsx:127 +#: src/components/network/ConnectionsTable.jsx:81 +#: src/components/network/WifiNetworksListPage.jsx:109 +#: src/components/network/WifiNetworksListPage.jsx:137 msgid "Forget" msgstr "" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:86 +#: src/components/network/ConnectionsTable.jsx:83 #, c-format msgid "Forget connection %s" msgstr "" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:101 +#: src/components/network/ConnectionsTable.jsx:98 #, c-format msgid "Actions for connection %s" msgstr "" #. TRANSLATORS: input field name -#: src/components/network/DnsDataList.jsx:75 -#: src/components/network/DnsDataList.jsx:76 +#: src/components/network/DnsDataList.jsx:81 +#: src/components/network/DnsDataList.jsx:82 msgid "Server IP" msgstr "" -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add DNS" msgstr "" #. TRANSLATORS: button label -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add another DNS" msgstr "" -#: src/components/network/DnsDataList.jsx:98 +#: src/components/network/DnsDataList.jsx:109 msgid "DNS" msgstr "" @@ -530,81 +505,81 @@ msgid "IP prefix or netmask" msgstr "" #. TRANSLATORS: error message -#: src/components/network/IpSettingsForm.jsx:90 +#: src/components/network/IpSettingsForm.jsx:104 msgid "At least one address must be provided for selected mode" msgstr "" #. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) -#: src/components/network/IpSettingsForm.jsx:145 -#: src/components/network/IpSettingsForm.jsx:150 -#: src/components/network/IpSettingsForm.jsx:152 +#: src/components/network/IpSettingsForm.jsx:160 +#: src/components/network/IpSettingsForm.jsx:165 +#: src/components/network/IpSettingsForm.jsx:167 msgid "Mode" msgstr "" -#: src/components/network/IpSettingsForm.jsx:156 +#: src/components/network/IpSettingsForm.jsx:174 msgid "Automatic (DHCP)" msgstr "" #. TRANSLATORS: manual network configuration mode with a static IP address -#: src/components/network/IpSettingsForm.jsx:158 +#: src/components/network/IpSettingsForm.jsx:177 #: src/components/storage/iscsi/NodeStartupOptions.js:25 msgid "Manual" msgstr "" #. TRANSLATORS: network gateway configuration -#: src/components/network/IpSettingsForm.jsx:166 -#: src/components/network/IpSettingsForm.jsx:169 +#: src/components/network/IpSettingsForm.jsx:185 +#: src/components/network/IpSettingsForm.jsx:188 msgid "Gateway" msgstr "" -#: src/components/network/IpSettingsForm.jsx:178 +#: src/components/network/IpSettingsForm.jsx:196 msgid "Gateway can be defined only in 'Manual' mode" msgstr "" -#: src/components/network/NetworkPage.jsx:85 +#: src/components/network/NetworkPage.jsx:93 msgid "No Wi-Fi supported" msgstr "" -#: src/components/network/NetworkPage.jsx:86 +#: src/components/network/NetworkPage.jsx:95 msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." msgstr "" -#: src/components/network/NetworkPage.jsx:99 +#: src/components/network/NetworkPage.jsx:109 msgid "Wi-Fi" msgstr "" #. TRANSLATORS: button label, connect to a WiFi network -#: src/components/network/NetworkPage.jsx:102 -#: src/components/network/WifiConnectionForm.jsx:128 -#: src/components/network/WifiNetworksListPage.jsx:97 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:130 +#: src/components/network/WifiNetworksListPage.jsx:105 msgid "Connect" msgstr "" -#: src/components/network/NetworkPage.jsx:109 +#: src/components/network/NetworkPage.jsx:119 #, c-format msgid "Conected to %s" msgstr "" -#: src/components/network/NetworkPage.jsx:114 +#: src/components/network/NetworkPage.jsx:126 msgid "No connected yet" msgstr "" -#: src/components/network/NetworkPage.jsx:115 +#: src/components/network/NetworkPage.jsx:127 msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." msgstr "" -#: src/components/network/NetworkPage.jsx:136 +#: src/components/network/NetworkPage.jsx:156 msgid "Wired" msgstr "" -#: src/components/network/NetworkPage.jsx:139 +#: src/components/network/NetworkPage.jsx:160 msgid "No wired connections found" msgstr "" -#: src/components/network/NetworkPage.jsx:149 +#: src/components/network/NetworkPage.jsx:173 #: src/components/network/routes.js:59 msgid "Network" msgstr "" @@ -620,16 +595,16 @@ msgstr "" msgid "WPA & WPA2 Personal" msgstr "" -#: src/components/network/WifiConnectionForm.jsx:86 -#: src/components/product/ProductRegistrationPage.jsx:69 -#: src/components/storage/ZFCPDiskForm.jsx:108 -#: src/components/storage/iscsi/DiscoverForm.jsx:110 -#: src/components/storage/iscsi/LoginForm.jsx:72 -#: src/components/users/FirstUserForm.jsx:203 +#: src/components/network/WifiConnectionForm.jsx:85 +#: src/components/product/ProductRegistrationPage.jsx:68 +#: src/components/storage/ZFCPDiskForm.jsx:105 +#: src/components/storage/iscsi/DiscoverForm.jsx:98 +#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/users/FirstUserForm.jsx:217 msgid "Something went wrong" msgstr "" -#: src/components/network/WifiConnectionForm.jsx:87 +#: src/components/network/WifiConnectionForm.jsx:86 msgid "Please, review provided settings and try again." msgstr "" @@ -640,103 +615,103 @@ msgid "SSID" msgstr "" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:104 -#: src/components/network/WifiConnectionForm.jsx:107 +#: src/components/network/WifiConnectionForm.jsx:105 +#: src/components/network/WifiConnectionForm.jsx:108 msgid "Security" msgstr "" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:116 +#: src/components/network/WifiConnectionForm.jsx:117 msgid "WPA Password" msgstr "" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:51 -#: src/components/network/WifiNetworksListPage.jsx:111 +#: src/components/network/WifiNetworksListPage.jsx:63 +#: src/components/network/WifiNetworksListPage.jsx:117 msgid "Connecting" msgstr "" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:54 -#: src/components/network/WifiNetworksListPage.jsx:115 -#: src/components/network/WifiNetworksListPage.jsx:154 +#: src/components/network/WifiNetworksListPage.jsx:66 +#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:164 msgid "Connected" msgstr "" #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:59 -#: src/components/network/WifiNetworksListPage.jsx:113 +#: src/components/network/WifiNetworksListPage.jsx:71 +#: src/components/network/WifiNetworksListPage.jsx:119 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" msgstr "" -#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:127 msgid "Disconnect" msgstr "" -#: src/components/network/WifiNetworksListPage.jsx:142 +#: src/components/network/WifiNetworksListPage.jsx:150 msgid "Connect to a hidden network" msgstr "" -#: src/components/network/WifiNetworksListPage.jsx:153 +#: src/components/network/WifiNetworksListPage.jsx:161 msgid "configured" msgstr "" -#: src/components/network/WifiNetworksListPage.jsx:244 +#: src/components/network/WifiNetworksListPage.jsx:265 msgid "Connect to hidden network" msgstr "" -#: src/components/network/WifiSelectorPage.jsx:131 +#: src/components/network/WifiSelectorPage.jsx:136 msgid "Connect to a Wi-Fi network" msgstr "" #. TRANSLATORS: %s will be replaced by a language name and territory, example: #. "English (United States)". -#: src/components/overview/L10nSection.jsx:38 +#: src/components/overview/L10nSection.jsx:33 #, c-format msgid "The system will use %s as its default language." msgstr "" -#: src/components/overview/OverviewPage.jsx:45 +#: src/components/overview/OverviewPage.jsx:47 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "" -#: src/components/overview/OverviewPage.jsx:46 +#: src/components/overview/OverviewPage.jsx:48 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:290 +#: src/components/storage/ProposalPage.jsx:307 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "" -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/overview/SoftwareSection.jsx:86 #: src/components/software/SoftwarePage.jsx:155 #: src/components/software/routes.js:32 msgid "Software" msgstr "" -#: src/components/overview/OverviewPage.jsx:52 +#: src/components/overview/OverviewPage.jsx:54 msgid "Ready for installation" msgstr "" -#: src/components/overview/OverviewPage.jsx:102 +#: src/components/overview/OverviewPage.jsx:104 msgid "Installation" msgstr "" -#: src/components/overview/OverviewPage.jsx:103 +#: src/components/overview/OverviewPage.jsx:105 msgid "Before installing, please check the following problems." msgstr "" -#: src/components/overview/OverviewPage.jsx:114 +#: src/components/overview/OverviewPage.jsx:116 msgid "" "Take your time to check your configuration before starting the installation " "process." msgstr "" -#: src/components/overview/OverviewPage.jsx:123 +#: src/components/overview/OverviewPage.jsx:125 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -804,35 +779,35 @@ msgid "" "custom strategy to find the needed space" msgstr "" -#: src/components/overview/StorageSection.jsx:175 -#: src/components/storage/InstallationDeviceField.jsx:63 +#: src/components/overview/StorageSection.jsx:179 +#: src/components/storage/InstallationDeviceField.jsx:66 msgid "No device selected yet" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:182 +#: src/components/overview/StorageSection.jsx:186 #, c-format msgid "Install using device %s shrinking existing partitions as needed" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:186 +#: src/components/overview/StorageSection.jsx:190 #, c-format msgid "Install using device %s without modifying existing partitions" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:190 +#: src/components/overview/StorageSection.jsx:194 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:195 +#: src/components/overview/StorageSection.jsx:199 #, c-format msgid "Install using device %s with a custom strategy to find the needed space" msgstr "" @@ -846,19 +821,15 @@ msgstr "" msgid "Register %s" msgstr "" -#: src/components/product/ProductRegistrationPage.jsx:74 +#: src/components/product/ProductRegistrationPage.jsx:73 msgid "Registration code" msgstr "" -#: src/components/product/ProductRegistrationPage.jsx:77 +#: src/components/product/ProductRegistrationPage.jsx:76 msgid "Email" msgstr "" -#: src/components/product/ProductSelectionPage.jsx:58 -msgid "Loading available products, please wait..." -msgstr "" - -#: src/components/product/ProductSelectionProgress.jsx:53 +#: src/components/product/ProductSelectionProgress.jsx:49 msgid "Configuring the product, please wait ..." msgstr "" @@ -877,7 +848,7 @@ msgid "Encrypted Device" msgstr "" #. TRANSLATORS: field label -#: src/components/questions/LuksActivationQuestion.jsx:69 +#: src/components/questions/LuksActivationQuestion.jsx:67 msgid "Encryption Password" msgstr "" @@ -897,17 +868,21 @@ msgstr "" msgid "Change selection" msgstr "" -#: src/components/software/SoftwarePatternsSelection.jsx:230 +#: src/components/software/SoftwarePatternsSelection.jsx:223 +msgid "auto selected" +msgstr "" + +#: src/components/software/SoftwarePatternsSelection.jsx:241 msgid "None of the patterns match the filter." msgstr "" -#: src/components/software/SoftwarePatternsSelection.jsx:238 +#: src/components/software/SoftwarePatternsSelection.jsx:248 msgid "Software selection" msgstr "" #. TRANSLATORS: search field placeholder text -#: src/components/software/SoftwarePatternsSelection.jsx:241 -#: src/components/software/SoftwarePatternsSelection.jsx:242 +#: src/components/software/SoftwarePatternsSelection.jsx:251 +#: src/components/software/SoftwarePatternsSelection.jsx:252 msgid "Filter by pattern title or description" msgstr "" @@ -918,7 +893,7 @@ msgstr "" msgid "Installation will take %s." msgstr "" -#: src/components/software/UsedSize.jsx:38 +#: src/components/software/UsedSize.jsx:37 msgid "This space includes the base system and the selected software patterns." msgstr "" @@ -926,63 +901,62 @@ msgstr "" msgid "Change boot options" msgstr "" -#: src/components/storage/BootConfigField.jsx:87 +#: src/components/storage/BootConfigField.jsx:81 msgid "Installation will not configure partitions for booting." msgstr "" -#: src/components/storage/BootConfigField.jsx:89 +#: src/components/storage/BootConfigField.jsx:85 msgid "" "Installation will configure partitions for booting at the installation disk." msgstr "" -#. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/BootConfigField.jsx:92 +#: src/components/storage/BootConfigField.jsx:89 #, c-format msgid "Installation will configure partitions for booting at %s." msgstr "" -#: src/components/storage/BootSelection.jsx:127 +#: src/components/storage/BootSelection.jsx:132 msgid "" "To ensure the new system is able to boot, the installer may need to create " "or configure some partitions in the appropriate disk." msgstr "" -#: src/components/storage/BootSelection.jsx:133 +#: src/components/storage/BootSelection.jsx:138 msgid "Partitions to boot will be allocated at the installation disk." msgstr "" #. TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") -#: src/components/storage/BootSelection.jsx:138 +#: src/components/storage/BootSelection.jsx:143 #, c-format msgid "Partitions to boot will be allocated at the installation disk (%s)." msgstr "" -#: src/components/storage/BootSelection.jsx:154 +#: src/components/storage/BootSelection.jsx:159 msgid "Select booting partition" msgstr "" -#: src/components/storage/BootSelection.jsx:170 +#: src/components/storage/BootSelection.jsx:180 #: src/components/storage/iscsi/NodeStartupOptions.js:27 msgid "Automatic" msgstr "" -#: src/components/storage/BootSelection.jsx:183 +#: src/components/storage/BootSelection.jsx:198 msgid "Select a disk" msgstr "" -#: src/components/storage/BootSelection.jsx:189 +#: src/components/storage/BootSelection.jsx:204 msgid "Partitions to boot will be allocated at the following device." msgstr "" -#: src/components/storage/BootSelection.jsx:192 +#: src/components/storage/BootSelection.jsx:207 msgid "Choose a disk for placing the boot loader" msgstr "" -#: src/components/storage/BootSelection.jsx:210 +#: src/components/storage/BootSelection.jsx:230 msgid "Do not configure" msgstr "" -#: src/components/storage/BootSelection.jsx:215 +#: src/components/storage/BootSelection.jsx:236 msgid "" "No partitions will be automatically configured for booting. Use with caution." msgstr "" @@ -991,269 +965,258 @@ msgstr "" msgid "Waiting for progress report" msgstr "" -#: src/components/storage/DASDFormatProgress.jsx:68 +#: src/components/storage/DASDFormatProgress.jsx:67 msgid "Formatting DASD devices" msgstr "" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "No" msgstr "" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "Yes" msgstr "" -#: src/components/storage/DASDTable.jsx:63 -#: src/components/storage/ZFCPDiskForm.jsx:112 -#: src/components/storage/ZFCPPage.jsx:302 -#: src/components/storage/ZFCPPage.jsx:362 +#: src/components/storage/DASDTable.jsx:69 +#: src/components/storage/ZFCPDiskForm.jsx:110 +#: src/components/storage/ZFCPPage.jsx:324 +#: src/components/storage/ZFCPPage.jsx:382 msgid "Channel ID" msgstr "" #. TRANSLATORS: table header -#: src/components/storage/DASDTable.jsx:64 -#: src/components/storage/ZFCPPage.jsx:303 -#: src/components/storage/iscsi/NodesPresenter.jsx:104 -#: src/components/storage/iscsi/NodesPresenter.jsx:125 -#: src/components/users/RootAuthMethods.jsx:157 +#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/ZFCPPage.jsx:325 +#: src/components/storage/iscsi/NodesPresenter.jsx:102 +#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/users/RootAuthMethods.jsx:159 msgid "Status" msgstr "" -#: src/components/storage/DASDTable.jsx:65 -#: src/components/storage/DeviceSelectorTable.jsx:186 -#: src/components/storage/ProposalResultTable.jsx:120 -#: src/components/storage/SpaceActionsTable.jsx:141 -#: src/components/storage/VolumeLocationSelectorTable.jsx:100 +#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DeviceSelectorTable.jsx:197 +#: src/components/storage/ProposalResultTable.jsx:130 +#: src/components/storage/SpaceActionsTable.jsx:200 +#: src/components/storage/VolumeLocationSelectorTable.jsx:107 msgid "Device" msgstr "" -#: src/components/storage/DASDTable.jsx:66 +#: src/components/storage/DASDTable.jsx:72 msgid "Type" msgstr "" #. TRANSLATORS: table header, the column contains "Yes"/"No" values #. for the DIAG access mode (special disk access mode on IBM mainframes), #. usually keep untranslated -#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/DASDTable.jsx:76 msgid "DIAG" msgstr "" -#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DASDTable.jsx:77 msgid "Formatted" msgstr "" -#: src/components/storage/DASDTable.jsx:72 +#: src/components/storage/DASDTable.jsx:78 msgid "Partition Info" msgstr "" #. TRANSLATORS: drop down menu label -#: src/components/storage/DASDTable.jsx:107 +#: src/components/storage/DASDTable.jsx:115 msgid "Perform an action" msgstr "" -#. TRANSLATORS: drop down menu action, activate the device -#: src/components/storage/DASDTable.jsx:113 -#: src/components/storage/ZFCPPage.jsx:333 +#: src/components/storage/DASDTable.jsx:122 +#: src/components/storage/ZFCPPage.jsx:353 msgid "Activate" msgstr "" -#. TRANSLATORS: drop down menu action, deactivate the device -#: src/components/storage/DASDTable.jsx:115 -#: src/components/storage/ZFCPPage.jsx:375 +#: src/components/storage/DASDTable.jsx:126 +#: src/components/storage/ZFCPPage.jsx:395 msgid "Deactivate" msgstr "" -#. TRANSLATORS: drop down menu action, enable DIAG access method -#: src/components/storage/DASDTable.jsx:118 +#: src/components/storage/DASDTable.jsx:131 msgid "Set DIAG On" msgstr "" -#. TRANSLATORS: drop down menu action, disable DIAG access method -#: src/components/storage/DASDTable.jsx:120 +#: src/components/storage/DASDTable.jsx:135 msgid "Set DIAG Off" msgstr "" -#. TRANSLATORS: drop down menu action, format the disk -#: src/components/storage/DASDTable.jsx:123 +#: src/components/storage/DASDTable.jsx:140 msgid "Format" msgstr "" -#: src/components/storage/DASDTable.jsx:223 -#: src/components/storage/DASDTable.jsx:224 +#: src/components/storage/DASDTable.jsx:261 +#: src/components/storage/DASDTable.jsx:262 msgid "Filter by min channel" msgstr "" -#: src/components/storage/DASDTable.jsx:231 +#: src/components/storage/DASDTable.jsx:269 msgid "Remove min channel filter" msgstr "" -#: src/components/storage/DASDTable.jsx:244 -#: src/components/storage/DASDTable.jsx:245 +#: src/components/storage/DASDTable.jsx:283 +#: src/components/storage/DASDTable.jsx:284 msgid "Filter by max channel" msgstr "" -#: src/components/storage/DASDTable.jsx:252 +#: src/components/storage/DASDTable.jsx:291 msgid "Remove max channel filter" msgstr "" -#: src/components/storage/DeviceSelection.jsx:101 +#: src/components/storage/DeviceSelection.jsx:108 msgid "Loading data, please wait a second..." msgstr "" -#. TRANSLATORS: description for using plain partitions for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:136 +#: src/components/storage/DeviceSelection.jsx:144 msgid "" "The file systems will be allocated by default as [new partitions in the " "selected device]." msgstr "" -#. TRANSLATORS: description for using logical volumes for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:141 +#: src/components/storage/DeviceSelection.jsx:151 msgid "" "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." msgstr "" -#: src/components/storage/DeviceSelection.jsx:149 +#: src/components/storage/DeviceSelection.jsx:160 msgid "Select installation device" msgstr "" -#: src/components/storage/DeviceSelection.jsx:155 +#: src/components/storage/DeviceSelection.jsx:166 msgid "Install new system on" msgstr "" -#: src/components/storage/DeviceSelection.jsx:158 +#: src/components/storage/DeviceSelection.jsx:169 msgid "An existing disk" msgstr "" -#: src/components/storage/DeviceSelection.jsx:167 +#: src/components/storage/DeviceSelection.jsx:178 msgid "A new LVM Volume Group" msgstr "" -#: src/components/storage/DeviceSelection.jsx:192 +#: src/components/storage/DeviceSelection.jsx:203 msgid "Device selector for target disk" msgstr "" -#: src/components/storage/DeviceSelection.jsx:215 +#: src/components/storage/DeviceSelection.jsx:226 msgid "Device selector for new LVM volume group" msgstr "" -#: src/components/storage/DeviceSelection.jsx:228 +#: src/components/storage/DeviceSelection.jsx:242 msgid "Prepare more devices by configuring advanced" msgstr "" -#: src/components/storage/DeviceSelection.jsx:229 +#: src/components/storage/DeviceSelection.jsx:243 msgid "storage techs" msgstr "" #. TRANSLATORS: multipath device type -#: src/components/storage/DeviceSelectorTable.jsx:57 +#: src/components/storage/DeviceSelectorTable.jsx:61 msgid "Multipath" msgstr "" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/DeviceSelectorTable.jsx:62 +#: src/components/storage/DeviceSelectorTable.jsx:66 #, c-format msgid "DASD %s" msgstr "" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/DeviceSelectorTable.jsx:67 +#: src/components/storage/DeviceSelectorTable.jsx:71 #, c-format msgid "Software %s" msgstr "" -#: src/components/storage/DeviceSelectorTable.jsx:72 +#: src/components/storage/DeviceSelectorTable.jsx:76 msgid "SD Card" msgstr "" #. TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" -#: src/components/storage/DeviceSelectorTable.jsx:77 +#: src/components/storage/DeviceSelectorTable.jsx:81 #, c-format msgid "%s disk" msgstr "" -#: src/components/storage/DeviceSelectorTable.jsx:78 +#: src/components/storage/DeviceSelectorTable.jsx:82 msgid "Disk" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:98 +#: src/components/storage/DeviceSelectorTable.jsx:102 #, c-format msgid "Members: %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:107 +#: src/components/storage/DeviceSelectorTable.jsx:111 #, c-format msgid "Devices: %s" msgstr "" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/DeviceSelectorTable.jsx:116 +#: src/components/storage/DeviceSelectorTable.jsx:120 #, c-format msgid "Wires: %s" msgstr "" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/DeviceSelectorTable.jsx:152 +#: src/components/storage/DeviceSelectorTable.jsx:155 #, c-format msgid "%s with %d partitions" msgstr "" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/DeviceSelectorTable.jsx:158 -#: src/components/storage/SpaceActionsTable.jsx:114 +#: src/components/storage/DeviceSelectorTable.jsx:161 +#: src/components/storage/SpaceActionsTable.jsx:175 msgid "No content found" msgstr "" -#: src/components/storage/DeviceSelectorTable.jsx:187 -#: src/components/storage/PartitionsField.jsx:450 -#: src/components/storage/ProposalResultTable.jsx:122 -#: src/components/storage/SpaceActionsTable.jsx:142 -#: src/components/storage/VolumeLocationSelectorTable.jsx:101 +#: src/components/storage/DeviceSelectorTable.jsx:198 +#: src/components/storage/PartitionsField.jsx:487 +#: src/components/storage/ProposalResultTable.jsx:132 +#: src/components/storage/SpaceActionsTable.jsx:201 +#: src/components/storage/VolumeLocationSelectorTable.jsx:108 msgid "Details" msgstr "" -#: src/components/storage/DeviceSelectorTable.jsx:188 -#: src/components/storage/PartitionsField.jsx:451 -#: src/components/storage/ProposalResultTable.jsx:123 -#: src/components/storage/SpaceActionsTable.jsx:143 -#: src/components/storage/VolumeFields.jsx:474 -#: src/components/storage/VolumeLocationSelectorTable.jsx:103 +#: src/components/storage/DeviceSelectorTable.jsx:199 +#: src/components/storage/PartitionsField.jsx:488 +#: src/components/storage/ProposalResultTable.jsx:133 +#: src/components/storage/SpaceActionsTable.jsx:202 +#: src/components/storage/VolumeFields.jsx:488 +#: src/components/storage/VolumeLocationSelectorTable.jsx:113 msgid "Size" msgstr "" -#: src/components/storage/DevicesTechMenu.jsx:44 +#: src/components/storage/DevicesTechMenu.jsx:38 msgid "Manage and format" msgstr "" -#: src/components/storage/DevicesTechMenu.jsx:62 +#: src/components/storage/DevicesTechMenu.jsx:52 msgid "Activate disks" msgstr "" -#: src/components/storage/DevicesTechMenu.jsx:64 +#: src/components/storage/DevicesTechMenu.jsx:53 msgid "zFCP" msgstr "" -#: src/components/storage/DevicesTechMenu.jsx:80 +#: src/components/storage/DevicesTechMenu.jsx:66 msgid "Connect to iSCSI targets" msgstr "" -#: src/components/storage/DevicesTechMenu.jsx:82 +#: src/components/storage/DevicesTechMenu.jsx:67 #: src/components/storage/routes.js:37 msgid "iSCSI" msgstr "" @@ -1263,59 +1226,57 @@ msgstr "" msgid "Encryption" msgstr "" -#: src/components/storage/EncryptionField.jsx:39 +#: src/components/storage/EncryptionField.jsx:40 msgid "" "Protection for the information stored at the device, including data, " "programs, and system files." msgstr "" -#: src/components/storage/EncryptionField.jsx:42 +#: src/components/storage/EncryptionField.jsx:44 msgid "disabled" msgstr "" -#: src/components/storage/EncryptionField.jsx:43 +#: src/components/storage/EncryptionField.jsx:45 msgid "enabled" msgstr "" -#: src/components/storage/EncryptionField.jsx:44 +#: src/components/storage/EncryptionField.jsx:46 msgid "using TPM unlocking" msgstr "" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Enable" msgstr "" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Modify" msgstr "" -#: src/components/storage/EncryptionSettingsDialog.jsx:37 +#: src/components/storage/EncryptionSettingsDialog.jsx:38 msgid "" "Full Disk Encryption (FDE) allows to protect the information stored at the " "device, including data, programs, and system files." msgstr "" #. TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation -#: src/components/storage/EncryptionSettingsDialog.jsx:40 +#: src/components/storage/EncryptionSettingsDialog.jsx:42 msgid "" "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot" msgstr "" -#. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing -#. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. -#: src/components/storage/EncryptionSettingsDialog.jsx:43 +#: src/components/storage/EncryptionSettingsDialog.jsx:46 msgid "" "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." msgstr "" -#: src/components/storage/EncryptionSettingsDialog.jsx:114 +#: src/components/storage/EncryptionSettingsDialog.jsx:129 msgid "Encrypt the system" msgstr "" #: src/components/storage/InstallationDeviceField.jsx:36 -#: src/components/storage/VolumeLocationSelectorTable.jsx:58 +#: src/components/storage/VolumeLocationSelectorTable.jsx:61 msgid "Installation device" msgstr "" @@ -1334,71 +1295,70 @@ msgstr "" msgid "File systems created at a new LVM volume group" msgstr "" -#. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) -#: src/components/storage/InstallationDeviceField.jsx:59 +#: src/components/storage/InstallationDeviceField.jsx:60 #, c-format msgid "File systems created at a new LVM volume group on %s" msgstr "" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:73 +#: src/components/storage/PartitionsField.jsx:84 #, c-format msgid "at least %s" msgstr "" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:97 +#: src/components/storage/PartitionsField.jsx:108 #, c-format msgid "Transactional Btrfs root volume (%s)" msgstr "" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:99 +#: src/components/storage/PartitionsField.jsx:110 #, c-format msgid "Transactional Btrfs root partition (%s)" msgstr "" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:104 +#: src/components/storage/PartitionsField.jsx:115 #, c-format msgid "Btrfs root volume with snapshots (%s)" msgstr "" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:106 +#: src/components/storage/PartitionsField.jsx:117 #, c-format msgid "Btrfs root partition with snapshots (%s)" msgstr "" #. TRANSLATORS: This results in something like "Mount /dev/sda3 at /home (25 GiB)" since #. %1$s is replaced by the device name, %2$s by the mount point and %3$s by the size -#: src/components/storage/PartitionsField.jsx:115 +#: src/components/storage/PartitionsField.jsx:126 #, c-format msgid "Mount %1$s at %2$s (%3$s)" msgstr "" #. TRANSLATORS: This results in something like "Swap at /dev/sda3 (2 GiB)" since #. %1$s is replaced by the device name, and %2$s by the size -#: src/components/storage/PartitionsField.jsx:121 +#: src/components/storage/PartitionsField.jsx:132 #, c-format msgid "Swap at %1$s (%2$s)" msgstr "" #. TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:125 +#: src/components/storage/PartitionsField.jsx:136 #, c-format msgid "Swap volume (%s)" msgstr "" #. TRANSLATORS: %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:127 +#: src/components/storage/PartitionsField.jsx:138 #, c-format msgid "Swap partition (%s)" msgstr "" #. TRANSLATORS: This results in something like "Btrfs root at /dev/sda3 (20 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size -#: src/components/storage/PartitionsField.jsx:136 +#: src/components/storage/PartitionsField.jsx:147 #, c-format msgid "%1$s root at %2$s (%3$s)" msgstr "" @@ -1406,21 +1366,21 @@ msgstr "" #. TRANSLATORS: "/" is in an LVM logical volume. #. Results in something like "Btrfs root volume (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:142 +#: src/components/storage/PartitionsField.jsx:153 #, c-format msgid "%1$s root volume (%2$s)" msgstr "" #. TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:145 +#: src/components/storage/PartitionsField.jsx:156 #, c-format msgid "%1$s root partition (%2$s)" msgstr "" #. TRANSLATORS: This results in something like "Ext4 /home at /dev/sda3 (20 GiB)" since #. %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size -#: src/components/storage/PartitionsField.jsx:151 +#: src/components/storage/PartitionsField.jsx:162 #, c-format msgid "%1$s %2$s at %3$s (%4$s)" msgstr "" @@ -1428,154 +1388,149 @@ msgstr "" #. TRANSLATORS: The filesystem is in an LVM logical volume. #. Results in something like "Ext4 /home volume (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:157 +#: src/components/storage/PartitionsField.jsx:168 #, c-format msgid "%1$s %2$s volume (%3$s)" msgstr "" #. TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:160 +#: src/components/storage/PartitionsField.jsx:171 #, c-format msgid "%1$s %2$s partition (%3$s)" msgstr "" -#: src/components/storage/PartitionsField.jsx:172 +#: src/components/storage/PartitionsField.jsx:182 msgid "Do not configure partitions for booting" msgstr "" -#: src/components/storage/PartitionsField.jsx:175 +#: src/components/storage/PartitionsField.jsx:184 msgid "Boot partitions at installation disk" msgstr "" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/PartitionsField.jsx:178 +#: src/components/storage/PartitionsField.jsx:187 #, c-format msgid "Boot partitions at %s" msgstr "" #. TRANSLATORS: header for a list of items referring to size limits for file systems -#: src/components/storage/PartitionsField.jsx:200 +#: src/components/storage/PartitionsField.jsx:209 msgid "These limits are affected by:" msgstr "" #. TRANSLATORS: list item, this affects the computed partition size limits -#: src/components/storage/PartitionsField.jsx:204 +#: src/components/storage/PartitionsField.jsx:213 msgid "The configuration of snapshots" msgstr "" -#. TRANSLATORS: list item, this affects the computed partition size limits -#. %s is replaced by a list of the volumes (like "/home, /boot") -#: src/components/storage/PartitionsField.jsx:208 +#: src/components/storage/PartitionsField.jsx:219 #, c-format msgid "Presence of other volumes (%s)" msgstr "" #. TRANSLATORS: list item, describes a factor that affects the computed size of a #. file system; eg. adjusting the size of the swap -#: src/components/storage/PartitionsField.jsx:212 +#: src/components/storage/PartitionsField.jsx:225 msgid "The amount of RAM in the system" msgstr "" -#. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/PartitionsField.jsx:263 +#: src/components/storage/PartitionsField.jsx:292 msgid "auto" msgstr "" #. TRANSLATORS: %s will be replaced by a file-system type like "Btrfs" or "Ext4" -#: src/components/storage/PartitionsField.jsx:279 +#: src/components/storage/PartitionsField.jsx:309 #, c-format msgid "Reused %s" msgstr "" -#: src/components/storage/PartitionsField.jsx:281 +#: src/components/storage/PartitionsField.jsx:310 msgid "Transactional Btrfs" msgstr "" -#: src/components/storage/PartitionsField.jsx:283 +#: src/components/storage/PartitionsField.jsx:311 msgid "Btrfs with snapshots" msgstr "" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:297 +#: src/components/storage/PartitionsField.jsx:325 #, c-format msgid "Partition at %s" msgstr "" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:300 +#: src/components/storage/PartitionsField.jsx:328 #, c-format msgid "Separate LVM at %s" msgstr "" -#: src/components/storage/PartitionsField.jsx:304 +#: src/components/storage/PartitionsField.jsx:331 msgid "Logical volume at system LVM" msgstr "" -#: src/components/storage/PartitionsField.jsx:306 +#: src/components/storage/PartitionsField.jsx:333 msgid "Partition at installation disk" msgstr "" -#: src/components/storage/PartitionsField.jsx:321 +#: src/components/storage/PartitionsField.jsx:348 msgid "Reset location" msgstr "" -#: src/components/storage/PartitionsField.jsx:322 +#: src/components/storage/PartitionsField.jsx:349 msgid "Change location" msgstr "" -#: src/components/storage/PartitionsField.jsx:323 -#: src/components/storage/SpaceActionsTable.jsx:78 +#: src/components/storage/PartitionsField.jsx:350 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "" -#: src/components/storage/PartitionsField.jsx:449 -#: src/components/storage/VolumeFields.jsx:61 -#: src/components/storage/VolumeFields.jsx:70 +#: src/components/storage/PartitionsField.jsx:486 +#: src/components/storage/VolumeFields.jsx:66 #: src/components/storage/VolumeFields.jsx:75 +#: src/components/storage/VolumeFields.jsx:80 msgid "Mount point" msgstr "" #. TRANSLATORS: where (and how) the file-system is going to be created -#: src/components/storage/PartitionsField.jsx:453 +#: src/components/storage/PartitionsField.jsx:490 msgid "Location" msgstr "" -#: src/components/storage/PartitionsField.jsx:495 +#: src/components/storage/PartitionsField.jsx:532 msgid "Table with mount points" msgstr "" -#: src/components/storage/PartitionsField.jsx:566 -#: src/components/storage/PartitionsField.jsx:585 -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/PartitionsField.jsx:604 +#: src/components/storage/PartitionsField.jsx:624 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Add file system" msgstr "" -#: src/components/storage/PartitionsField.jsx:596 +#: src/components/storage/PartitionsField.jsx:636 msgid "Other" msgstr "" -#: src/components/storage/PartitionsField.jsx:731 +#: src/components/storage/PartitionsField.jsx:777 msgid "Reset to defaults" msgstr "" -#: src/components/storage/PartitionsField.jsx:801 +#: src/components/storage/PartitionsField.jsx:849 msgid "Partitions and file systems" msgstr "" -#: src/components/storage/PartitionsField.jsx:802 +#: src/components/storage/PartitionsField.jsx:851 msgid "" "Structure of the new system, including any additional partition needed for " "booting" msgstr "" -#: src/components/storage/PartitionsField.jsx:808 +#: src/components/storage/PartitionsField.jsx:858 msgid "Show partitions and file-systems actions" msgstr "" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:62 +#: src/components/storage/ProposalActionsDialog.jsx:65 #, c-format msgid "Hide %d subvolume action" msgid_plural "Hide %d subvolume actions" @@ -1583,8 +1538,7 @@ msgstr[0] "" msgstr[1] "" msgstr[2] "" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:64 +#: src/components/storage/ProposalActionsDialog.jsx:70 #, c-format msgid "Show %d subvolume action" msgid_plural "Show %d subvolume actions" @@ -1600,32 +1554,24 @@ msgstr "" msgid "Destructive actions are allowed" msgstr "" -#: src/components/storage/ProposalActionsSummary.jsx:66 -#, c-format -msgid "There is %d destructive action planned" -msgid_plural "There are %d destructive actions planned" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" - -#: src/components/storage/ProposalActionsSummary.jsx:79 -#: src/components/storage/ProposalActionsSummary.jsx:126 +#: src/components/storage/ProposalActionsSummary.jsx:82 +#: src/components/storage/ProposalActionsSummary.jsx:132 msgid "affecting" msgstr "" -#: src/components/storage/ProposalActionsSummary.jsx:107 +#: src/components/storage/ProposalActionsSummary.jsx:112 msgid "Shrinking partitions is not allowed" msgstr "" -#: src/components/storage/ProposalActionsSummary.jsx:111 +#: src/components/storage/ProposalActionsSummary.jsx:116 msgid "Shrinking partitions is allowed" msgstr "" -#: src/components/storage/ProposalActionsSummary.jsx:113 +#: src/components/storage/ProposalActionsSummary.jsx:118 msgid "Shrinking some partitions is allowed but not needed" msgstr "" -#: src/components/storage/ProposalActionsSummary.jsx:116 +#: src/components/storage/ProposalActionsSummary.jsx:121 #, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" @@ -1633,11 +1579,11 @@ msgstr[0] "" msgstr[1] "" msgstr[2] "" -#: src/components/storage/ProposalActionsSummary.jsx:151 +#: src/components/storage/ProposalActionsSummary.jsx:159 msgid "Cannot accommodate the required file systems for installation" msgstr "" -#: src/components/storage/ProposalActionsSummary.jsx:160 +#: src/components/storage/ProposalActionsSummary.jsx:167 #, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" @@ -1645,42 +1591,42 @@ msgstr[0] "" msgstr[1] "" msgstr[2] "" -#: src/components/storage/ProposalActionsSummary.jsx:179 +#: src/components/storage/ProposalActionsSummary.jsx:182 msgid "Waiting for actions information..." msgstr "" -#: src/components/storage/ProposalPage.jsx:314 +#: src/components/storage/ProposalPage.jsx:329 msgid "Planned Actions" msgstr "" -#: src/components/storage/ProposalResultSection.jsx:42 +#: src/components/storage/ProposalResultSection.jsx:43 msgid "Waiting for information about storage configuration" msgstr "" -#: src/components/storage/ProposalResultSection.jsx:70 +#: src/components/storage/ProposalResultSection.jsx:73 msgid "Final layout" msgstr "" -#: src/components/storage/ProposalResultSection.jsx:71 +#: src/components/storage/ProposalResultSection.jsx:74 msgid "The systems will be configured as displayed below." msgstr "" -#: src/components/storage/ProposalResultSection.jsx:78 +#: src/components/storage/ProposalResultSection.jsx:83 msgid "Storage proposal not possible" msgstr "" -#: src/components/storage/ProposalResultTable.jsx:74 +#: src/components/storage/ProposalResultTable.jsx:79 msgid "New" msgstr "" #. TRANSLATORS: Label to indicate the device size before resizing, where %s is #. replaced by the original size (e.g., 3.00 GiB). -#: src/components/storage/ProposalResultTable.jsx:98 +#: src/components/storage/ProposalResultTable.jsx:105 #, c-format msgid "Before %s" msgstr "" -#: src/components/storage/ProposalResultTable.jsx:121 +#: src/components/storage/ProposalResultTable.jsx:131 msgid "Mount Point" msgstr "" @@ -1688,7 +1634,7 @@ msgstr "" msgid "Transactional root file system" msgstr "" -#: src/components/storage/ProposalTransactionalInfo.jsx:48 +#: src/components/storage/ProposalTransactionalInfo.jsx:49 #, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " @@ -1699,190 +1645,179 @@ msgstr "" msgid "Use Btrfs snapshots for the root file system" msgstr "" -#: src/components/storage/SnapshotsField.jsx:37 +#: src/components/storage/SnapshotsField.jsx:38 msgid "" "Allows to boot to a previous version of the system after configuration " "changes or software upgrades." msgstr "" -#. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) -#: src/components/storage/SpaceActionsTable.jsx:75 +#: src/components/storage/SpaceActionsTable.jsx:68 #, c-format -msgid "Space action selector for %s" +msgid "Up to %s can be recovered by shrinking the device." msgstr "" -#: src/components/storage/SpaceActionsTable.jsx:79 -msgid "Allow resize" +#: src/components/storage/SpaceActionsTable.jsx:77 +msgid "The device cannot be shrunk:" msgstr "" -#: src/components/storage/SpaceActionsTable.jsx:80 -msgid "Do not modify" +#: src/components/storage/SpaceActionsTable.jsx:98 +#, c-format +msgid "Show information about %s" msgstr "" -#: src/components/storage/SpaceActionsTable.jsx:111 +#: src/components/storage/SpaceActionsTable.jsx:172 msgid "The content may be deleted" msgstr "" -#: src/components/storage/SpaceActionsTable.jsx:144 -msgid "Shrinkable" -msgstr "" - -#: src/components/storage/SpaceActionsTable.jsx:146 +#: src/components/storage/SpaceActionsTable.jsx:204 msgid "Action" msgstr "" -#: src/components/storage/SpaceActionsTable.jsx:162 +#: src/components/storage/SpaceActionsTable.jsx:215 msgid "Actions to find space" msgstr "" -#: src/components/storage/SpacePolicySelection.jsx:170 +#: src/components/storage/SpacePolicySelection.jsx:172 msgid "Space policy" msgstr "" -#: src/components/storage/VolumeDialog.jsx:78 +#: src/components/storage/VolumeDialog.jsx:83 #, c-format msgid "Add %s file system" msgstr "" -#: src/components/storage/VolumeDialog.jsx:79 +#: src/components/storage/VolumeDialog.jsx:84 #, c-format msgid "Edit %s file system" msgstr "" -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Edit file system" msgstr "" #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:96 +#: src/components/storage/VolumeDialog.jsx:101 msgid "The type and size of the file system cannot be edited." msgstr "" -#. TRANSLATORS: Description of a warning. The first %s is replaced by a device name (e.g., -#. /dev/vda) and the second %s is replaced by a mount path (e.g., /home). -#: src/components/storage/VolumeDialog.jsx:99 +#: src/components/storage/VolumeDialog.jsx:105 #, c-format msgid "The current file system on %s is selected to be mounted at %s." msgstr "" #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:105 +#: src/components/storage/VolumeDialog.jsx:113 msgid "The size of the file system cannot be edited" msgstr "" #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). -#: src/components/storage/VolumeDialog.jsx:107 +#: src/components/storage/VolumeDialog.jsx:115 #, c-format msgid "The file system is allocated at the device %s." msgstr "" -#: src/components/storage/VolumeDialog.jsx:152 +#: src/components/storage/VolumeDialog.jsx:163 msgid "A mount point is required" msgstr "" -#: src/components/storage/VolumeDialog.jsx:179 +#: src/components/storage/VolumeDialog.jsx:190 msgid "The mount point is invalid" msgstr "" -#: src/components/storage/VolumeDialog.jsx:207 +#: src/components/storage/VolumeDialog.jsx:218 msgid "A size value is required" msgstr "" -#: src/components/storage/VolumeDialog.jsx:235 +#: src/components/storage/VolumeDialog.jsx:246 msgid "Minimum size is required" msgstr "" -#: src/components/storage/VolumeDialog.jsx:267 +#: src/components/storage/VolumeDialog.jsx:278 msgid "Maximum must be greater than minimum" msgstr "" -#: src/components/storage/VolumeDialog.jsx:309 +#: src/components/storage/VolumeDialog.jsx:320 #, c-format msgid "There is already a file system for %s." msgstr "" -#: src/components/storage/VolumeDialog.jsx:311 +#: src/components/storage/VolumeDialog.jsx:322 msgid "Do you want to edit it?" msgstr "" -#: src/components/storage/VolumeDialog.jsx:356 +#: src/components/storage/VolumeDialog.jsx:367 #, c-format msgid "There is a predefined file system for %s." msgstr "" -#: src/components/storage/VolumeDialog.jsx:358 +#: src/components/storage/VolumeDialog.jsx:369 msgid "Do you want to add it?" msgstr "" -#. TRANSLATORS: info about possible file system types. -#: src/components/storage/VolumeFields.jsx:217 +#: src/components/storage/VolumeFields.jsx:225 msgid "" "The options for the file system type depends on the product and the mount " "point." msgstr "" -#: src/components/storage/VolumeFields.jsx:223 +#: src/components/storage/VolumeFields.jsx:232 msgid "More info for file system types" msgstr "" #. TRANSLATORS: label for the file system selector. -#: src/components/storage/VolumeFields.jsx:234 +#: src/components/storage/VolumeFields.jsx:243 msgid "File system type" msgstr "" #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:265 +#: src/components/storage/VolumeFields.jsx:274 msgid "the configuration of snapshots" msgstr "" -#. TRANSLATORS: item which affects the final computed partition size -#. %s is replaced by a list of mount points like "/home, /boot" -#: src/components/storage/VolumeFields.jsx:270 +#: src/components/storage/VolumeFields.jsx:281 #, c-format msgid "the presence of the file system for %s" msgstr "" #. TRANSLATORS: conjunction for merging two list items -#: src/components/storage/VolumeFields.jsx:272 +#: src/components/storage/VolumeFields.jsx:283 msgid ", " msgstr "" #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:276 +#: src/components/storage/VolumeFields.jsx:289 msgid "the amount of RAM in the system" msgstr "" -#. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeFields.jsx:279 +#: src/components/storage/VolumeFields.jsx:293 #, c-format msgid "The final size depends on %s." msgstr "" #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeFields.jsx:281 +#: src/components/storage/VolumeFields.jsx:295 msgid " and " msgstr "" -#. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeFields.jsx:286 +#: src/components/storage/VolumeFields.jsx:302 msgid "Automatically calculated size according to the selected product." msgstr "" -#: src/components/storage/VolumeFields.jsx:305 +#: src/components/storage/VolumeFields.jsx:321 msgid "Exact size for the file system." msgstr "" #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeFields.jsx:318 +#: src/components/storage/VolumeFields.jsx:330 msgid "Exact size" msgstr "" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeFields.jsx:335 +#: src/components/storage/VolumeFields.jsx:347 msgid "Size unit" msgstr "" -#: src/components/storage/VolumeFields.jsx:363 +#: src/components/storage/VolumeFields.jsx:376 msgid "" "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 " @@ -1890,50 +1825,49 @@ msgid "" msgstr "" #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeFields.jsx:370 +#: src/components/storage/VolumeFields.jsx:384 msgid "Minimum" msgstr "" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeFields.jsx:381 +#: src/components/storage/VolumeFields.jsx:395 msgid "Minimum desired size" msgstr "" -#: src/components/storage/VolumeFields.jsx:392 +#: src/components/storage/VolumeFields.jsx:406 msgid "Unit for the minimum size" msgstr "" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:404 +#: src/components/storage/VolumeFields.jsx:418 msgid "Maximum" msgstr "" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:416 +#: src/components/storage/VolumeFields.jsx:430 msgid "Maximum desired size" msgstr "" -#: src/components/storage/VolumeFields.jsx:426 +#: src/components/storage/VolumeFields.jsx:440 msgid "Unit for the maximum size" msgstr "" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeFields.jsx:444 +#: src/components/storage/VolumeFields.jsx:458 msgid "Auto" msgstr "" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeFields.jsx:446 +#: src/components/storage/VolumeFields.jsx:460 msgid "Fixed" msgstr "" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeFields.jsx:448 +#: src/components/storage/VolumeFields.jsx:462 msgid "Range" msgstr "" -#. TRANSLATORS: Description of the dialog for changing the location of a file system. -#: src/components/storage/VolumeLocationDialog.jsx:40 +#: src/components/storage/VolumeLocationDialog.jsx:41 msgid "" "The file systems are allocated at the installation device by default. " "Indicate a custom location to create the file system at a specific device." @@ -1941,153 +1875,147 @@ msgstr "" #. TRANSLATORS: Title of the dialog for changing the location of a file system. %s is replaced #. by a mount path (e.g., /home). -#: src/components/storage/VolumeLocationDialog.jsx:135 +#: src/components/storage/VolumeLocationDialog.jsx:137 #, c-format msgid "Location for %s file system" msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:145 +#: src/components/storage/VolumeLocationDialog.jsx:147 msgid "Select in which device to allocate the file system" msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:148 +#: src/components/storage/VolumeLocationDialog.jsx:150 msgid "Select a location" msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:160 +#: src/components/storage/VolumeLocationDialog.jsx:162 msgid "Select how to allocate the file system" msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:165 +#: src/components/storage/VolumeLocationDialog.jsx:167 msgid "Create a new partition" msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:166 +#: src/components/storage/VolumeLocationDialog.jsx:169 msgid "" "The file system will be allocated as a new partition at the selected disk." msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:175 +#: src/components/storage/VolumeLocationDialog.jsx:179 msgid "Create a dedicated LVM volume group" msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:176 +#: src/components/storage/VolumeLocationDialog.jsx:181 msgid "" "A new volume group will be allocated in the selected disk and the file " "system will be created as a logical volume." msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:185 +#: src/components/storage/VolumeLocationDialog.jsx:191 msgid "Format the device" msgstr "" -#. TRANSLATORS: %s is replaced by a file system type (e.g., Ext4). -#: src/components/storage/VolumeLocationDialog.jsx:188 +#: src/components/storage/VolumeLocationDialog.jsx:195 #, c-format msgid "The selected device will be formatted as %s file system." msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:198 +#: src/components/storage/VolumeLocationDialog.jsx:206 msgid "Mount the file system" msgstr "" -#: src/components/storage/VolumeLocationDialog.jsx:199 +#: src/components/storage/VolumeLocationDialog.jsx:208 msgid "" "The current file system on the selected device will be mounted without " "formatting the device." msgstr "" -#: src/components/storage/VolumeLocationSelectorTable.jsx:102 +#: src/components/storage/VolumeLocationSelectorTable.jsx:110 msgid "Usage" msgstr "" -#: src/components/storage/ZFCPDiskForm.jsx:109 +#: src/components/storage/ZFCPDiskForm.jsx:106 msgid "The zFCP disk was not activated." msgstr "" #. TRANSLATORS: abbrev. World Wide Port Name #: src/components/storage/ZFCPDiskForm.jsx:123 -#: src/components/storage/ZFCPPage.jsx:363 +#: src/components/storage/ZFCPPage.jsx:383 msgid "WWPN" msgstr "" #. TRANSLATORS: abbrev. Logical Unit Number -#: src/components/storage/ZFCPDiskForm.jsx:134 -#: src/components/storage/ZFCPPage.jsx:364 +#: src/components/storage/ZFCPDiskForm.jsx:131 +#: src/components/storage/ZFCPPage.jsx:384 msgid "LUN" msgstr "" -#: src/components/storage/ZFCPPage.jsx:304 +#: src/components/storage/ZFCPPage.jsx:326 msgid "Auto LUNs Scan" msgstr "" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Activated" msgstr "" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Deactivated" msgstr "" -#: src/components/storage/ZFCPPage.jsx:418 +#: src/components/storage/ZFCPPage.jsx:437 msgid "No zFCP controllers found." msgstr "" -#: src/components/storage/ZFCPPage.jsx:419 +#: src/components/storage/ZFCPPage.jsx:438 msgid "Please, try to read the zFCP devices again." msgstr "" -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:421 +#: src/components/storage/ZFCPPage.jsx:441 msgid "Read zFCP devices" msgstr "" -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:430 +#: src/components/storage/ZFCPPage.jsx:452 msgid "" "Automatic LUN scan is [enabled]. Activating a controller which is running in " "NPIV mode will automatically configures all its LUNs." msgstr "" -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:433 +#: src/components/storage/ZFCPPage.jsx:457 msgid "" "Automatic LUN scan is [disabled]. LUNs have to be manually configured after " "activating a controller." msgstr "" -#: src/components/storage/ZFCPPage.jsx:490 +#: src/components/storage/ZFCPPage.jsx:519 msgid "Activate a zFCP disk" msgstr "" -#: src/components/storage/ZFCPPage.jsx:529 +#: src/components/storage/ZFCPPage.jsx:553 msgid "Please, try to activate a zFCP controller." msgstr "" -#: src/components/storage/ZFCPPage.jsx:536 +#: src/components/storage/ZFCPPage.jsx:559 msgid "Please, try to activate a zFCP disk." msgstr "" -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:538 +#: src/components/storage/ZFCPPage.jsx:562 msgid "Activate zFCP disk" msgstr "" -#: src/components/storage/ZFCPPage.jsx:545 +#: src/components/storage/ZFCPPage.jsx:570 msgid "No zFCP disks found." msgstr "" -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:560 +#: src/components/storage/ZFCPPage.jsx:586 msgid "Activate new disk" msgstr "" #. TRANSLATORS: section title -#: src/components/storage/ZFCPPage.jsx:572 +#: src/components/storage/ZFCPPage.jsx:599 msgid "Disks" msgstr "" -#: src/components/storage/device-utils.jsx:88 +#: src/components/storage/device-utils.jsx:92 msgid "Unused space" msgstr "" @@ -2099,70 +2027,70 @@ msgstr "" msgid "Authentication by target" msgstr "" -#: src/components/storage/iscsi/AuthFields.jsx:80 -#: src/components/storage/iscsi/AuthFields.jsx:85 -#: src/components/storage/iscsi/AuthFields.jsx:87 -#: src/components/storage/iscsi/AuthFields.jsx:112 -#: src/components/storage/iscsi/AuthFields.jsx:117 -#: src/components/storage/iscsi/AuthFields.jsx:119 +#: src/components/storage/iscsi/AuthFields.jsx:78 +#: src/components/storage/iscsi/AuthFields.jsx:82 +#: src/components/storage/iscsi/AuthFields.jsx:84 +#: src/components/storage/iscsi/AuthFields.jsx:104 +#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:110 msgid "User name" msgstr "" -#: src/components/storage/iscsi/AuthFields.jsx:91 -#: src/components/storage/iscsi/AuthFields.jsx:124 +#: src/components/storage/iscsi/AuthFields.jsx:88 +#: src/components/storage/iscsi/AuthFields.jsx:116 msgid "Incorrect user name" msgstr "" -#: src/components/storage/iscsi/AuthFields.jsx:105 -#: src/components/storage/iscsi/AuthFields.jsx:139 +#: src/components/storage/iscsi/AuthFields.jsx:99 +#: src/components/storage/iscsi/AuthFields.jsx:130 msgid "Incorrect password" msgstr "" -#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:102 msgid "Authentication by initiator" msgstr "" -#: src/components/storage/iscsi/AuthFields.jsx:133 +#: src/components/storage/iscsi/AuthFields.jsx:123 msgid "Target Password" msgstr "" #. TRANSLATORS: popup title -#: src/components/storage/iscsi/DiscoverForm.jsx:102 +#: src/components/storage/iscsi/DiscoverForm.jsx:94 msgid "Discover iSCSI Targets" msgstr "" -#: src/components/storage/iscsi/DiscoverForm.jsx:112 -#: src/components/storage/iscsi/LoginForm.jsx:73 +#: src/components/storage/iscsi/DiscoverForm.jsx:99 +#: src/components/storage/iscsi/LoginForm.jsx:70 msgid "Make sure you provide the correct values" msgstr "" -#: src/components/storage/iscsi/DiscoverForm.jsx:118 +#: src/components/storage/iscsi/DiscoverForm.jsx:103 msgid "IP address" msgstr "" #. TRANSLATORS: network address -#: src/components/storage/iscsi/DiscoverForm.jsx:125 -#: src/components/storage/iscsi/DiscoverForm.jsx:127 +#: src/components/storage/iscsi/DiscoverForm.jsx:108 +#: src/components/storage/iscsi/DiscoverForm.jsx:110 msgid "Address" msgstr "" -#: src/components/storage/iscsi/DiscoverForm.jsx:132 +#: src/components/storage/iscsi/DiscoverForm.jsx:115 msgid "Incorrect IP address" msgstr "" #. TRANSLATORS: network port number -#: src/components/storage/iscsi/DiscoverForm.jsx:136 -#: src/components/storage/iscsi/DiscoverForm.jsx:143 -#: src/components/storage/iscsi/DiscoverForm.jsx:145 +#: src/components/storage/iscsi/DiscoverForm.jsx:117 +#: src/components/storage/iscsi/DiscoverForm.jsx:122 +#: src/components/storage/iscsi/DiscoverForm.jsx:124 msgid "Port" msgstr "" -#: src/components/storage/iscsi/DiscoverForm.jsx:150 +#: src/components/storage/iscsi/DiscoverForm.jsx:129 msgid "Incorrect port" msgstr "" #. TRANSLATORS: %s is replaced by the iSCSI target node name -#: src/components/storage/iscsi/EditNodeForm.jsx:50 +#: src/components/storage/iscsi/EditNodeForm.jsx:48 #, c-format msgid "Edit %s" msgstr "" @@ -2180,8 +2108,8 @@ msgstr "" #. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 #: src/components/storage/iscsi/InitiatorPresenter.jsx:86 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 -#: src/components/storage/iscsi/NodesPresenter.jsx:124 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 +#: src/components/storage/iscsi/NodesPresenter.jsx:122 msgid "iBFT" msgstr "" @@ -2196,14 +2124,14 @@ msgid "Initiator" msgstr "" #. TRANSLATORS: %s is replaced by the iSCSI target name -#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/storage/iscsi/LoginForm.jsx:66 #, c-format msgid "Login %s" msgstr "" #. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) -#: src/components/storage/iscsi/LoginForm.jsx:76 -#: src/components/storage/iscsi/LoginForm.jsx:79 +#: src/components/storage/iscsi/LoginForm.jsx:74 +#: src/components/storage/iscsi/LoginForm.jsx:77 msgid "Startup" msgstr "" @@ -2225,27 +2153,26 @@ msgstr "" msgid "Logout" msgstr "" -#: src/components/storage/iscsi/NodesPresenter.jsx:101 -#: src/components/storage/iscsi/NodesPresenter.jsx:122 +#: src/components/storage/iscsi/NodesPresenter.jsx:99 +#: src/components/storage/iscsi/NodesPresenter.jsx:120 msgid "Portal" msgstr "" -#: src/components/storage/iscsi/NodesPresenter.jsx:102 -#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/storage/iscsi/NodesPresenter.jsx:100 +#: src/components/storage/iscsi/NodesPresenter.jsx:121 msgid "Interface" msgstr "" -#: src/components/storage/iscsi/TargetsSection.jsx:142 +#: src/components/storage/iscsi/TargetsSection.jsx:138 msgid "No iSCSI targets found." msgstr "" -#: src/components/storage/iscsi/TargetsSection.jsx:143 +#: src/components/storage/iscsi/TargetsSection.jsx:140 msgid "" "Please, perform an iSCSI discovery in order to find available iSCSI targets." msgstr "" -#. TRANSLATORS: button label, starts iSCSI discovery -#: src/components/storage/iscsi/TargetsSection.jsx:145 +#: src/components/storage/iscsi/TargetsSection.jsx:144 msgid "Discover iSCSI targets" msgstr "" @@ -2255,7 +2182,7 @@ msgid "Discover" msgstr "" #. TRANSLATORS: iSCSI targets section title -#: src/components/storage/iscsi/TargetsSection.jsx:170 +#: src/components/storage/iscsi/TargetsSection.jsx:167 msgid "Targets" msgstr "" @@ -2344,74 +2271,74 @@ msgstr "" msgid "No user defined yet." msgstr "" -#: src/components/users/FirstUser.jsx:38 +#: src/components/users/FirstUser.jsx:39 msgid "" "Please, be aware that a user must be defined before installing the system to " "be able to log into it." msgstr "" -#: src/components/users/FirstUser.jsx:42 +#: src/components/users/FirstUser.jsx:45 msgid "Define a user now" msgstr "" -#: src/components/users/FirstUser.jsx:54 -#: src/components/users/FirstUserForm.jsx:210 +#: src/components/users/FirstUser.jsx:58 +#: src/components/users/FirstUserForm.jsx:227 msgid "Full name" msgstr "" -#: src/components/users/FirstUser.jsx:55 -#: src/components/users/FirstUserForm.jsx:224 -#: src/components/users/FirstUserForm.jsx:229 -#: src/components/users/FirstUserForm.jsx:232 +#: src/components/users/FirstUser.jsx:59 +#: src/components/users/FirstUserForm.jsx:241 +#: src/components/users/FirstUserForm.jsx:246 +#: src/components/users/FirstUserForm.jsx:249 msgid "Username" msgstr "" -#: src/components/users/FirstUser.jsx:120 -#: src/components/users/RootAuthMethods.jsx:99 -#: src/components/users/RootAuthMethods.jsx:111 +#: src/components/users/FirstUser.jsx:124 +#: src/components/users/RootAuthMethods.jsx:104 +#: src/components/users/RootAuthMethods.jsx:116 msgid "Discard" msgstr "" -#: src/components/users/FirstUserForm.jsx:46 +#: src/components/users/FirstUserForm.jsx:57 msgid "Username suggestion dropdown" msgstr "" #. TRANSLATORS: dropdown username suggestions -#: src/components/users/FirstUserForm.jsx:61 +#: src/components/users/FirstUserForm.jsx:72 msgid "Use suggested username" msgstr "" -#: src/components/users/FirstUserForm.jsx:140 +#: src/components/users/FirstUserForm.jsx:151 msgid "All fields are required" msgstr "" -#: src/components/users/FirstUserForm.jsx:147 +#: src/components/users/FirstUserForm.jsx:158 msgid "Please, try again." msgstr "" -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Create user" msgstr "" -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Edit user" msgstr "" -#: src/components/users/FirstUserForm.jsx:214 -#: src/components/users/FirstUserForm.jsx:216 +#: src/components/users/FirstUserForm.jsx:231 +#: src/components/users/FirstUserForm.jsx:233 msgid "User full name" msgstr "" -#: src/components/users/FirstUserForm.jsx:254 +#: src/components/users/FirstUserForm.jsx:271 msgid "Edit password too" msgstr "" -#: src/components/users/FirstUserForm.jsx:269 +#: src/components/users/FirstUserForm.jsx:287 msgid "user autologin" msgstr "" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:273 +#: src/components/users/FirstUserForm.jsx:291 msgid "Auto-login" msgstr "" @@ -2419,62 +2346,60 @@ msgstr "" msgid "No root authentication method defined yet." msgstr "" -#: src/components/users/RootAuthMethods.jsx:38 +#: src/components/users/RootAuthMethods.jsx:39 msgid "" "Please, define at least one authentication method for logging into the " "system as root." msgstr "" -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:43 +#: src/components/users/RootAuthMethods.jsx:46 msgid "Set a password" msgstr "" -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:45 +#: src/components/users/RootAuthMethods.jsx:50 msgid "Upload a SSH Public Key" msgstr "" -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Set" msgstr "" -#: src/components/users/RootAuthMethods.jsx:129 +#: src/components/users/RootAuthMethods.jsx:132 msgid "Already set" msgstr "" -#: src/components/users/RootAuthMethods.jsx:130 -#: src/components/users/RootAuthMethods.jsx:134 +#: src/components/users/RootAuthMethods.jsx:132 +#: src/components/users/RootAuthMethods.jsx:136 msgid "Not set" msgstr "" #. TRANSLATORS: table header, user authentication method -#: src/components/users/RootAuthMethods.jsx:155 +#: src/components/users/RootAuthMethods.jsx:157 msgid "Method" msgstr "" -#: src/components/users/RootAuthMethods.jsx:170 +#: src/components/users/RootAuthMethods.jsx:174 msgid "SSH Key" msgstr "" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Change the root password" msgstr "" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Set a root password" msgstr "" -#: src/components/users/RootAuthMethods.jsx:194 -msgid "Add a SSH Public Key for root" +#: src/components/users/RootAuthMethods.jsx:203 +msgid "Edit the SSH Public Key for root" msgstr "" -#: src/components/users/RootAuthMethods.jsx:194 -msgid "Edit the SSH Public Key for root" +#: src/components/users/RootAuthMethods.jsx:204 +msgid "Add a SSH Public Key for root" msgstr "" -#: src/components/users/RootPasswordPopup.jsx:42 +#: src/components/users/RootPasswordPopup.jsx:43 msgid "Root password" msgstr "" @@ -2508,6 +2433,12 @@ msgstr "" msgid "Root authentication" msgstr "" +#~ msgid "Reading file..." +#~ msgstr "Soubor se načítá…" + +#~ msgid "Cannot read the file" +#~ msgstr "Soubor nelze přečíst" + #, fuzzy #~ msgid "Create or edit the first user" #~ msgstr "Soubor nelze přečíst" diff --git a/web/po/de.po b/web/po/de.po index 205932bce5..b73e88bcb8 100644 --- a/web/po/de.po +++ b/web/po/de.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-07-06 01:47+0000\n" +"POT-Creation-Date: 2024-07-14 02:32+0000\n" +"PO-Revision-Date: 2024-07-13 19:47+0000\n" "Last-Translator: Ettore Atalan \n" "Language-Team: German \n" @@ -19,11 +19,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.6.2\n" -#: src/MainLayout.jsx:40 +#: src/MainLayout.jsx:52 msgid "Agama" msgstr "Agama" -#: src/MainLayout.jsx:82 +#: src/MainLayout.jsx:94 msgid "Change product" msgstr "Produkt ändern" @@ -31,12 +31,11 @@ msgstr "Produkt ändern" msgid "About" msgstr "Über" -#: src/components/core/About.jsx:71 +#: src/components/core/About.jsx:69 msgid "About Agama" msgstr "Über Agama" -#. TRANSLATORS: content of the "About" popup (1/2) -#: src/components/core/About.jsx:76 +#: src/components/core/About.jsx:74 msgid "" "Agama is an experimental installer for (open)SUSE systems. It is still under " "development so, please, do not use it in production environments. If you " @@ -51,27 +50,18 @@ msgstr "" #. TRANSLATORS: content of the "About" popup (2/2) #. %s is replaced by the project URL -#: src/components/core/About.jsx:88 +#: src/components/core/About.jsx:86 #, c-format msgid "For more information, please visit the project's repository at %s." msgstr "" "Für weitere Informationen besuchen Sie bitte das Repositorium des Projekts " "unter %s." -#: src/components/core/About.jsx:94 src/components/core/FileViewer.jsx:81 -#: src/components/core/LogsButton.jsx:123 -#: src/components/software/SoftwarePatternsSelection.jsx:260 +#: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 +#: src/components/software/SoftwarePatternsSelection.jsx:268 msgid "Close" msgstr "Schließen" -#: src/components/core/FileViewer.jsx:66 -msgid "Reading file..." -msgstr "Datei wird gelesen ..." - -#: src/components/core/FileViewer.jsx:72 -msgid "Cannot read the file" -msgstr "Die Datei kann nicht gelesen werden" - #: src/components/core/InstallButton.jsx:32 msgid "Confirm Installation" msgstr "Installation bestätigen" @@ -96,9 +86,9 @@ msgid "Continue" msgstr "Fortsetzen" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:97 -#: src/components/core/Popup.jsx:136 -#: src/components/network/WifiConnectionForm.jsx:131 +#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 +#: src/components/core/Popup.jsx:132 +#: src/components/network/WifiConnectionForm.jsx:134 msgid "Cancel" msgstr "Abbrechen" @@ -107,11 +97,11 @@ msgstr "Abbrechen" msgid "Install" msgstr "Installieren" -#: src/components/core/InstallationFinished.jsx:42 +#: src/components/core/InstallationFinished.jsx:48 msgid "TPM sealing requires the new system to be booted directly." msgstr "Bei der TPM-Versiegelung muss das neue System direkt gebootet werden." -#: src/components/core/InstallationFinished.jsx:47 +#: src/components/core/InstallationFinished.jsx:53 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." @@ -119,16 +109,15 @@ msgstr "" "Wenn ein lokales Medium zur Ausführung dieses Installationsprogramms " "verwendet wurde, entfernen Sie es vor dem nächsten Start." -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "Hide details" msgstr "Details ausblenden" -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "See more details" msgstr "Siehe weitere Details" -#. TRANSLATORS: "Trusted Platform Module" is the name of the technology and "TPM" its abbreviation -#: src/components/core/InstallationFinished.jsx:55 +#: src/components/core/InstallationFinished.jsx:62 msgid "" "The final step to configure the Trusted Platform Module (TPM) to " "automatically open encrypted devices will take place during the first boot " @@ -140,29 +129,29 @@ msgstr "" "neuen Systems. Damit dies funktioniert, muss der Rechner direkt mit dem " "neuen Bootloader booten." -#: src/components/core/InstallationFinished.jsx:97 +#: src/components/core/InstallationFinished.jsx:107 msgid "Congratulations!" msgstr "Gratulation!" -#: src/components/core/InstallationFinished.jsx:102 +#: src/components/core/InstallationFinished.jsx:116 msgid "The installation on your machine is complete." msgstr "Die Installation auf Ihrem Rechner ist abgeschlossen." -#: src/components/core/InstallationFinished.jsx:105 +#: src/components/core/InstallationFinished.jsx:119 msgid "At this point you can power off the machine." msgstr "Nun können Sie den Rechner ausschalten." -#: src/components/core/InstallationFinished.jsx:106 +#: src/components/core/InstallationFinished.jsx:121 msgid "At this point you can reboot the machine to log in to the new system." msgstr "" "Nun können Sie den Rechner neu starten, um sich bei dem neuen System " "anzumelden." -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Finish" msgstr "Fertigstellen" -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Reboot" msgstr "Neustart" @@ -170,40 +159,40 @@ msgstr "Neustart" msgid "Installing the system, please wait ..." msgstr "Das System wird installiert, bitte warten ..." -#: src/components/core/InstallerOptions.jsx:83 +#: src/components/core/InstallerOptions.jsx:92 msgid "Show installer options" msgstr "Installationsprogrammoptionen anzeigen" -#: src/components/core/InstallerOptions.jsx:88 +#: src/components/core/InstallerOptions.jsx:95 msgid "Installer options" msgstr "Installationsprogrammoptionen" -#: src/components/core/InstallerOptions.jsx:94 -#: src/components/core/InstallerOptions.jsx:99 -#: src/components/core/InstallerOptions.jsx:100 -#: src/components/l10n/L10nPage.jsx:67 +#: src/components/core/InstallerOptions.jsx:98 +#: src/components/core/InstallerOptions.jsx:102 +#: src/components/core/InstallerOptions.jsx:103 +#: src/components/l10n/L10nPage.jsx:60 msgid "Language" msgstr "Sprache" -#: src/components/core/InstallerOptions.jsx:114 -#: src/components/core/InstallerOptions.jsx:121 +#: src/components/core/InstallerOptions.jsx:115 +#: src/components/core/InstallerOptions.jsx:120 msgid "Keyboard layout" msgstr "Tastaturbelegung" -#: src/components/core/InstallerOptions.jsx:130 +#: src/components/core/InstallerOptions.jsx:129 msgid "Cannot be changed in remote installation" msgstr "Kann bei der Ferninstallation nicht geändert werden" -#: src/components/core/InstallerOptions.jsx:135 -#: src/components/network/IpSettingsForm.jsx:210 -#: src/components/product/ProductRegistrationPage.jsx:89 -#: src/components/storage/BootSelection.jsx:228 -#: src/components/storage/DeviceSelection.jsx:240 -#: src/components/storage/EncryptionSettingsDialog.jsx:138 -#: src/components/storage/SpacePolicySelection.jsx:198 -#: src/components/storage/VolumeDialog.jsx:781 -#: src/components/storage/ZFCPPage.jsx:503 -#: src/components/users/FirstUserForm.jsx:285 +#: src/components/core/InstallerOptions.jsx:142 +#: src/components/network/IpSettingsForm.jsx:228 +#: src/components/product/ProductRegistrationPage.jsx:85 +#: src/components/storage/BootSelection.jsx:250 +#: src/components/storage/DeviceSelection.jsx:254 +#: src/components/storage/EncryptionSettingsDialog.jsx:155 +#: src/components/storage/SpacePolicySelection.jsx:200 +#: src/components/storage/VolumeDialog.jsx:794 +#: src/components/storage/ZFCPPage.jsx:528 +#: src/components/users/FirstUserForm.jsx:303 msgid "Accept" msgstr "Annehmen" @@ -214,32 +203,29 @@ msgstr "" "Bevor Sie mit der Installation beginnen, müssen Sie sich mit folgenden " "Problemen befassen:" -#: src/components/core/ListSearch.jsx:51 +#: src/components/core/ListSearch.jsx:48 msgid "Search" msgstr "Suchen" -#: src/components/core/LoginPage.jsx:61 +#: src/components/core/LoginPage.jsx:64 msgid "Could not log in. Please, make sure that the password is correct." msgstr "" "Die Anmeldung ist fehlgeschlagen. Bitte stellen Sie sicher, dass das " "Passwort korrekt ist." -#: src/components/core/LoginPage.jsx:63 +#: src/components/core/LoginPage.jsx:66 msgid "Could not authenticate against the server, please check it." msgstr "" "Der Server konnte nicht authentifiziert werden, bitte überprüfen Sie dies." #. TRANSLATORS: Title for a form to provide the password for the root user. %s #. will be replaced by "root" -#: src/components/core/LoginPage.jsx:71 +#: src/components/core/LoginPage.jsx:74 #, c-format msgid "Log in as %s" msgstr "Als %s anmelden" -#. TRANSLATORS: description why root password is needed. The text in the -#. square brackets [] is displayed in bold, use only please, do not translate -#. it and keep the brackets. -#: src/components/core/LoginPage.jsx:76 +#: src/components/core/LoginPage.jsx:80 msgid "The installer requires [root] user privileges." msgstr "Das Installationsprogramm erfordert [root]-Benutzerrechte." @@ -247,32 +233,32 @@ msgstr "Das Installationsprogramm erfordert [root]-Benutzerrechte." msgid "Please, provide its password to log in to the system." msgstr "Bitte geben Sie das Passwort für die Anmeldung am System an." -#: src/components/core/LoginPage.jsx:98 +#: src/components/core/LoginPage.jsx:96 msgid "Login form" msgstr "Anmeldeformular" -#: src/components/core/LoginPage.jsx:104 +#: src/components/core/LoginPage.jsx:102 msgid "Password input" msgstr "Passworteingabe" -#: src/components/core/LoginPage.jsx:113 +#: src/components/core/LoginPage.jsx:111 msgid "Log in" msgstr "Anmelden" -#: src/components/core/LoginPage.jsx:124 +#: src/components/core/LoginPage.jsx:121 msgid "More about this" msgstr "Mehr dazu" -#: src/components/core/LogsButton.jsx:103 +#: src/components/core/LogsButton.jsx:101 msgid "Collecting logs..." msgstr "Protokolle werden gesammelt ..." -#: src/components/core/LogsButton.jsx:103 -#: src/components/core/LogsButton.jsx:106 +#: src/components/core/LogsButton.jsx:101 +#: src/components/core/LogsButton.jsx:104 msgid "Download logs" msgstr "Protokolle herunterladen" -#: src/components/core/LogsButton.jsx:112 +#: src/components/core/LogsButton.jsx:111 msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." @@ -280,33 +266,33 @@ msgstr "" "Der Browser wird das Herunterladen der Protokolle starten, sobald sie bereit " "sind. Bitte haben Sie Geduld." -#: src/components/core/LogsButton.jsx:120 +#: src/components/core/LogsButton.jsx:121 msgid "Something went wrong while collecting logs. Please, try again." msgstr "" "Beim Sammeln der Protokolle ist etwas schiefgelaufen. Bitte versuchen Sie es " "erneut." -#: src/components/core/PasswordAndConfirmationInput.jsx:48 +#: src/components/core/PasswordAndConfirmationInput.jsx:55 msgid "Passwords do not match" msgstr "Passwörter stimmen nicht überein" -#: src/components/core/PasswordAndConfirmationInput.jsx:72 -#: src/components/network/WifiConnectionForm.jsx:120 -#: src/components/storage/iscsi/AuthFields.jsx:95 -#: src/components/storage/iscsi/AuthFields.jsx:100 -#: src/components/users/RootAuthMethods.jsx:163 +#: src/components/core/PasswordAndConfirmationInput.jsx:79 +#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/storage/iscsi/AuthFields.jsx:90 +#: src/components/storage/iscsi/AuthFields.jsx:94 +#: src/components/users/RootAuthMethods.jsx:165 msgid "Password" msgstr "Passwort" -#: src/components/core/PasswordAndConfirmationInput.jsx:85 +#: src/components/core/PasswordAndConfirmationInput.jsx:90 msgid "Password confirmation" msgstr "Passwort bestätigen" -#: src/components/core/PasswordInput.jsx:64 +#: src/components/core/PasswordInput.jsx:61 msgid "Password visibility button" msgstr "Schaltfläche für die Sichtbarkeit des Passworts" -#: src/components/core/Popup.jsx:100 +#: src/components/core/Popup.jsx:92 msgid "Confirm" msgstr "Bestätigen" @@ -323,117 +309,106 @@ msgstr "Fertiggestellt" msgid "In progress" msgstr "In Bearbeitung" -#: src/components/core/ProgressReport.jsx:70 +#: src/components/core/ProgressReport.jsx:74 msgid "Pending" msgstr "Ausstehend" -#: src/components/core/ProgressReport.jsx:134 +#: src/components/core/ProgressReport.jsx:138 msgid "Waiting for progress status..." msgstr "Warten auf den Fortschrittsstatus ..." #: src/components/core/RowActions.jsx:64 -#: src/components/storage/PartitionsField.jsx:454 -#: src/components/storage/ProposalActionsSummary.jsx:226 +#: src/components/storage/PartitionsField.jsx:491 +#: src/components/storage/ProposalActionsSummary.jsx:233 msgid "Actions" msgstr "Aktionen" -#: src/components/core/SectionSkeleton.jsx:29 +#: src/components/core/SectionSkeleton.jsx:27 msgid "Waiting" msgstr "Warten" -#: src/components/core/Selector.jsx:126 -#: src/components/software/SoftwarePatternsSelection.jsx:212 -msgid "auto selected" -msgstr "automatisch ausgewählt" - -#. TRANSLATORS: page title -#: src/components/core/ServerError.jsx:34 -msgid "Agama Error" -msgstr "Agama-Fehler" - -#: src/components/core/ServerError.jsx:38 +#: src/components/core/ServerError.jsx:47 msgid "Cannot connect to Agama server" msgstr "Verbindung zum Agama-Server nicht möglich" -#: src/components/core/ServerError.jsx:43 +#: src/components/core/ServerError.jsx:51 msgid "Please, check whether it is running." msgstr "Bitte prüfen Sie, ob es läuft." -#. TRANSLATORS: button label -#: src/components/core/ServerError.jsx:51 +#: src/components/core/ServerError.jsx:56 msgid "Reload" msgstr "Neu laden" -#: src/components/l10n/KeyboardSelection.jsx:45 +#: src/components/l10n/KeyboardSelection.jsx:41 msgid "Filter by description or keymap code" msgstr "Nach Beschreibung oder Tastenzuordnungscode filtern" -#: src/components/l10n/KeyboardSelection.jsx:85 +#: src/components/l10n/KeyboardSelection.jsx:71 msgid "None of the keymaps match the filter." msgstr "Keine der Tastenzuordnungen entspricht dem Filter." -#: src/components/l10n/KeyboardSelection.jsx:92 +#: src/components/l10n/KeyboardSelection.jsx:77 msgid "Keyboard selection" msgstr "Tastaturauswahl" -#: src/components/l10n/KeyboardSelection.jsx:107 -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/l10n/LocaleSelection.jsx:107 -#: src/components/l10n/TimezoneSelection.jsx:145 -#: src/components/product/ProductSelectionPage.jsx:101 +#: src/components/l10n/KeyboardSelection.jsx:90 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/l10n/LocaleSelection.jsx:92 +#: src/components/l10n/TimezoneSelection.jsx:125 +#: src/components/product/ProductSelectionPage.jsx:90 msgid "Select" msgstr "Auswählen" -#: src/components/l10n/L10nPage.jsx:60 src/components/l10n/routes.js:34 -#: src/components/overview/L10nSection.jsx:42 +#: src/components/l10n/L10nPage.jsx:53 +#: src/components/overview/L10nSection.jsx:37 src/routes/l10n.js:38 msgid "Localization" msgstr "Lokalisierung" -#: src/components/l10n/L10nPage.jsx:68 src/components/l10n/L10nPage.jsx:79 -#: src/components/l10n/L10nPage.jsx:90 +#: src/components/l10n/L10nPage.jsx:61 src/components/l10n/L10nPage.jsx:70 +#: src/components/l10n/L10nPage.jsx:80 msgid "Not selected yet" msgstr "Noch nicht ausgewählt" -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/network/NetworkPage.jsx:102 -#: src/components/storage/InstallationDeviceField.jsx:105 -#: src/components/storage/ProposalActionsSummary.jsx:228 -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/storage/InstallationDeviceField.jsx:108 +#: src/components/storage/ProposalActionsSummary.jsx:238 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Change" msgstr "Ändern" -#: src/components/l10n/L10nPage.jsx:78 +#: src/components/l10n/L10nPage.jsx:70 msgid "Keyboard" msgstr "Tastatur" -#: src/components/l10n/L10nPage.jsx:89 +#: src/components/l10n/L10nPage.jsx:79 msgid "Time zone" msgstr "Zeitzone" -#: src/components/l10n/LocaleSelection.jsx:44 +#: src/components/l10n/LocaleSelection.jsx:39 msgid "Filter by language, territory or locale code" msgstr "Nach Sprache, Gebiet oder Sprachumgebungscode filtern" -#: src/components/l10n/LocaleSelection.jsx:84 +#: src/components/l10n/LocaleSelection.jsx:72 msgid "None of the locales match the filter." msgstr "Keines der Gebietsschemata entspricht dem Filter." -#: src/components/l10n/LocaleSelection.jsx:91 +#: src/components/l10n/LocaleSelection.jsx:78 msgid "Locale selection" msgstr "Gebietsschema-Auswahl" -#: src/components/l10n/TimezoneSelection.jsx:71 +#: src/components/l10n/TimezoneSelection.jsx:64 msgid "Filter by territory, time zone code or UTC offset" msgstr "Nach Gebiet, Zeitzonencode oder UTC-Abweichung filtern" -#: src/components/l10n/TimezoneSelection.jsx:122 +#: src/components/l10n/TimezoneSelection.jsx:101 msgid "None of the time zones match the filter." msgstr "Keine der Zeitzonen entspricht dem Filter." -#: src/components/l10n/TimezoneSelection.jsx:129 +#: src/components/l10n/TimezoneSelection.jsx:107 msgid " Timezone selection" msgstr " Zeitzonenauswahl" @@ -442,111 +417,111 @@ msgid "Loading installation environment, please wait." msgstr "Installationsumgebung wird geladen, bitte warten." #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:78 -#: src/components/network/DnsDataList.jsx:84 +#: src/components/network/AddressesDataList.jsx:88 +#: src/components/network/DnsDataList.jsx:95 msgid "Remove" msgstr "Entfernen" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:90 -#: src/components/network/AddressesDataList.jsx:91 +#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:101 #: src/components/network/IpAddressInput.jsx:33 msgid "IP Address" msgstr "IP-Adresse" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:99 -#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:109 +#: src/components/network/AddressesDataList.jsx:110 msgid "Prefix length or netmask" msgstr "Präfixlänge oder Netzmaske" -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add an address" msgstr "Adresse hinzufügen" #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add another address" msgstr "Weitere Adresse hinzufügen" -#: src/components/network/AddressesDataList.jsx:121 +#: src/components/network/AddressesDataList.jsx:131 msgid "Addresses" msgstr "Adressen" -#: src/components/network/AddressesDataList.jsx:123 +#: src/components/network/AddressesDataList.jsx:133 msgid "Addresses data list" msgstr "Adressdatenliste" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:67 -#: src/components/network/ConnectionsTable.jsx:95 -#: src/components/storage/ZFCPPage.jsx:361 +#: src/components/network/ConnectionsTable.jsx:64 +#: src/components/network/ConnectionsTable.jsx:92 +#: src/components/storage/ZFCPPage.jsx:381 #: src/components/storage/iscsi/InitiatorForm.jsx:52 #: src/components/storage/iscsi/InitiatorPresenter.jsx:68 #: src/components/storage/iscsi/InitiatorPresenter.jsx:85 -#: src/components/storage/iscsi/NodesPresenter.jsx:100 -#: src/components/storage/iscsi/NodesPresenter.jsx:121 +#: src/components/storage/iscsi/NodesPresenter.jsx:98 +#: src/components/storage/iscsi/NodesPresenter.jsx:119 msgid "Name" msgstr "Name" #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:69 -#: src/components/network/ConnectionsTable.jsx:96 +#: src/components/network/ConnectionsTable.jsx:66 +#: src/components/network/ConnectionsTable.jsx:93 msgid "IP addresses" msgstr "IP-Adressen" -#: src/components/network/ConnectionsTable.jsx:77 -#: src/components/network/WifiNetworksListPage.jsx:100 -#: src/components/network/WifiNetworksListPage.jsx:124 -#: src/components/storage/PartitionsField.jsx:320 +#: src/components/network/ConnectionsTable.jsx:74 +#: src/components/network/WifiNetworksListPage.jsx:107 +#: src/components/network/WifiNetworksListPage.jsx:130 +#: src/components/storage/PartitionsField.jsx:347 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 -#: src/components/users/FirstUser.jsx:116 +#: src/components/users/FirstUser.jsx:120 msgid "Edit" msgstr "Bearbeiten" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:80 -#: src/components/network/IpSettingsForm.jsx:136 +#: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/IpSettingsForm.jsx:151 #, c-format msgid "Edit connection %s" msgstr "Verbindung %s bearbeiten" -#: src/components/network/ConnectionsTable.jsx:84 -#: src/components/network/WifiNetworksListPage.jsx:103 -#: src/components/network/WifiNetworksListPage.jsx:127 +#: src/components/network/ConnectionsTable.jsx:81 +#: src/components/network/WifiNetworksListPage.jsx:109 +#: src/components/network/WifiNetworksListPage.jsx:137 msgid "Forget" msgstr "Vergessen" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:86 +#: src/components/network/ConnectionsTable.jsx:83 #, c-format msgid "Forget connection %s" msgstr "Verbindung %s vergessen" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:101 +#: src/components/network/ConnectionsTable.jsx:98 #, c-format msgid "Actions for connection %s" msgstr "Aktionen für Verbindung %s" #. TRANSLATORS: input field name -#: src/components/network/DnsDataList.jsx:75 -#: src/components/network/DnsDataList.jsx:76 +#: src/components/network/DnsDataList.jsx:81 +#: src/components/network/DnsDataList.jsx:82 msgid "Server IP" msgstr "Server-IP" -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add DNS" msgstr "DNS hinzufügen" #. TRANSLATORS: button label -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add another DNS" msgstr "Weiteren DNS hinzufügen" -#: src/components/network/DnsDataList.jsx:98 +#: src/components/network/DnsDataList.jsx:109 msgid "DNS" msgstr "DNS" @@ -556,43 +531,43 @@ msgid "IP prefix or netmask" msgstr "IP-Präfix oder Netzmaske" #. TRANSLATORS: error message -#: src/components/network/IpSettingsForm.jsx:90 +#: src/components/network/IpSettingsForm.jsx:104 msgid "At least one address must be provided for selected mode" msgstr "" "Für den ausgewählten Modus muss mindestens eine Adresse angegeben werden" #. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) -#: src/components/network/IpSettingsForm.jsx:145 -#: src/components/network/IpSettingsForm.jsx:150 -#: src/components/network/IpSettingsForm.jsx:152 +#: src/components/network/IpSettingsForm.jsx:160 +#: src/components/network/IpSettingsForm.jsx:165 +#: src/components/network/IpSettingsForm.jsx:167 msgid "Mode" msgstr "Modus" -#: src/components/network/IpSettingsForm.jsx:156 +#: src/components/network/IpSettingsForm.jsx:174 msgid "Automatic (DHCP)" msgstr "Automatisch (DHCP)" #. TRANSLATORS: manual network configuration mode with a static IP address -#: src/components/network/IpSettingsForm.jsx:158 +#: src/components/network/IpSettingsForm.jsx:177 #: src/components/storage/iscsi/NodeStartupOptions.js:25 msgid "Manual" msgstr "Manuell" #. TRANSLATORS: network gateway configuration -#: src/components/network/IpSettingsForm.jsx:166 -#: src/components/network/IpSettingsForm.jsx:169 +#: src/components/network/IpSettingsForm.jsx:185 +#: src/components/network/IpSettingsForm.jsx:188 msgid "Gateway" msgstr "Gateway" -#: src/components/network/IpSettingsForm.jsx:178 +#: src/components/network/IpSettingsForm.jsx:196 msgid "Gateway can be defined only in 'Manual' mode" msgstr "Gateway kann nur im Modus ‚Manuell‘ definiert werden" -#: src/components/network/NetworkPage.jsx:85 +#: src/components/network/NetworkPage.jsx:93 msgid "No Wi-Fi supported" msgstr "Kein Wi-Fi unterstützt" -#: src/components/network/NetworkPage.jsx:86 +#: src/components/network/NetworkPage.jsx:95 #, fuzzy msgid "" "The system does not support Wi-Fi connections, probably because of missing " @@ -601,28 +576,28 @@ msgstr "" "Das System unterstützt keine WiFi-Verbindungen, wahrscheinlich wegen " "fehlender oder deaktivierter Hardware." -#: src/components/network/NetworkPage.jsx:99 +#: src/components/network/NetworkPage.jsx:109 #, fuzzy msgid "Wi-Fi" msgstr "Wi-Fi" #. TRANSLATORS: button label, connect to a WiFi network -#: src/components/network/NetworkPage.jsx:102 -#: src/components/network/WifiConnectionForm.jsx:128 -#: src/components/network/WifiNetworksListPage.jsx:97 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:130 +#: src/components/network/WifiNetworksListPage.jsx:105 msgid "Connect" msgstr "Verbinden" -#: src/components/network/NetworkPage.jsx:109 +#: src/components/network/NetworkPage.jsx:119 #, c-format msgid "Conected to %s" msgstr "Verbunden mit %s" -#: src/components/network/NetworkPage.jsx:114 +#: src/components/network/NetworkPage.jsx:126 msgid "No connected yet" msgstr "Noch nicht verbunden" -#: src/components/network/NetworkPage.jsx:115 +#: src/components/network/NetworkPage.jsx:127 #, fuzzy msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." @@ -630,17 +605,16 @@ msgstr "" "Das System wurde noch nicht für die Verbindung mit einem WiFi-Netzwerk " "konfiguriert." -#: src/components/network/NetworkPage.jsx:136 +#: src/components/network/NetworkPage.jsx:156 #, fuzzy msgid "Wired" msgstr "Kabelgebunden" -#: src/components/network/NetworkPage.jsx:139 -#, fuzzy +#: src/components/network/NetworkPage.jsx:160 msgid "No wired connections found" -msgstr "Keine kabelgebundenen Verbindungen gefunden." +msgstr "Keine kabelgebundenen Verbindungen gefunden" -#: src/components/network/NetworkPage.jsx:149 +#: src/components/network/NetworkPage.jsx:173 #: src/components/network/routes.js:59 msgid "Network" msgstr "Netzwerk" @@ -656,16 +630,16 @@ msgstr "Kein" msgid "WPA & WPA2 Personal" msgstr "WPA & WPA2 Personal" -#: src/components/network/WifiConnectionForm.jsx:86 -#: src/components/product/ProductRegistrationPage.jsx:69 -#: src/components/storage/ZFCPDiskForm.jsx:108 -#: src/components/storage/iscsi/DiscoverForm.jsx:110 -#: src/components/storage/iscsi/LoginForm.jsx:72 -#: src/components/users/FirstUserForm.jsx:203 +#: src/components/network/WifiConnectionForm.jsx:85 +#: src/components/product/ProductRegistrationPage.jsx:68 +#: src/components/storage/ZFCPDiskForm.jsx:105 +#: src/components/storage/iscsi/DiscoverForm.jsx:98 +#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/users/FirstUserForm.jsx:217 msgid "Something went wrong" msgstr "Etwas ist schiefgelaufen" -#: src/components/network/WifiConnectionForm.jsx:87 +#: src/components/network/WifiConnectionForm.jsx:86 msgid "Please, review provided settings and try again." msgstr "" "Bitte überprüfen Sie die bereitgestellten Einstellungen und versuchen Sie es " @@ -678,99 +652,99 @@ msgid "SSID" msgstr "SSID" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:104 -#: src/components/network/WifiConnectionForm.jsx:107 +#: src/components/network/WifiConnectionForm.jsx:105 +#: src/components/network/WifiConnectionForm.jsx:108 msgid "Security" msgstr "Sicherheit" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:116 +#: src/components/network/WifiConnectionForm.jsx:117 msgid "WPA Password" msgstr "WPA-Passwort" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:51 -#: src/components/network/WifiNetworksListPage.jsx:111 +#: src/components/network/WifiNetworksListPage.jsx:63 +#: src/components/network/WifiNetworksListPage.jsx:117 msgid "Connecting" msgstr "Wird verbunden" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:54 -#: src/components/network/WifiNetworksListPage.jsx:115 -#: src/components/network/WifiNetworksListPage.jsx:154 +#: src/components/network/WifiNetworksListPage.jsx:66 +#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:164 msgid "Connected" msgstr "Verbunden" #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:59 -#: src/components/network/WifiNetworksListPage.jsx:113 +#: src/components/network/WifiNetworksListPage.jsx:71 +#: src/components/network/WifiNetworksListPage.jsx:119 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" msgstr "Getrennt" -#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:127 msgid "Disconnect" msgstr "Trennen" -#: src/components/network/WifiNetworksListPage.jsx:142 +#: src/components/network/WifiNetworksListPage.jsx:150 #, fuzzy msgid "Connect to a hidden network" msgstr "Mit einem verborgenem Netzwerk verbinden" -#: src/components/network/WifiNetworksListPage.jsx:153 +#: src/components/network/WifiNetworksListPage.jsx:161 msgid "configured" msgstr "konfiguriert" -#: src/components/network/WifiNetworksListPage.jsx:244 +#: src/components/network/WifiNetworksListPage.jsx:265 msgid "Connect to hidden network" msgstr "Mit verborgenem Netzwerk verbinden" -#: src/components/network/WifiSelectorPage.jsx:131 +#: src/components/network/WifiSelectorPage.jsx:136 #, fuzzy msgid "Connect to a Wi-Fi network" msgstr "Mit einem Wi-Fi-Netzwerk verbinden" #. TRANSLATORS: %s will be replaced by a language name and territory, example: #. "English (United States)". -#: src/components/overview/L10nSection.jsx:38 +#: src/components/overview/L10nSection.jsx:33 #, c-format msgid "The system will use %s as its default language." msgstr "Das System wird %s als Standardsprache verwenden." -#: src/components/overview/OverviewPage.jsx:45 +#: src/components/overview/OverviewPage.jsx:47 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "Benutzer" -#: src/components/overview/OverviewPage.jsx:46 +#: src/components/overview/OverviewPage.jsx:48 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:290 +#: src/components/storage/ProposalPage.jsx:307 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "Speicherung" -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/overview/SoftwareSection.jsx:86 #: src/components/software/SoftwarePage.jsx:155 #: src/components/software/routes.js:32 msgid "Software" msgstr "Software" -#: src/components/overview/OverviewPage.jsx:52 +#: src/components/overview/OverviewPage.jsx:54 msgid "Ready for installation" msgstr "Bereit zur Installation" -#: src/components/overview/OverviewPage.jsx:102 +#: src/components/overview/OverviewPage.jsx:104 msgid "Installation" msgstr "Installation" -#: src/components/overview/OverviewPage.jsx:103 +#: src/components/overview/OverviewPage.jsx:105 msgid "Before installing, please check the following problems." msgstr "Bitte überprüfen Sie vor der Installation die folgenden Probleme." -#: src/components/overview/OverviewPage.jsx:114 +#: src/components/overview/OverviewPage.jsx:116 msgid "" "Take your time to check your configuration before starting the installation " "process." @@ -778,7 +752,7 @@ msgstr "" "Nehmen Sie sich die Zeit, Ihre Konfiguration zu überprüfen, bevor Sie mit " "der Installation beginnen." -#: src/components/overview/OverviewPage.jsx:123 +#: src/components/overview/OverviewPage.jsx:125 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -862,14 +836,14 @@ msgstr "" "%s unter Verwendung einer benutzerdefinierten Strategie, um den benötigten " "Platz zu finden" -#: src/components/overview/StorageSection.jsx:175 -#: src/components/storage/InstallationDeviceField.jsx:63 +#: src/components/overview/StorageSection.jsx:179 +#: src/components/storage/InstallationDeviceField.jsx:66 msgid "No device selected yet" msgstr "Noch kein Gerät ausgewählt" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:182 +#: src/components/overview/StorageSection.jsx:186 #, c-format msgid "Install using device %s shrinking existing partitions as needed" msgstr "" @@ -878,7 +852,7 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:186 +#: src/components/overview/StorageSection.jsx:190 #, c-format msgid "Install using device %s without modifying existing partitions" msgstr "" @@ -887,7 +861,7 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:190 +#: src/components/overview/StorageSection.jsx:194 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" @@ -896,7 +870,7 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:195 +#: src/components/overview/StorageSection.jsx:199 #, c-format msgid "Install using device %s with a custom strategy to find the needed space" msgstr "" @@ -912,19 +886,15 @@ msgstr "Übersicht" msgid "Register %s" msgstr "%s registrieren" -#: src/components/product/ProductRegistrationPage.jsx:74 +#: src/components/product/ProductRegistrationPage.jsx:73 msgid "Registration code" msgstr "Registrierungscode" -#: src/components/product/ProductRegistrationPage.jsx:77 +#: src/components/product/ProductRegistrationPage.jsx:76 msgid "Email" msgstr "E-Mail" -#: src/components/product/ProductSelectionPage.jsx:58 -msgid "Loading available products, please wait..." -msgstr "Verfügbare Produkte werden geladen, bitte warten ..." - -#: src/components/product/ProductSelectionProgress.jsx:53 +#: src/components/product/ProductSelectionProgress.jsx:49 msgid "Configuring the product, please wait ..." msgstr "Produkt wird konfiguriert, bitte warten ..." @@ -943,7 +913,7 @@ msgid "Encrypted Device" msgstr "Verschlüsseltes Gerät" #. TRANSLATORS: field label -#: src/components/questions/LuksActivationQuestion.jsx:69 +#: src/components/questions/LuksActivationQuestion.jsx:67 msgid "Encryption Password" msgstr "Verschlüsselungspasswort" @@ -963,17 +933,21 @@ msgstr "Ausgewählte Muster" msgid "Change selection" msgstr "Auswahl ändern" -#: src/components/software/SoftwarePatternsSelection.jsx:230 +#: src/components/software/SoftwarePatternsSelection.jsx:223 +msgid "auto selected" +msgstr "automatisch ausgewählt" + +#: src/components/software/SoftwarePatternsSelection.jsx:241 msgid "None of the patterns match the filter." msgstr "Keines der Muster entspricht dem Filter." -#: src/components/software/SoftwarePatternsSelection.jsx:238 +#: src/components/software/SoftwarePatternsSelection.jsx:248 msgid "Software selection" msgstr "Softwareauswahl" #. TRANSLATORS: search field placeholder text -#: src/components/software/SoftwarePatternsSelection.jsx:241 -#: src/components/software/SoftwarePatternsSelection.jsx:242 +#: src/components/software/SoftwarePatternsSelection.jsx:251 +#: src/components/software/SoftwarePatternsSelection.jsx:252 msgid "Filter by pattern title or description" msgstr "Nach Mustertitel oder Beschreibung filtern" @@ -984,7 +958,7 @@ msgstr "Nach Mustertitel oder Beschreibung filtern" msgid "Installation will take %s." msgstr "Installation wird %s in Anspruch nehmen." -#: src/components/software/UsedSize.jsx:38 +#: src/components/software/UsedSize.jsx:37 msgid "This space includes the base system and the selected software patterns." msgstr "" "Dieser Bereich umfasst das Basissystem und die ausgewählten Softwaremuster." @@ -993,25 +967,24 @@ msgstr "" msgid "Change boot options" msgstr "Boot-Optionen ändern" -#: src/components/storage/BootConfigField.jsx:87 +#: src/components/storage/BootConfigField.jsx:81 msgid "Installation will not configure partitions for booting." msgstr "" "Bei der Installation werden keine Partitionen für das Booten konfiguriert." -#: src/components/storage/BootConfigField.jsx:89 +#: src/components/storage/BootConfigField.jsx:85 msgid "" "Installation will configure partitions for booting at the installation disk." msgstr "" "Bei der Installation werden die Partitionen für das Booten von der " "Installationsfestplatte konfiguriert." -#. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/BootConfigField.jsx:92 +#: src/components/storage/BootConfigField.jsx:89 #, c-format msgid "Installation will configure partitions for booting at %s." msgstr "Die Installation konfiguriert Partitionen für das Booten bei %s." -#: src/components/storage/BootSelection.jsx:127 +#: src/components/storage/BootSelection.jsx:132 msgid "" "To ensure the new system is able to boot, the installer may need to create " "or configure some partitions in the appropriate disk." @@ -1020,47 +993,47 @@ msgstr "" "Installationsprogramm möglicherweise einige Partitionen auf der " "entsprechenden Festplatte erstellen oder konfigurieren." -#: src/components/storage/BootSelection.jsx:133 +#: src/components/storage/BootSelection.jsx:138 msgid "Partitions to boot will be allocated at the installation disk." msgstr "" "Die zu bootenden Partitionen werden auf der Installationsfestplatte " "zugewiesen." #. TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") -#: src/components/storage/BootSelection.jsx:138 +#: src/components/storage/BootSelection.jsx:143 #, c-format msgid "Partitions to boot will be allocated at the installation disk (%s)." msgstr "" "Die zu bootenden Partitionen werden auf der Installationsfestplatte (%s) " "zugewiesen." -#: src/components/storage/BootSelection.jsx:154 +#: src/components/storage/BootSelection.jsx:159 msgid "Select booting partition" msgstr "Boot-Partition auswählen" -#: src/components/storage/BootSelection.jsx:170 +#: src/components/storage/BootSelection.jsx:180 #: src/components/storage/iscsi/NodeStartupOptions.js:27 msgid "Automatic" msgstr "Automatisch" -#: src/components/storage/BootSelection.jsx:183 +#: src/components/storage/BootSelection.jsx:198 msgid "Select a disk" msgstr "Festplatte auswählen" -#: src/components/storage/BootSelection.jsx:189 +#: src/components/storage/BootSelection.jsx:204 msgid "Partitions to boot will be allocated at the following device." msgstr "" "Die zu bootenden Partitionen werden auf dem folgenden Gerät zugewiesen." -#: src/components/storage/BootSelection.jsx:192 +#: src/components/storage/BootSelection.jsx:207 msgid "Choose a disk for placing the boot loader" msgstr "Wählen Sie eine Festplatte für den Bootloader aus" -#: src/components/storage/BootSelection.jsx:210 +#: src/components/storage/BootSelection.jsx:230 msgid "Do not configure" msgstr "Nicht konfigurieren" -#: src/components/storage/BootSelection.jsx:215 +#: src/components/storage/BootSelection.jsx:236 msgid "" "No partitions will be automatically configured for booting. Use with caution." msgstr "" @@ -1071,125 +1044,117 @@ msgstr "" msgid "Waiting for progress report" msgstr "Warten auf den Fortschrittsbericht" -#: src/components/storage/DASDFormatProgress.jsx:68 +#: src/components/storage/DASDFormatProgress.jsx:67 msgid "Formatting DASD devices" msgstr "DASD-Geräte formatieren" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "No" msgstr "Nein" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "Yes" msgstr "Ja" -#: src/components/storage/DASDTable.jsx:63 -#: src/components/storage/ZFCPDiskForm.jsx:112 -#: src/components/storage/ZFCPPage.jsx:302 -#: src/components/storage/ZFCPPage.jsx:362 +#: src/components/storage/DASDTable.jsx:69 +#: src/components/storage/ZFCPDiskForm.jsx:110 +#: src/components/storage/ZFCPPage.jsx:324 +#: src/components/storage/ZFCPPage.jsx:382 msgid "Channel ID" msgstr "Kanalkennung" #. TRANSLATORS: table header -#: src/components/storage/DASDTable.jsx:64 -#: src/components/storage/ZFCPPage.jsx:303 -#: src/components/storage/iscsi/NodesPresenter.jsx:104 -#: src/components/storage/iscsi/NodesPresenter.jsx:125 -#: src/components/users/RootAuthMethods.jsx:157 +#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/ZFCPPage.jsx:325 +#: src/components/storage/iscsi/NodesPresenter.jsx:102 +#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/users/RootAuthMethods.jsx:159 msgid "Status" msgstr "Status" -#: src/components/storage/DASDTable.jsx:65 -#: src/components/storage/DeviceSelectorTable.jsx:186 -#: src/components/storage/ProposalResultTable.jsx:120 -#: src/components/storage/SpaceActionsTable.jsx:141 -#: src/components/storage/VolumeLocationSelectorTable.jsx:100 +#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DeviceSelectorTable.jsx:197 +#: src/components/storage/ProposalResultTable.jsx:130 +#: src/components/storage/SpaceActionsTable.jsx:200 +#: src/components/storage/VolumeLocationSelectorTable.jsx:107 msgid "Device" msgstr "Gerät" -#: src/components/storage/DASDTable.jsx:66 +#: src/components/storage/DASDTable.jsx:72 msgid "Type" msgstr "Art" #. TRANSLATORS: table header, the column contains "Yes"/"No" values #. for the DIAG access mode (special disk access mode on IBM mainframes), #. usually keep untranslated -#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/DASDTable.jsx:76 msgid "DIAG" msgstr "DIAG" -#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DASDTable.jsx:77 msgid "Formatted" msgstr "Formatiert" -#: src/components/storage/DASDTable.jsx:72 +#: src/components/storage/DASDTable.jsx:78 msgid "Partition Info" msgstr "Partitionierungsinformationen" #. TRANSLATORS: drop down menu label -#: src/components/storage/DASDTable.jsx:107 +#: src/components/storage/DASDTable.jsx:115 msgid "Perform an action" msgstr "Aktion durchführen" -#. TRANSLATORS: drop down menu action, activate the device -#: src/components/storage/DASDTable.jsx:113 -#: src/components/storage/ZFCPPage.jsx:333 +#: src/components/storage/DASDTable.jsx:122 +#: src/components/storage/ZFCPPage.jsx:353 msgid "Activate" msgstr "Aktivieren" -#. TRANSLATORS: drop down menu action, deactivate the device -#: src/components/storage/DASDTable.jsx:115 -#: src/components/storage/ZFCPPage.jsx:375 +#: src/components/storage/DASDTable.jsx:126 +#: src/components/storage/ZFCPPage.jsx:395 msgid "Deactivate" msgstr "Deaktivieren" -#. TRANSLATORS: drop down menu action, enable DIAG access method -#: src/components/storage/DASDTable.jsx:118 +#: src/components/storage/DASDTable.jsx:131 msgid "Set DIAG On" msgstr "DIAG einschalten" -#. TRANSLATORS: drop down menu action, disable DIAG access method -#: src/components/storage/DASDTable.jsx:120 +#: src/components/storage/DASDTable.jsx:135 msgid "Set DIAG Off" msgstr "DIAG ausschalten" -#. TRANSLATORS: drop down menu action, format the disk -#: src/components/storage/DASDTable.jsx:123 +#: src/components/storage/DASDTable.jsx:140 msgid "Format" msgstr "Formatieren" -#: src/components/storage/DASDTable.jsx:223 -#: src/components/storage/DASDTable.jsx:224 +#: src/components/storage/DASDTable.jsx:261 +#: src/components/storage/DASDTable.jsx:262 msgid "Filter by min channel" msgstr "" -#: src/components/storage/DASDTable.jsx:231 +#: src/components/storage/DASDTable.jsx:269 msgid "Remove min channel filter" msgstr "" -#: src/components/storage/DASDTable.jsx:244 -#: src/components/storage/DASDTable.jsx:245 +#: src/components/storage/DASDTable.jsx:283 +#: src/components/storage/DASDTable.jsx:284 msgid "Filter by max channel" msgstr "" -#: src/components/storage/DASDTable.jsx:252 +#: src/components/storage/DASDTable.jsx:291 msgid "Remove max channel filter" msgstr "" -#: src/components/storage/DeviceSelection.jsx:101 +#: src/components/storage/DeviceSelection.jsx:108 msgid "Loading data, please wait a second..." msgstr "Daten werden geladen, bitte warten Sie eine Sekunde ..." -#. TRANSLATORS: description for using plain partitions for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:136 +#: src/components/storage/DeviceSelection.jsx:144 msgid "" "The file systems will be allocated by default as [new partitions in the " "selected device]." @@ -1197,10 +1162,7 @@ msgstr "" "Die Dateisysteme werden standardmäßig zugewiesen als [neue Partitionen im " "ausgewählten Gerät]." -#. TRANSLATORS: description for using logical volumes for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:141 +#: src/components/storage/DeviceSelection.jsx:151 #, fuzzy msgid "" "The file systems will be allocated by default as [logical volumes of a new " @@ -1211,140 +1173,140 @@ msgstr "" "neuen LVM-Volume-Gruppe]. Die entsprechenden physischen Volumes werden bei " "Bedarf als neue Partitionen auf den ausgewählten Geräten erstellt." -#: src/components/storage/DeviceSelection.jsx:149 +#: src/components/storage/DeviceSelection.jsx:160 msgid "Select installation device" msgstr "Installationsgerät auswählen" -#: src/components/storage/DeviceSelection.jsx:155 +#: src/components/storage/DeviceSelection.jsx:166 msgid "Install new system on" msgstr "Neues System installieren auf" -#: src/components/storage/DeviceSelection.jsx:158 +#: src/components/storage/DeviceSelection.jsx:169 msgid "An existing disk" msgstr "Eine vorhandene Festplatte" -#: src/components/storage/DeviceSelection.jsx:167 +#: src/components/storage/DeviceSelection.jsx:178 msgid "A new LVM Volume Group" msgstr "Eine neue LVM-Volume-Gruppe" -#: src/components/storage/DeviceSelection.jsx:192 +#: src/components/storage/DeviceSelection.jsx:203 msgid "Device selector for target disk" msgstr "Geräteselektor für Zielfestplatte" -#: src/components/storage/DeviceSelection.jsx:215 +#: src/components/storage/DeviceSelection.jsx:226 #, fuzzy msgid "Device selector for new LVM volume group" msgstr "Geräteselektor für neue LVM-Volume-Gruppe" -#: src/components/storage/DeviceSelection.jsx:228 +#: src/components/storage/DeviceSelection.jsx:242 #, fuzzy msgid "Prepare more devices by configuring advanced" msgstr "" "Bereiten Sie weitere Geräte vor, indem Sie erweiterte Konfigurationen " "vornehmen" -#: src/components/storage/DeviceSelection.jsx:229 +#: src/components/storage/DeviceSelection.jsx:243 #, fuzzy msgid "storage techs" msgstr "Speichertechnologien" #. TRANSLATORS: multipath device type -#: src/components/storage/DeviceSelectorTable.jsx:57 +#: src/components/storage/DeviceSelectorTable.jsx:61 msgid "Multipath" msgstr "Multipfad" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/DeviceSelectorTable.jsx:62 +#: src/components/storage/DeviceSelectorTable.jsx:66 #, c-format msgid "DASD %s" msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/DeviceSelectorTable.jsx:67 +#: src/components/storage/DeviceSelectorTable.jsx:71 #, c-format msgid "Software %s" msgstr "Software %s" -#: src/components/storage/DeviceSelectorTable.jsx:72 +#: src/components/storage/DeviceSelectorTable.jsx:76 msgid "SD Card" msgstr "SD-Karte" #. TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" -#: src/components/storage/DeviceSelectorTable.jsx:77 +#: src/components/storage/DeviceSelectorTable.jsx:81 #, c-format msgid "%s disk" msgstr "Festplatte %s" -#: src/components/storage/DeviceSelectorTable.jsx:78 +#: src/components/storage/DeviceSelectorTable.jsx:82 msgid "Disk" msgstr "Festplatte" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:98 +#: src/components/storage/DeviceSelectorTable.jsx:102 #, c-format msgid "Members: %s" msgstr "Mitglieder: %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:107 +#: src/components/storage/DeviceSelectorTable.jsx:111 #, c-format msgid "Devices: %s" msgstr "Geräte: %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/DeviceSelectorTable.jsx:116 +#: src/components/storage/DeviceSelectorTable.jsx:120 #, fuzzy, c-format msgid "Wires: %s" msgstr "Leitungen: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/DeviceSelectorTable.jsx:152 +#: src/components/storage/DeviceSelectorTable.jsx:155 #, c-format msgid "%s with %d partitions" msgstr "%s mit %d Partitionen" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/DeviceSelectorTable.jsx:158 -#: src/components/storage/SpaceActionsTable.jsx:114 +#: src/components/storage/DeviceSelectorTable.jsx:161 +#: src/components/storage/SpaceActionsTable.jsx:175 msgid "No content found" msgstr "Kein Inhalt gefunden" -#: src/components/storage/DeviceSelectorTable.jsx:187 -#: src/components/storage/PartitionsField.jsx:450 -#: src/components/storage/ProposalResultTable.jsx:122 -#: src/components/storage/SpaceActionsTable.jsx:142 -#: src/components/storage/VolumeLocationSelectorTable.jsx:101 +#: src/components/storage/DeviceSelectorTable.jsx:198 +#: src/components/storage/PartitionsField.jsx:487 +#: src/components/storage/ProposalResultTable.jsx:132 +#: src/components/storage/SpaceActionsTable.jsx:201 +#: src/components/storage/VolumeLocationSelectorTable.jsx:108 msgid "Details" msgstr "Details" -#: src/components/storage/DeviceSelectorTable.jsx:188 -#: src/components/storage/PartitionsField.jsx:451 -#: src/components/storage/ProposalResultTable.jsx:123 -#: src/components/storage/SpaceActionsTable.jsx:143 -#: src/components/storage/VolumeFields.jsx:474 -#: src/components/storage/VolumeLocationSelectorTable.jsx:103 +#: src/components/storage/DeviceSelectorTable.jsx:199 +#: src/components/storage/PartitionsField.jsx:488 +#: src/components/storage/ProposalResultTable.jsx:133 +#: src/components/storage/SpaceActionsTable.jsx:202 +#: src/components/storage/VolumeFields.jsx:488 +#: src/components/storage/VolumeLocationSelectorTable.jsx:113 msgid "Size" msgstr "Größe" -#: src/components/storage/DevicesTechMenu.jsx:44 +#: src/components/storage/DevicesTechMenu.jsx:38 msgid "Manage and format" msgstr "Verwalten und formatieren" -#: src/components/storage/DevicesTechMenu.jsx:62 +#: src/components/storage/DevicesTechMenu.jsx:52 msgid "Activate disks" msgstr "Festplatten aktivieren" -#: src/components/storage/DevicesTechMenu.jsx:64 +#: src/components/storage/DevicesTechMenu.jsx:53 msgid "zFCP" msgstr "zFCP" -#: src/components/storage/DevicesTechMenu.jsx:80 +#: src/components/storage/DevicesTechMenu.jsx:66 msgid "Connect to iSCSI targets" msgstr "Mit iSCSI-Zielen verbinden" -#: src/components/storage/DevicesTechMenu.jsx:82 +#: src/components/storage/DevicesTechMenu.jsx:67 #: src/components/storage/routes.js:37 msgid "iSCSI" msgstr "iSCSI" @@ -1354,7 +1316,7 @@ msgstr "iSCSI" msgid "Encryption" msgstr "Verschlüsselung" -#: src/components/storage/EncryptionField.jsx:39 +#: src/components/storage/EncryptionField.jsx:40 #, fuzzy msgid "" "Protection for the information stored at the device, including data, " @@ -1364,27 +1326,27 @@ msgstr "" "auf dem Gerät gespeicherten Informationen, einschließlich Daten, Programme " "und Systemdateien." -#: src/components/storage/EncryptionField.jsx:42 +#: src/components/storage/EncryptionField.jsx:44 msgid "disabled" msgstr "deaktiviert" -#: src/components/storage/EncryptionField.jsx:43 +#: src/components/storage/EncryptionField.jsx:45 msgid "enabled" msgstr "aktiviert" -#: src/components/storage/EncryptionField.jsx:44 +#: src/components/storage/EncryptionField.jsx:46 msgid "using TPM unlocking" msgstr "TPM-Entsperrung verwenden" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Enable" msgstr "Aktivieren" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Modify" msgstr "Ändern" -#: src/components/storage/EncryptionSettingsDialog.jsx:37 +#: src/components/storage/EncryptionSettingsDialog.jsx:38 msgid "" "Full Disk Encryption (FDE) allows to protect the information stored at the " "device, including data, programs, and system files." @@ -1394,16 +1356,14 @@ msgstr "" "und Systemdateien." #. TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation -#: src/components/storage/EncryptionSettingsDialog.jsx:40 +#: src/components/storage/EncryptionSettingsDialog.jsx:42 msgid "" "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot" msgstr "" "Das Trusted Platform Module (TPM) zur automatischen Entschlüsselung bei " "jedem Bootvorgang verwenden" -#. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing -#. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. -#: src/components/storage/EncryptionSettingsDialog.jsx:43 +#: src/components/storage/EncryptionSettingsDialog.jsx:46 #, fuzzy msgid "" "The password will not be needed to boot and access the data if the TPM can " @@ -1415,12 +1375,12 @@ msgstr "" "Systems verifizieren kann. Die TPM-Versiegelung erfordert, dass das neue " "System bei seinem ersten Start direkt gebootet wird." -#: src/components/storage/EncryptionSettingsDialog.jsx:114 +#: src/components/storage/EncryptionSettingsDialog.jsx:129 msgid "Encrypt the system" msgstr "System verschlüsseln" #: src/components/storage/InstallationDeviceField.jsx:36 -#: src/components/storage/VolumeLocationSelectorTable.jsx:58 +#: src/components/storage/VolumeLocationSelectorTable.jsx:61 msgid "Installation device" msgstr "Installationsgerät" @@ -1441,93 +1401,92 @@ msgstr "Dateisysteme als neue Partitionen bei %s erstellt" msgid "File systems created at a new LVM volume group" msgstr "Dateisysteme bei einer neuen LVM-Volume-Gruppe erstellt" -#. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) -#: src/components/storage/InstallationDeviceField.jsx:59 +#: src/components/storage/InstallationDeviceField.jsx:60 #, fuzzy, c-format msgid "File systems created at a new LVM volume group on %s" msgstr "Dateisysteme bei einer neuen LVM-Volume-Gruppe auf %s erstellt" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:73 +#: src/components/storage/PartitionsField.jsx:84 #, c-format msgid "at least %s" msgstr "mindestens %s" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:97 +#: src/components/storage/PartitionsField.jsx:108 #, fuzzy, c-format msgid "Transactional Btrfs root volume (%s)" msgstr "Transaktionales Btrfs-Wurzel-Volume (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:99 +#: src/components/storage/PartitionsField.jsx:110 #, fuzzy, c-format msgid "Transactional Btrfs root partition (%s)" msgstr "Transaktionale Btrfs-Wurzelpartition (%s)" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:104 +#: src/components/storage/PartitionsField.jsx:115 #, fuzzy, c-format msgid "Btrfs root volume with snapshots (%s)" msgstr "Btrfs-Wurzel-Volume mit Schnappschüssen (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:106 +#: src/components/storage/PartitionsField.jsx:117 #, fuzzy, c-format msgid "Btrfs root partition with snapshots (%s)" msgstr "Btrfs-Wurzel-Volume mit Schnappschüssen (%s)" #. TRANSLATORS: This results in something like "Mount /dev/sda3 at /home (25 GiB)" since #. %1$s is replaced by the device name, %2$s by the mount point and %3$s by the size -#: src/components/storage/PartitionsField.jsx:115 +#: src/components/storage/PartitionsField.jsx:126 #, c-format msgid "Mount %1$s at %2$s (%3$s)" msgstr "%1$s unter %2$s (%3$s) einhängen" #. TRANSLATORS: This results in something like "Swap at /dev/sda3 (2 GiB)" since #. %1$s is replaced by the device name, and %2$s by the size -#: src/components/storage/PartitionsField.jsx:121 +#: src/components/storage/PartitionsField.jsx:132 #, c-format msgid "Swap at %1$s (%2$s)" msgstr "Auslagerung bei %1$s (%2$s)" #. TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:125 +#: src/components/storage/PartitionsField.jsx:136 #, c-format msgid "Swap volume (%s)" msgstr "Auslagerungsvolumen (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:127 +#: src/components/storage/PartitionsField.jsx:138 #, c-format msgid "Swap partition (%s)" msgstr "Auslagerungspartition (%s)" #. TRANSLATORS: This results in something like "Btrfs root at /dev/sda3 (20 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size -#: src/components/storage/PartitionsField.jsx:136 -#, c-format +#: src/components/storage/PartitionsField.jsx:147 +#, fuzzy, c-format msgid "%1$s root at %2$s (%3$s)" -msgstr "" +msgstr "Wurzel von %1$s bei %2$s (%3$s)" #. TRANSLATORS: "/" is in an LVM logical volume. #. Results in something like "Btrfs root volume (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:142 +#: src/components/storage/PartitionsField.jsx:153 #, fuzzy, c-format msgid "%1$s root volume (%2$s)" -msgstr "%1$s Wurzel-Volume (%2$s)" +msgstr "Wurzel-Volume von %1$s (%2$s)" #. TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:145 +#: src/components/storage/PartitionsField.jsx:156 #, fuzzy, c-format msgid "%1$s root partition (%2$s)" msgstr "%1$s Wurzelpartition (%2$s)" #. TRANSLATORS: This results in something like "Ext4 /home at /dev/sda3 (20 GiB)" since #. %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size -#: src/components/storage/PartitionsField.jsx:151 +#: src/components/storage/PartitionsField.jsx:162 #, c-format msgid "%1$s %2$s at %3$s (%4$s)" msgstr "%1$s %2$s unter %3$s (%4$s)" @@ -1535,145 +1494,141 @@ msgstr "%1$s %2$s unter %3$s (%4$s)" #. TRANSLATORS: The filesystem is in an LVM logical volume. #. Results in something like "Ext4 /home volume (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:157 +#: src/components/storage/PartitionsField.jsx:168 #, fuzzy, c-format msgid "%1$s %2$s volume (%3$s)" msgstr "Volume %1$s %2$s (%3$s)" #. TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:160 +#: src/components/storage/PartitionsField.jsx:171 #, c-format msgid "%1$s %2$s partition (%3$s)" msgstr "Partition %1$s %2$s (%3$s)" -#: src/components/storage/PartitionsField.jsx:172 +#: src/components/storage/PartitionsField.jsx:182 msgid "Do not configure partitions for booting" msgstr "Keine Partitionen zum Booten konfigurieren" -#: src/components/storage/PartitionsField.jsx:175 +#: src/components/storage/PartitionsField.jsx:184 msgid "Boot partitions at installation disk" msgstr "Boot-Partitionen auf der Installationsfestplatte" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/PartitionsField.jsx:178 +#: src/components/storage/PartitionsField.jsx:187 #, c-format msgid "Boot partitions at %s" msgstr "Boot-Partitionen auf %s" #. TRANSLATORS: header for a list of items referring to size limits for file systems -#: src/components/storage/PartitionsField.jsx:200 +#: src/components/storage/PartitionsField.jsx:209 msgid "These limits are affected by:" msgstr "Diese Einschränkungen werden beeinflusst durch:" #. TRANSLATORS: list item, this affects the computed partition size limits -#: src/components/storage/PartitionsField.jsx:204 +#: src/components/storage/PartitionsField.jsx:213 msgid "The configuration of snapshots" msgstr "Die Konfiguration der Schnappschüsse" -#. TRANSLATORS: list item, this affects the computed partition size limits -#. %s is replaced by a list of the volumes (like "/home, /boot") -#: src/components/storage/PartitionsField.jsx:208 +#: src/components/storage/PartitionsField.jsx:219 #, c-format msgid "Presence of other volumes (%s)" msgstr "Vorhandensein anderer Volumen (%s)" #. TRANSLATORS: list item, describes a factor that affects the computed size of a #. file system; eg. adjusting the size of the swap -#: src/components/storage/PartitionsField.jsx:212 +#: src/components/storage/PartitionsField.jsx:225 msgid "The amount of RAM in the system" msgstr "Die Größe des Arbeitsspeichers im System" -#. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/PartitionsField.jsx:263 +#: src/components/storage/PartitionsField.jsx:292 msgid "auto" msgstr "automatisch" #. TRANSLATORS: %s will be replaced by a file-system type like "Btrfs" or "Ext4" -#: src/components/storage/PartitionsField.jsx:279 +#: src/components/storage/PartitionsField.jsx:309 #, c-format msgid "Reused %s" msgstr "Wiederverwendetes %s" -#: src/components/storage/PartitionsField.jsx:281 +#: src/components/storage/PartitionsField.jsx:310 msgid "Transactional Btrfs" msgstr "Transaktionales Btrfs" -#: src/components/storage/PartitionsField.jsx:283 +#: src/components/storage/PartitionsField.jsx:311 msgid "Btrfs with snapshots" msgstr "Btrfs mit Schnappschüssen" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:297 +#: src/components/storage/PartitionsField.jsx:325 #, c-format msgid "Partition at %s" msgstr "Partition auf %s" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:300 +#: src/components/storage/PartitionsField.jsx:328 #, c-format msgid "Separate LVM at %s" msgstr "Separater LVM auf %s" -#: src/components/storage/PartitionsField.jsx:304 +#: src/components/storage/PartitionsField.jsx:331 #, fuzzy msgid "Logical volume at system LVM" msgstr "Logisches Volume im System-LVM" -#: src/components/storage/PartitionsField.jsx:306 +#: src/components/storage/PartitionsField.jsx:333 msgid "Partition at installation disk" msgstr "Partition auf der Installationsfestplatte" -#: src/components/storage/PartitionsField.jsx:321 +#: src/components/storage/PartitionsField.jsx:348 msgid "Reset location" msgstr "Ort zurücksetzen" -#: src/components/storage/PartitionsField.jsx:322 +#: src/components/storage/PartitionsField.jsx:349 msgid "Change location" msgstr "Ort ändern" -#: src/components/storage/PartitionsField.jsx:323 -#: src/components/storage/SpaceActionsTable.jsx:78 +#: src/components/storage/PartitionsField.jsx:350 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Löschen" -#: src/components/storage/PartitionsField.jsx:449 -#: src/components/storage/VolumeFields.jsx:61 -#: src/components/storage/VolumeFields.jsx:70 +#: src/components/storage/PartitionsField.jsx:486 +#: src/components/storage/VolumeFields.jsx:66 #: src/components/storage/VolumeFields.jsx:75 +#: src/components/storage/VolumeFields.jsx:80 msgid "Mount point" msgstr "Einhängepunkt" #. TRANSLATORS: where (and how) the file-system is going to be created -#: src/components/storage/PartitionsField.jsx:453 +#: src/components/storage/PartitionsField.jsx:490 msgid "Location" msgstr "Ort" -#: src/components/storage/PartitionsField.jsx:495 +#: src/components/storage/PartitionsField.jsx:532 msgid "Table with mount points" msgstr "Tabelle mit Einhängepunkten" -#: src/components/storage/PartitionsField.jsx:566 -#: src/components/storage/PartitionsField.jsx:585 -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/PartitionsField.jsx:604 +#: src/components/storage/PartitionsField.jsx:624 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Add file system" msgstr "Dateisystem hinzufügen" -#: src/components/storage/PartitionsField.jsx:596 +#: src/components/storage/PartitionsField.jsx:636 #, fuzzy msgid "Other" msgstr "Sonstige" -#: src/components/storage/PartitionsField.jsx:731 +#: src/components/storage/PartitionsField.jsx:777 msgid "Reset to defaults" msgstr "Auf Standardeinstellungen zurücksetzen" -#: src/components/storage/PartitionsField.jsx:801 +#: src/components/storage/PartitionsField.jsx:849 msgid "Partitions and file systems" msgstr "Partitionen und Dateisysteme" -#: src/components/storage/PartitionsField.jsx:802 +#: src/components/storage/PartitionsField.jsx:851 msgid "" "Structure of the new system, including any additional partition needed for " "booting" @@ -1681,20 +1636,18 @@ msgstr "" "Struktur des neuen Systems, einschließlich aller zusätzlichen Partitionen, " "die zum Booten benötigt werden" -#: src/components/storage/PartitionsField.jsx:808 +#: src/components/storage/PartitionsField.jsx:858 msgid "Show partitions and file-systems actions" msgstr "Partitionen- und Dateisystemaktionen anzeigen" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:62 +#: src/components/storage/ProposalActionsDialog.jsx:65 #, c-format msgid "Hide %d subvolume action" msgid_plural "Hide %d subvolume actions" msgstr[0] "" msgstr[1] "" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:64 +#: src/components/storage/ProposalActionsDialog.jsx:70 #, c-format msgid "Show %d subvolume action" msgid_plural "Show %d subvolume actions" @@ -1709,88 +1662,81 @@ msgstr "Destruktive Aktionen sind nicht erlaubt" msgid "Destructive actions are allowed" msgstr "Destruktive Aktionen sind erlaubt" -#: src/components/storage/ProposalActionsSummary.jsx:66 -#, c-format -msgid "There is %d destructive action planned" -msgid_plural "There are %d destructive actions planned" -msgstr[0] "Es ist %d zerstörerische Aktion geplant" -msgstr[1] "Es sind %d zerstörerische Aktionen geplant" - -#: src/components/storage/ProposalActionsSummary.jsx:79 -#: src/components/storage/ProposalActionsSummary.jsx:126 +#: src/components/storage/ProposalActionsSummary.jsx:82 +#: src/components/storage/ProposalActionsSummary.jsx:132 #, fuzzy msgid "affecting" msgstr "beeinflusst" -#: src/components/storage/ProposalActionsSummary.jsx:107 +#: src/components/storage/ProposalActionsSummary.jsx:112 msgid "Shrinking partitions is not allowed" msgstr "Verkleinern von Partitionen ist nicht erlaubt" -#: src/components/storage/ProposalActionsSummary.jsx:111 +#: src/components/storage/ProposalActionsSummary.jsx:116 msgid "Shrinking partitions is allowed" msgstr "Verkleinern von Partitionen ist erlaubt" -#: src/components/storage/ProposalActionsSummary.jsx:113 +#: src/components/storage/ProposalActionsSummary.jsx:118 msgid "Shrinking some partitions is allowed but not needed" msgstr "" "Das Verkleinern einiger Partitionen ist erlaubt, aber nicht erforderlich" -#: src/components/storage/ProposalActionsSummary.jsx:116 +#: src/components/storage/ProposalActionsSummary.jsx:121 #, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" msgstr[0] "%d Partition wird verkleinert" msgstr[1] "%d Partitionen werden verkleinert" -#: src/components/storage/ProposalActionsSummary.jsx:151 +#: src/components/storage/ProposalActionsSummary.jsx:159 msgid "Cannot accommodate the required file systems for installation" msgstr "" "Die für die Installation erforderlichen Dateisysteme können nicht " "untergebracht werden" -#: src/components/storage/ProposalActionsSummary.jsx:160 +#: src/components/storage/ProposalActionsSummary.jsx:167 #, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" msgstr[0] "Geplante Aktion überprüfen" msgstr[1] "Geplante %d Aktionen überprüfen" -#: src/components/storage/ProposalActionsSummary.jsx:179 +#: src/components/storage/ProposalActionsSummary.jsx:182 msgid "Waiting for actions information..." msgstr "Warten auf Informationen zu Aktionen ..." -#: src/components/storage/ProposalPage.jsx:314 +#: src/components/storage/ProposalPage.jsx:329 msgid "Planned Actions" msgstr "Geplante Aktionen" -#: src/components/storage/ProposalResultSection.jsx:42 +#: src/components/storage/ProposalResultSection.jsx:43 msgid "Waiting for information about storage configuration" msgstr "Warten auf Informationen zur Speicherkonfiguration" -#: src/components/storage/ProposalResultSection.jsx:70 +#: src/components/storage/ProposalResultSection.jsx:73 msgid "Final layout" msgstr "Endgültige Anordnung" -#: src/components/storage/ProposalResultSection.jsx:71 +#: src/components/storage/ProposalResultSection.jsx:74 msgid "The systems will be configured as displayed below." msgstr "Die Systeme werden wie unten dargestellt konfiguriert." -#: src/components/storage/ProposalResultSection.jsx:78 +#: src/components/storage/ProposalResultSection.jsx:83 msgid "Storage proposal not possible" msgstr "Speichervorschlag nicht möglich" -#: src/components/storage/ProposalResultTable.jsx:74 +#: src/components/storage/ProposalResultTable.jsx:79 msgid "New" msgstr "Neu" #. TRANSLATORS: Label to indicate the device size before resizing, where %s is #. replaced by the original size (e.g., 3.00 GiB). -#: src/components/storage/ProposalResultTable.jsx:98 +#: src/components/storage/ProposalResultTable.jsx:105 #, c-format msgid "Before %s" msgstr "Vor %s" -#: src/components/storage/ProposalResultTable.jsx:121 +#: src/components/storage/ProposalResultTable.jsx:131 msgid "Mount Point" msgstr "Einhängepunkt" @@ -1798,7 +1744,7 @@ msgstr "Einhängepunkt" msgid "Transactional root file system" msgstr "Transaktionales Wurzeldateisystem" -#: src/components/storage/ProposalTransactionalInfo.jsx:48 +#: src/components/storage/ProposalTransactionalInfo.jsx:49 #, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " @@ -1812,7 +1758,7 @@ msgstr "" msgid "Use Btrfs snapshots for the root file system" msgstr "Btrfs-Schnappschüsse für das Wurzeldateisystem verwenden" -#: src/components/storage/SnapshotsField.jsx:37 +#: src/components/storage/SnapshotsField.jsx:38 msgid "" "Allows to boot to a previous version of the system after configuration " "changes or software upgrades." @@ -1820,63 +1766,57 @@ msgstr "" "Ermöglicht das Booten zu einer früheren Version des Systems nach " "Konfigurationsänderungen oder Softwareaktualisierungen." -#. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) -#: src/components/storage/SpaceActionsTable.jsx:75 -#, c-format, fuzzy -msgid "Space action selector for %s" -msgstr "Speicherplatz-Aktionsselektor für %s" +#: src/components/storage/SpaceActionsTable.jsx:68 +#, c-format +msgid "Up to %s can be recovered by shrinking the device." +msgstr "Bis zu %s können durch Verkleinern des Geräts zurückgewonnen werden." -#: src/components/storage/SpaceActionsTable.jsx:79 -msgid "Allow resize" -msgstr "Größenänderung erlauben" +#: src/components/storage/SpaceActionsTable.jsx:77 +msgid "The device cannot be shrunk:" +msgstr "Das Gerät kann nicht verkleinert werden:" -#: src/components/storage/SpaceActionsTable.jsx:80 -msgid "Do not modify" -msgstr "Nicht verändern" +#: src/components/storage/SpaceActionsTable.jsx:98 +#, c-format +msgid "Show information about %s" +msgstr "Informationen über %s anzeigen" -#: src/components/storage/SpaceActionsTable.jsx:111 +#: src/components/storage/SpaceActionsTable.jsx:172 msgid "The content may be deleted" msgstr "Der Inhalt kann gelöscht werden" -#: src/components/storage/SpaceActionsTable.jsx:144 -msgid "Shrinkable" -msgstr "Verkleinerbar" - -#: src/components/storage/SpaceActionsTable.jsx:146 +#: src/components/storage/SpaceActionsTable.jsx:204 msgid "Action" msgstr "Aktion" -#: src/components/storage/SpaceActionsTable.jsx:162 +#: src/components/storage/SpaceActionsTable.jsx:215 msgid "Actions to find space" msgstr "Aktionen, um Platz zu finden" -#: src/components/storage/SpacePolicySelection.jsx:170 +#: src/components/storage/SpacePolicySelection.jsx:172 #, fuzzy msgid "Space policy" msgstr "Speicherplatzrichtlinie" -#: src/components/storage/VolumeDialog.jsx:78 +#: src/components/storage/VolumeDialog.jsx:83 #, c-format msgid "Add %s file system" msgstr "Dateisystem %s hinzufügen" -#: src/components/storage/VolumeDialog.jsx:79 +#: src/components/storage/VolumeDialog.jsx:84 #, c-format msgid "Edit %s file system" msgstr "Dateisystem %s bearbeiten" -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Edit file system" msgstr "Dateisystem bearbeiten" #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:96 +#: src/components/storage/VolumeDialog.jsx:101 msgid "The type and size of the file system cannot be edited." msgstr "Der Typ und die Größe des Dateisystems können nicht bearbeitet werden." -#. TRANSLATORS: Description of a warning. The first %s is replaced by a device name (e.g., -#. /dev/vda) and the second %s is replaced by a mount path (e.g., /home). -#: src/components/storage/VolumeDialog.jsx:99 +#: src/components/storage/VolumeDialog.jsx:105 #, c-format msgid "The current file system on %s is selected to be mounted at %s." msgstr "" @@ -1884,56 +1824,55 @@ msgstr "" "werden." #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:105 +#: src/components/storage/VolumeDialog.jsx:113 msgid "The size of the file system cannot be edited" msgstr "Die Größe des Dateisystems kann nicht bearbeitet werden" #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). -#: src/components/storage/VolumeDialog.jsx:107 +#: src/components/storage/VolumeDialog.jsx:115 #, c-format msgid "The file system is allocated at the device %s." msgstr "Das Dateisystem ist dem Gerät %s zugewiesen." -#: src/components/storage/VolumeDialog.jsx:152 +#: src/components/storage/VolumeDialog.jsx:163 msgid "A mount point is required" msgstr "Ein Einhängepunkt ist erforderlich" -#: src/components/storage/VolumeDialog.jsx:179 +#: src/components/storage/VolumeDialog.jsx:190 msgid "The mount point is invalid" msgstr "Der Einhängepunkt ist ungültig" -#: src/components/storage/VolumeDialog.jsx:207 +#: src/components/storage/VolumeDialog.jsx:218 msgid "A size value is required" msgstr "Ein Größenwert ist erforderlich" -#: src/components/storage/VolumeDialog.jsx:235 +#: src/components/storage/VolumeDialog.jsx:246 msgid "Minimum size is required" msgstr "Mindestgröße ist erforderlich" -#: src/components/storage/VolumeDialog.jsx:267 +#: src/components/storage/VolumeDialog.jsx:278 msgid "Maximum must be greater than minimum" msgstr "Das Maximum muss größer sein als das Minimum" -#: src/components/storage/VolumeDialog.jsx:309 +#: src/components/storage/VolumeDialog.jsx:320 #, c-format msgid "There is already a file system for %s." msgstr "Es gibt bereits ein Dateisystem für %s." -#: src/components/storage/VolumeDialog.jsx:311 +#: src/components/storage/VolumeDialog.jsx:322 msgid "Do you want to edit it?" msgstr "Möchten Sie es bearbeiten?" -#: src/components/storage/VolumeDialog.jsx:356 +#: src/components/storage/VolumeDialog.jsx:367 #, c-format msgid "There is a predefined file system for %s." msgstr "Es gibt ein vordefiniertes Dateisystem für %s." -#: src/components/storage/VolumeDialog.jsx:358 +#: src/components/storage/VolumeDialog.jsx:369 msgid "Do you want to add it?" msgstr "Möchten Sie es hinzufügen?" -#. TRANSLATORS: info about possible file system types. -#: src/components/storage/VolumeFields.jsx:217 +#: src/components/storage/VolumeFields.jsx:225 msgid "" "The options for the file system type depends on the product and the mount " "point." @@ -1941,68 +1880,64 @@ msgstr "" "Die Optionen für den Dateisystemtyp hängen vom Produkt und dem Einhängepunkt " "ab." -#: src/components/storage/VolumeFields.jsx:223 +#: src/components/storage/VolumeFields.jsx:232 msgid "More info for file system types" msgstr "Weitere Informationen zu Dateisystemtypen" #. TRANSLATORS: label for the file system selector. -#: src/components/storage/VolumeFields.jsx:234 +#: src/components/storage/VolumeFields.jsx:243 msgid "File system type" msgstr "Dateisystemtyp" #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:265 +#: src/components/storage/VolumeFields.jsx:274 msgid "the configuration of snapshots" msgstr "die Konfiguration von Schnappschüssen" -#. TRANSLATORS: item which affects the final computed partition size -#. %s is replaced by a list of mount points like "/home, /boot" -#: src/components/storage/VolumeFields.jsx:270 +#: src/components/storage/VolumeFields.jsx:281 #, c-format msgid "the presence of the file system for %s" msgstr "das Vorhandensein des Dateisystems für %s" #. TRANSLATORS: conjunction for merging two list items -#: src/components/storage/VolumeFields.jsx:272 +#: src/components/storage/VolumeFields.jsx:283 msgid ", " msgstr ", " #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:276 +#: src/components/storage/VolumeFields.jsx:289 msgid "the amount of RAM in the system" msgstr "die Größe des Arbeitsspeichers im System" -#. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeFields.jsx:279 +#: src/components/storage/VolumeFields.jsx:293 #, c-format msgid "The final size depends on %s." msgstr "Die endgültige Größe hängt von %s ab." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeFields.jsx:281 +#: src/components/storage/VolumeFields.jsx:295 msgid " and " msgstr " und " -#. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeFields.jsx:286 +#: src/components/storage/VolumeFields.jsx:302 msgid "Automatically calculated size according to the selected product." msgstr "Automatisch berechnete Größe entsprechend dem ausgewählten Produkt." -#: src/components/storage/VolumeFields.jsx:305 +#: src/components/storage/VolumeFields.jsx:321 msgid "Exact size for the file system." msgstr "Exakte Größe des Dateisystems." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeFields.jsx:318 +#: src/components/storage/VolumeFields.jsx:330 msgid "Exact size" msgstr "Exakte Größe" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeFields.jsx:335 +#: src/components/storage/VolumeFields.jsx:347 msgid "Size unit" msgstr "Größeneinheit" -#: src/components/storage/VolumeFields.jsx:363 +#: src/components/storage/VolumeFields.jsx:376 msgid "" "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 " @@ -2013,50 +1948,49 @@ msgstr "" "angegeben wird, wird das Dateisystem so groß wie möglich." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeFields.jsx:370 +#: src/components/storage/VolumeFields.jsx:384 msgid "Minimum" msgstr "Minimum" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeFields.jsx:381 +#: src/components/storage/VolumeFields.jsx:395 msgid "Minimum desired size" msgstr "Gewünschte Mindestgröße" -#: src/components/storage/VolumeFields.jsx:392 +#: src/components/storage/VolumeFields.jsx:406 msgid "Unit for the minimum size" msgstr "Einheit für die Mindestgröße" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:404 +#: src/components/storage/VolumeFields.jsx:418 msgid "Maximum" msgstr "Maximum" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:416 +#: src/components/storage/VolumeFields.jsx:430 msgid "Maximum desired size" msgstr "Gewünschte Maximalgröße" -#: src/components/storage/VolumeFields.jsx:426 +#: src/components/storage/VolumeFields.jsx:440 msgid "Unit for the maximum size" msgstr "Einheit für die Maximalgröße" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeFields.jsx:444 +#: src/components/storage/VolumeFields.jsx:458 msgid "Auto" msgstr "Automatisch" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeFields.jsx:446 +#: src/components/storage/VolumeFields.jsx:460 msgid "Fixed" msgstr "Unveränderbar" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeFields.jsx:448 +#: src/components/storage/VolumeFields.jsx:462 msgid "Range" msgstr "Bereich" -#. TRANSLATORS: Description of the dialog for changing the location of a file system. -#: src/components/storage/VolumeLocationDialog.jsx:40 +#: src/components/storage/VolumeLocationDialog.jsx:41 msgid "" "The file systems are allocated at the installation device by default. " "Indicate a custom location to create the file system at a specific device." @@ -2067,29 +2001,29 @@ msgstr "" #. TRANSLATORS: Title of the dialog for changing the location of a file system. %s is replaced #. by a mount path (e.g., /home). -#: src/components/storage/VolumeLocationDialog.jsx:135 +#: src/components/storage/VolumeLocationDialog.jsx:137 #, c-format msgid "Location for %s file system" msgstr "Speicherort für Dateisystem %s" -#: src/components/storage/VolumeLocationDialog.jsx:145 +#: src/components/storage/VolumeLocationDialog.jsx:147 msgid "Select in which device to allocate the file system" msgstr "" "Wählen Sie aus, auf welchem Gerät das Dateisystem zugewiesen werden soll" -#: src/components/storage/VolumeLocationDialog.jsx:148 +#: src/components/storage/VolumeLocationDialog.jsx:150 msgid "Select a location" msgstr "Ort auswählen" -#: src/components/storage/VolumeLocationDialog.jsx:160 +#: src/components/storage/VolumeLocationDialog.jsx:162 msgid "Select how to allocate the file system" msgstr "Wählen Sie aus, wie das Dateisystem zugewiesen werden soll" -#: src/components/storage/VolumeLocationDialog.jsx:165 +#: src/components/storage/VolumeLocationDialog.jsx:167 msgid "Create a new partition" msgstr "Eine neue Partition erstellen" -#: src/components/storage/VolumeLocationDialog.jsx:166 +#: src/components/storage/VolumeLocationDialog.jsx:169 #, fuzzy msgid "" "The file system will be allocated as a new partition at the selected disk." @@ -2097,12 +2031,12 @@ msgstr "" "Das Dateisystem wird als neue Partition auf der ausgewählten Festplatte " "zugewiesen." -#: src/components/storage/VolumeLocationDialog.jsx:175 +#: src/components/storage/VolumeLocationDialog.jsx:179 #, fuzzy msgid "Create a dedicated LVM volume group" msgstr "Dedizierte LVM-Volume-Gruppe erstellen" -#: src/components/storage/VolumeLocationDialog.jsx:176 +#: src/components/storage/VolumeLocationDialog.jsx:181 #, fuzzy msgid "" "A new volume group will be allocated in the selected disk and the file " @@ -2111,21 +2045,20 @@ msgstr "" "Eine neue Volume-Gruppe wird auf der ausgewählten Festplatte zugewiesen und " "das Dateisystem wird als logisches Volume erstellt." -#: src/components/storage/VolumeLocationDialog.jsx:185 +#: src/components/storage/VolumeLocationDialog.jsx:191 msgid "Format the device" msgstr "Gerät formatieren" -#. TRANSLATORS: %s is replaced by a file system type (e.g., Ext4). -#: src/components/storage/VolumeLocationDialog.jsx:188 +#: src/components/storage/VolumeLocationDialog.jsx:195 #, c-format msgid "The selected device will be formatted as %s file system." msgstr "Das ausgewählte Gerät wird als Dateisystem %s formatiert." -#: src/components/storage/VolumeLocationDialog.jsx:198 +#: src/components/storage/VolumeLocationDialog.jsx:206 msgid "Mount the file system" msgstr "Dateisystem einhängen" -#: src/components/storage/VolumeLocationDialog.jsx:199 +#: src/components/storage/VolumeLocationDialog.jsx:208 msgid "" "The current file system on the selected device will be mounted without " "formatting the device." @@ -2133,98 +2066,93 @@ msgstr "" "Das aktuelle Dateisystem auf dem ausgewählten Gerät wird eingehängt, ohne " "das Gerät zu formatieren." -#: src/components/storage/VolumeLocationSelectorTable.jsx:102 +#: src/components/storage/VolumeLocationSelectorTable.jsx:110 #, fuzzy msgid "Usage" msgstr "Belegung" -#: src/components/storage/ZFCPDiskForm.jsx:109 +#: src/components/storage/ZFCPDiskForm.jsx:106 msgid "The zFCP disk was not activated." msgstr "Die zFCP-Festplatte wurde nicht aktiviert." #. TRANSLATORS: abbrev. World Wide Port Name #: src/components/storage/ZFCPDiskForm.jsx:123 -#: src/components/storage/ZFCPPage.jsx:363 +#: src/components/storage/ZFCPPage.jsx:383 msgid "WWPN" msgstr "WWPN" #. TRANSLATORS: abbrev. Logical Unit Number -#: src/components/storage/ZFCPDiskForm.jsx:134 -#: src/components/storage/ZFCPPage.jsx:364 +#: src/components/storage/ZFCPDiskForm.jsx:131 +#: src/components/storage/ZFCPPage.jsx:384 msgid "LUN" msgstr "LUN" -#: src/components/storage/ZFCPPage.jsx:304 +#: src/components/storage/ZFCPPage.jsx:326 msgid "Auto LUNs Scan" msgstr "" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Activated" msgstr "Aktiviert" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Deactivated" msgstr "Deaktiviert" -#: src/components/storage/ZFCPPage.jsx:418 +#: src/components/storage/ZFCPPage.jsx:437 msgid "No zFCP controllers found." msgstr "Keine zFCP-Controller gefunden." -#: src/components/storage/ZFCPPage.jsx:419 +#: src/components/storage/ZFCPPage.jsx:438 msgid "Please, try to read the zFCP devices again." msgstr "Bitte versuchen Sie, die zFCP-Geräte erneut einzulesen." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:421 +#: src/components/storage/ZFCPPage.jsx:441 msgid "Read zFCP devices" msgstr "zFCP-Geräte lesen" -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:430 +#: src/components/storage/ZFCPPage.jsx:452 msgid "" "Automatic LUN scan is [enabled]. Activating a controller which is running in " "NPIV mode will automatically configures all its LUNs." msgstr "" -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:433 +#: src/components/storage/ZFCPPage.jsx:457 msgid "" "Automatic LUN scan is [disabled]. LUNs have to be manually configured after " "activating a controller." msgstr "" -#: src/components/storage/ZFCPPage.jsx:490 +#: src/components/storage/ZFCPPage.jsx:519 msgid "Activate a zFCP disk" msgstr "zFCP-Festplatte aktivieren" -#: src/components/storage/ZFCPPage.jsx:529 +#: src/components/storage/ZFCPPage.jsx:553 msgid "Please, try to activate a zFCP controller." msgstr "Bitte versuchen Sie, einen zFCP-Controller zu aktivieren." -#: src/components/storage/ZFCPPage.jsx:536 +#: src/components/storage/ZFCPPage.jsx:559 msgid "Please, try to activate a zFCP disk." msgstr "Bitte versuchen Sie, eine zFCP-Festplatte zu aktivieren." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:538 +#: src/components/storage/ZFCPPage.jsx:562 msgid "Activate zFCP disk" msgstr "zFCP-Festplatte aktivieren" -#: src/components/storage/ZFCPPage.jsx:545 +#: src/components/storage/ZFCPPage.jsx:570 msgid "No zFCP disks found." msgstr "Keine zFCP-Festplatten gefunden." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:560 +#: src/components/storage/ZFCPPage.jsx:586 msgid "Activate new disk" msgstr "Neue Festplatte aktivieren" #. TRANSLATORS: section title -#: src/components/storage/ZFCPPage.jsx:572 +#: src/components/storage/ZFCPPage.jsx:599 msgid "Disks" msgstr "Festplatten" -#: src/components/storage/device-utils.jsx:88 +#: src/components/storage/device-utils.jsx:92 msgid "Unused space" msgstr "Ungenutzter Platz" @@ -2237,70 +2165,70 @@ msgstr "Nur verfügbar, wenn Authentifizierung durch das Ziel angeboten wird" msgid "Authentication by target" msgstr "Authentifizierung durch das Ziel" -#: src/components/storage/iscsi/AuthFields.jsx:80 -#: src/components/storage/iscsi/AuthFields.jsx:85 -#: src/components/storage/iscsi/AuthFields.jsx:87 -#: src/components/storage/iscsi/AuthFields.jsx:112 -#: src/components/storage/iscsi/AuthFields.jsx:117 -#: src/components/storage/iscsi/AuthFields.jsx:119 +#: src/components/storage/iscsi/AuthFields.jsx:78 +#: src/components/storage/iscsi/AuthFields.jsx:82 +#: src/components/storage/iscsi/AuthFields.jsx:84 +#: src/components/storage/iscsi/AuthFields.jsx:104 +#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:110 msgid "User name" msgstr "Benutzername" -#: src/components/storage/iscsi/AuthFields.jsx:91 -#: src/components/storage/iscsi/AuthFields.jsx:124 +#: src/components/storage/iscsi/AuthFields.jsx:88 +#: src/components/storage/iscsi/AuthFields.jsx:116 msgid "Incorrect user name" msgstr "Falscher Benutzername" -#: src/components/storage/iscsi/AuthFields.jsx:105 -#: src/components/storage/iscsi/AuthFields.jsx:139 +#: src/components/storage/iscsi/AuthFields.jsx:99 +#: src/components/storage/iscsi/AuthFields.jsx:130 msgid "Incorrect password" msgstr "Falsches Passwort" -#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:102 msgid "Authentication by initiator" msgstr "Authentifizierung durch den Initiator" -#: src/components/storage/iscsi/AuthFields.jsx:133 +#: src/components/storage/iscsi/AuthFields.jsx:123 msgid "Target Password" msgstr "Ziel-Passwort" #. TRANSLATORS: popup title -#: src/components/storage/iscsi/DiscoverForm.jsx:102 +#: src/components/storage/iscsi/DiscoverForm.jsx:94 msgid "Discover iSCSI Targets" msgstr "iSCSI-Ziele erkennen" -#: src/components/storage/iscsi/DiscoverForm.jsx:112 -#: src/components/storage/iscsi/LoginForm.jsx:73 +#: src/components/storage/iscsi/DiscoverForm.jsx:99 +#: src/components/storage/iscsi/LoginForm.jsx:70 msgid "Make sure you provide the correct values" msgstr "Stellen Sie sicher, dass Sie die richtigen Werte angeben" -#: src/components/storage/iscsi/DiscoverForm.jsx:118 +#: src/components/storage/iscsi/DiscoverForm.jsx:103 msgid "IP address" msgstr "IP-Adresse" #. TRANSLATORS: network address -#: src/components/storage/iscsi/DiscoverForm.jsx:125 -#: src/components/storage/iscsi/DiscoverForm.jsx:127 +#: src/components/storage/iscsi/DiscoverForm.jsx:108 +#: src/components/storage/iscsi/DiscoverForm.jsx:110 msgid "Address" msgstr "Adresse" -#: src/components/storage/iscsi/DiscoverForm.jsx:132 +#: src/components/storage/iscsi/DiscoverForm.jsx:115 msgid "Incorrect IP address" msgstr "Falsche IP-Adresse" #. TRANSLATORS: network port number -#: src/components/storage/iscsi/DiscoverForm.jsx:136 -#: src/components/storage/iscsi/DiscoverForm.jsx:143 -#: src/components/storage/iscsi/DiscoverForm.jsx:145 +#: src/components/storage/iscsi/DiscoverForm.jsx:117 +#: src/components/storage/iscsi/DiscoverForm.jsx:122 +#: src/components/storage/iscsi/DiscoverForm.jsx:124 msgid "Port" msgstr "Port" -#: src/components/storage/iscsi/DiscoverForm.jsx:150 +#: src/components/storage/iscsi/DiscoverForm.jsx:129 msgid "Incorrect port" msgstr "Falscher Port" #. TRANSLATORS: %s is replaced by the iSCSI target node name -#: src/components/storage/iscsi/EditNodeForm.jsx:50 +#: src/components/storage/iscsi/EditNodeForm.jsx:48 #, c-format msgid "Edit %s" msgstr "%s bearbeiten" @@ -2318,8 +2246,8 @@ msgstr "Name des Initiators" #. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 #: src/components/storage/iscsi/InitiatorPresenter.jsx:86 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 -#: src/components/storage/iscsi/NodesPresenter.jsx:124 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 +#: src/components/storage/iscsi/NodesPresenter.jsx:122 msgid "iBFT" msgstr "iBFT" @@ -2335,14 +2263,14 @@ msgid "Initiator" msgstr "Initiator" #. TRANSLATORS: %s is replaced by the iSCSI target name -#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/storage/iscsi/LoginForm.jsx:66 #, c-format msgid "Login %s" msgstr "" #. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) -#: src/components/storage/iscsi/LoginForm.jsx:76 -#: src/components/storage/iscsi/LoginForm.jsx:79 +#: src/components/storage/iscsi/LoginForm.jsx:74 +#: src/components/storage/iscsi/LoginForm.jsx:77 #, fuzzy msgid "Startup" msgstr "Inbetriebnahme" @@ -2365,29 +2293,28 @@ msgstr "Anmelden" msgid "Logout" msgstr "Abmelden" -#: src/components/storage/iscsi/NodesPresenter.jsx:101 -#: src/components/storage/iscsi/NodesPresenter.jsx:122 +#: src/components/storage/iscsi/NodesPresenter.jsx:99 +#: src/components/storage/iscsi/NodesPresenter.jsx:120 msgid "Portal" msgstr "Portal" -#: src/components/storage/iscsi/NodesPresenter.jsx:102 -#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/storage/iscsi/NodesPresenter.jsx:100 +#: src/components/storage/iscsi/NodesPresenter.jsx:121 msgid "Interface" msgstr "Schnittstelle" -#: src/components/storage/iscsi/TargetsSection.jsx:142 +#: src/components/storage/iscsi/TargetsSection.jsx:138 msgid "No iSCSI targets found." msgstr "Keine iSCSI-Ziele gefunden." -#: src/components/storage/iscsi/TargetsSection.jsx:143 +#: src/components/storage/iscsi/TargetsSection.jsx:140 msgid "" "Please, perform an iSCSI discovery in order to find available iSCSI targets." msgstr "" "Bitte führen Sie eine iSCSI-Erkennung durch, um verfügbare iSCSI-Ziele zu " "finden." -#. TRANSLATORS: button label, starts iSCSI discovery -#: src/components/storage/iscsi/TargetsSection.jsx:145 +#: src/components/storage/iscsi/TargetsSection.jsx:144 msgid "Discover iSCSI targets" msgstr "iSCSI-Ziele erkennen" @@ -2397,7 +2324,7 @@ msgid "Discover" msgstr "Erkennen" #. TRANSLATORS: iSCSI targets section title -#: src/components/storage/iscsi/TargetsSection.jsx:170 +#: src/components/storage/iscsi/TargetsSection.jsx:167 msgid "Targets" msgstr "Ziele" @@ -2492,7 +2419,7 @@ msgstr "mit benutzerdefinierten Aktionen" msgid "No user defined yet." msgstr "Noch kein Benutzer definiert." -#: src/components/users/FirstUser.jsx:38 +#: src/components/users/FirstUser.jsx:39 msgid "" "Please, be aware that a user must be defined before installing the system to " "be able to log into it." @@ -2500,68 +2427,68 @@ msgstr "" "Bitte beachten Sie, dass vor der Installation des Systems ein Benutzer " "definiert werden muss, um sich am System anmelden zu können." -#: src/components/users/FirstUser.jsx:42 +#: src/components/users/FirstUser.jsx:45 msgid "Define a user now" msgstr "Definieren Sie jetzt einen Benutzer" -#: src/components/users/FirstUser.jsx:54 -#: src/components/users/FirstUserForm.jsx:210 +#: src/components/users/FirstUser.jsx:58 +#: src/components/users/FirstUserForm.jsx:227 msgid "Full name" msgstr "Vollständiger Name" -#: src/components/users/FirstUser.jsx:55 -#: src/components/users/FirstUserForm.jsx:224 -#: src/components/users/FirstUserForm.jsx:229 -#: src/components/users/FirstUserForm.jsx:232 +#: src/components/users/FirstUser.jsx:59 +#: src/components/users/FirstUserForm.jsx:241 +#: src/components/users/FirstUserForm.jsx:246 +#: src/components/users/FirstUserForm.jsx:249 msgid "Username" msgstr "Benutzername" -#: src/components/users/FirstUser.jsx:120 -#: src/components/users/RootAuthMethods.jsx:99 -#: src/components/users/RootAuthMethods.jsx:111 +#: src/components/users/FirstUser.jsx:124 +#: src/components/users/RootAuthMethods.jsx:104 +#: src/components/users/RootAuthMethods.jsx:116 msgid "Discard" msgstr "Verwerfen" -#: src/components/users/FirstUserForm.jsx:46 +#: src/components/users/FirstUserForm.jsx:57 msgid "Username suggestion dropdown" msgstr "Dropdown-Liste mit Vorschlägen für Benutzernamen" #. TRANSLATORS: dropdown username suggestions -#: src/components/users/FirstUserForm.jsx:61 +#: src/components/users/FirstUserForm.jsx:72 msgid "Use suggested username" msgstr "Vorgeschlagenen Benutzernamen verwenden" -#: src/components/users/FirstUserForm.jsx:140 +#: src/components/users/FirstUserForm.jsx:151 msgid "All fields are required" msgstr "Alle Felder sind erforderlich" -#: src/components/users/FirstUserForm.jsx:147 +#: src/components/users/FirstUserForm.jsx:158 msgid "Please, try again." msgstr "Bitte versuchen Sie es erneut." -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Create user" msgstr "Benutzer erstellen" -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Edit user" msgstr "Benutzer bearbeiten" -#: src/components/users/FirstUserForm.jsx:214 -#: src/components/users/FirstUserForm.jsx:216 +#: src/components/users/FirstUserForm.jsx:231 +#: src/components/users/FirstUserForm.jsx:233 msgid "User full name" msgstr "Vollständiger Name des Benutzers" -#: src/components/users/FirstUserForm.jsx:254 +#: src/components/users/FirstUserForm.jsx:271 msgid "Edit password too" msgstr "Auch Passwort bearbeiten" -#: src/components/users/FirstUserForm.jsx:269 +#: src/components/users/FirstUserForm.jsx:287 msgid "user autologin" msgstr "Automatische Benutzeranmeldung" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:273 +#: src/components/users/FirstUserForm.jsx:291 msgid "Auto-login" msgstr "Automatisches Anmelden" @@ -2569,7 +2496,7 @@ msgstr "Automatisches Anmelden" msgid "No root authentication method defined yet." msgstr "Noch keine Root-Authentifizierungsmethode definiert." -#: src/components/users/RootAuthMethods.jsx:38 +#: src/components/users/RootAuthMethods.jsx:39 msgid "" "Please, define at least one authentication method for logging into the " "system as root." @@ -2577,56 +2504,54 @@ msgstr "" "Bitte definieren Sie mindestens eine Authentifizierungsmethode, um sich als " "root am System anmelden zu können." -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:43 +#: src/components/users/RootAuthMethods.jsx:46 msgid "Set a password" msgstr "Passwort festlegen" -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:45 +#: src/components/users/RootAuthMethods.jsx:50 msgid "Upload a SSH Public Key" msgstr "Öffentlichen SSH-Schlüssel hochladen" -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Set" msgstr "Festlegen" -#: src/components/users/RootAuthMethods.jsx:129 +#: src/components/users/RootAuthMethods.jsx:132 msgid "Already set" msgstr "Bereits festgelegt" -#: src/components/users/RootAuthMethods.jsx:130 -#: src/components/users/RootAuthMethods.jsx:134 +#: src/components/users/RootAuthMethods.jsx:132 +#: src/components/users/RootAuthMethods.jsx:136 msgid "Not set" msgstr "Nicht festgelegt" #. TRANSLATORS: table header, user authentication method -#: src/components/users/RootAuthMethods.jsx:155 +#: src/components/users/RootAuthMethods.jsx:157 msgid "Method" msgstr "Methode" -#: src/components/users/RootAuthMethods.jsx:170 +#: src/components/users/RootAuthMethods.jsx:174 msgid "SSH Key" msgstr "SSH-Schlüssel" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Change the root password" msgstr "Root-Passwort ändern" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Set a root password" msgstr "Root-Passwort festlegen" -#: src/components/users/RootAuthMethods.jsx:194 -msgid "Add a SSH Public Key for root" -msgstr "Öffentlichen SSH-Schlüssel für root hinzufügen" - -#: src/components/users/RootAuthMethods.jsx:194 +#: src/components/users/RootAuthMethods.jsx:203 msgid "Edit the SSH Public Key for root" msgstr "Öffentlichen SSH-Schlüssel für root bearbeiten" -#: src/components/users/RootPasswordPopup.jsx:42 +#: src/components/users/RootAuthMethods.jsx:204 +msgid "Add a SSH Public Key for root" +msgstr "Öffentlichen SSH-Schlüssel für root hinzufügen" + +#: src/components/users/RootPasswordPopup.jsx:43 msgid "Root password" msgstr "Root-Passwort" @@ -2660,6 +2585,37 @@ msgstr "Erster Benutzer" msgid "Root authentication" msgstr "Root-Authentifizierung" +#~ msgid "Reading file..." +#~ msgstr "Datei wird gelesen ..." + +#~ msgid "Cannot read the file" +#~ msgstr "Die Datei kann nicht gelesen werden" + +#~ msgid "Agama Error" +#~ msgstr "Agama-Fehler" + +#~ msgid "Loading available products, please wait..." +#~ msgstr "Verfügbare Produkte werden geladen, bitte warten ..." + +#, c-format +#~ msgid "There is %d destructive action planned" +#~ msgid_plural "There are %d destructive actions planned" +#~ msgstr[0] "Es ist %d zerstörerische Aktion geplant" +#~ msgstr[1] "Es sind %d zerstörerische Aktionen geplant" + +#, fuzzy, c-format +#~ msgid "Space action selector for %s" +#~ msgstr "Speicherplatz-Aktionsselektor für %s" + +#~ msgid "Allow resize" +#~ msgstr "Größenänderung erlauben" + +#~ msgid "Do not modify" +#~ msgstr "Nicht verändern" + +#~ msgid "Shrinkable" +#~ msgstr "Verkleinerbar" + #~ msgid "Choose a language" #~ msgstr "Wählen Sie eine Sprache aus" @@ -3021,9 +2977,6 @@ msgstr "Root-Authentifizierung" #~ msgid "Waiting for information about selected device" #~ msgstr "Warten auf Informationen über das ausgewählte Gerät" -#~ msgid "Waiting for information about space policy" -#~ msgstr "Warten auf Informationen über Speicherplatzrichtlinie" - #, c-format #~ msgid "" #~ "The filesystem will be allocated as a new partition at the installation " diff --git a/web/po/es.po b/web/po/es.po index 9f01eecdf3..0afdad92c1 100644 --- a/web/po/es.po +++ b/web/po/es.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-30 02:27+0000\n" -"PO-Revision-Date: 2024-07-02 11:46+0000\n" +"POT-Creation-Date: 2024-07-14 02:32+0000\n" +"PO-Revision-Date: 2024-07-10 20:46+0000\n" "Last-Translator: Victor hck \n" "Language-Team: Spanish \n" @@ -19,11 +19,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.6.2\n" -#: src/MainLayout.jsx:40 +#: src/MainLayout.jsx:52 msgid "Agama" msgstr "Agama" -#: src/MainLayout.jsx:82 +#: src/MainLayout.jsx:94 msgid "Change product" msgstr "Cambiar de producto" @@ -31,12 +31,11 @@ msgstr "Cambiar de producto" msgid "About" msgstr "Acerca de" -#: src/components/core/About.jsx:71 +#: src/components/core/About.jsx:69 msgid "About Agama" msgstr "Acerca de Agama" -#. TRANSLATORS: content of the "About" popup (1/2) -#: src/components/core/About.jsx:76 +#: src/components/core/About.jsx:74 msgid "" "Agama is an experimental installer for (open)SUSE systems. It is still under " "development so, please, do not use it in production environments. If you " @@ -50,26 +49,17 @@ msgstr "" #. TRANSLATORS: content of the "About" popup (2/2) #. %s is replaced by the project URL -#: src/components/core/About.jsx:88 +#: src/components/core/About.jsx:86 #, c-format msgid "For more information, please visit the project's repository at %s." msgstr "" "Para obtener más información, visite el repositorio del proyecto en %s." -#: src/components/core/About.jsx:94 src/components/core/FileViewer.jsx:81 -#: src/components/core/LogsButton.jsx:123 -#: src/components/software/SoftwarePatternsSelection.jsx:260 +#: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 +#: src/components/software/SoftwarePatternsSelection.jsx:268 msgid "Close" msgstr "Cerrar" -#: src/components/core/FileViewer.jsx:66 -msgid "Reading file..." -msgstr "Leyendo el archivo..." - -#: src/components/core/FileViewer.jsx:72 -msgid "Cannot read the file" -msgstr "No se puede leer el archivo" - #: src/components/core/InstallButton.jsx:32 msgid "Confirm Installation" msgstr "Confirmar instalación" @@ -92,9 +82,9 @@ msgid "Continue" msgstr "Continuar" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:97 -#: src/components/core/Popup.jsx:136 -#: src/components/network/WifiConnectionForm.jsx:131 +#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 +#: src/components/core/Popup.jsx:132 +#: src/components/network/WifiConnectionForm.jsx:134 msgid "Cancel" msgstr "Cancelar" @@ -103,11 +93,11 @@ msgstr "Cancelar" msgid "Install" msgstr "Instalar" -#: src/components/core/InstallationFinished.jsx:42 +#: src/components/core/InstallationFinished.jsx:48 msgid "TPM sealing requires the new system to be booted directly." msgstr "El sellado TPM requiere que el nuevo sistema se inicie directamente." -#: src/components/core/InstallationFinished.jsx:47 +#: src/components/core/InstallationFinished.jsx:53 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." @@ -115,16 +105,15 @@ msgstr "" "Si se utilizó un medio local para ejecutar este instalador, expúlselo antes " "del próximo inicio." -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "Hide details" msgstr "Ocultar detalles" -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "See more details" msgstr "Ver más detalles" -#. TRANSLATORS: "Trusted Platform Module" is the name of the technology and "TPM" its abbreviation -#: src/components/core/InstallationFinished.jsx:55 +#: src/components/core/InstallationFinished.jsx:62 msgid "" "The final step to configure the Trusted Platform Module (TPM) to " "automatically open encrypted devices will take place during the first boot " @@ -136,29 +125,29 @@ msgstr "" "inicio del nuevo sistema. Para que eso funcione, la máquina debe iniciarse " "directamente en el nuevo gestor de arranque." -#: src/components/core/InstallationFinished.jsx:97 +#: src/components/core/InstallationFinished.jsx:107 msgid "Congratulations!" msgstr "¡Felicidades!" -#: src/components/core/InstallationFinished.jsx:102 +#: src/components/core/InstallationFinished.jsx:116 msgid "The installation on your machine is complete." msgstr "La instalación en su equipo está completa." -#: src/components/core/InstallationFinished.jsx:105 +#: src/components/core/InstallationFinished.jsx:119 msgid "At this point you can power off the machine." msgstr "En este punto puede apagar el equipo." -#: src/components/core/InstallationFinished.jsx:106 +#: src/components/core/InstallationFinished.jsx:121 msgid "At this point you can reboot the machine to log in to the new system." msgstr "" "En este punto, puede reiniciar el equipo para iniciar sesión en el nuevo " "sistema." -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Finish" msgstr "Finalizar" -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Reboot" msgstr "Reiniciar" @@ -166,40 +155,40 @@ msgstr "Reiniciar" msgid "Installing the system, please wait ..." msgstr "Instalando el sistema, por favor espere..." -#: src/components/core/InstallerOptions.jsx:83 +#: src/components/core/InstallerOptions.jsx:92 msgid "Show installer options" msgstr "Mostrar las opciones del instalador" -#: src/components/core/InstallerOptions.jsx:88 +#: src/components/core/InstallerOptions.jsx:95 msgid "Installer options" msgstr "Opciones del instalador" -#: src/components/core/InstallerOptions.jsx:94 -#: src/components/core/InstallerOptions.jsx:99 -#: src/components/core/InstallerOptions.jsx:100 -#: src/components/l10n/L10nPage.jsx:67 +#: src/components/core/InstallerOptions.jsx:98 +#: src/components/core/InstallerOptions.jsx:102 +#: src/components/core/InstallerOptions.jsx:103 +#: src/components/l10n/L10nPage.jsx:60 msgid "Language" msgstr "Idioma" -#: src/components/core/InstallerOptions.jsx:114 -#: src/components/core/InstallerOptions.jsx:121 +#: src/components/core/InstallerOptions.jsx:115 +#: src/components/core/InstallerOptions.jsx:120 msgid "Keyboard layout" msgstr "Esquema del teclado" -#: src/components/core/InstallerOptions.jsx:130 +#: src/components/core/InstallerOptions.jsx:129 msgid "Cannot be changed in remote installation" msgstr "No se puede cambiar en instalación remota" -#: src/components/core/InstallerOptions.jsx:135 -#: src/components/network/IpSettingsForm.jsx:210 -#: src/components/product/ProductRegistrationPage.jsx:89 -#: src/components/storage/BootSelection.jsx:228 -#: src/components/storage/DeviceSelection.jsx:240 -#: src/components/storage/EncryptionSettingsDialog.jsx:138 -#: src/components/storage/SpacePolicySelection.jsx:198 -#: src/components/storage/VolumeDialog.jsx:781 -#: src/components/storage/ZFCPPage.jsx:503 -#: src/components/users/FirstUserForm.jsx:285 +#: src/components/core/InstallerOptions.jsx:142 +#: src/components/network/IpSettingsForm.jsx:228 +#: src/components/product/ProductRegistrationPage.jsx:85 +#: src/components/storage/BootSelection.jsx:250 +#: src/components/storage/DeviceSelection.jsx:254 +#: src/components/storage/EncryptionSettingsDialog.jsx:155 +#: src/components/storage/SpacePolicySelection.jsx:200 +#: src/components/storage/VolumeDialog.jsx:794 +#: src/components/storage/ZFCPPage.jsx:528 +#: src/components/users/FirstUserForm.jsx:303 msgid "Accept" msgstr "Aceptar" @@ -209,64 +198,62 @@ msgid "" msgstr "" "Antes de comenzar la instalación, debe solucionar los siguientes problemas:" -#: src/components/core/ListSearch.jsx:51 +#: src/components/core/ListSearch.jsx:48 msgid "Search" msgstr "Buscar" -#: src/components/core/LoginPage.jsx:61 +#: src/components/core/LoginPage.jsx:64 msgid "Could not log in. Please, make sure that the password is correct." msgstr "" "No se ha podido iniciar sesión. Por favor, asegúrese de que la contraseña es " "correcta." -#: src/components/core/LoginPage.jsx:63 +#: src/components/core/LoginPage.jsx:66 msgid "Could not authenticate against the server, please check it." msgstr "No se pudo autenticar en el servidor, por favor verifíquelo." #. TRANSLATORS: Title for a form to provide the password for the root user. %s #. will be replaced by "root" -#: src/components/core/LoginPage.jsx:71 +#: src/components/core/LoginPage.jsx:74 #, c-format msgid "Log in as %s" msgstr "Iniciar sesión como %s" -#. TRANSLATORS: description why root password is needed. The text in the -#. square brackets [] is displayed in bold, use only please, do not translate -#. it and keep the brackets. -#: src/components/core/LoginPage.jsx:76 +#: src/components/core/LoginPage.jsx:80 msgid "The installer requires [root] user privileges." msgstr "El instalador requiere privilegios de usuario [root]." #: src/components/core/LoginPage.jsx:95 msgid "Please, provide its password to log in to the system." -msgstr "Por favor, proporcione su contraseña para iniciar sesión en el sistema." +msgstr "" +"Por favor, proporcione su contraseña para iniciar sesión en el sistema." -#: src/components/core/LoginPage.jsx:98 +#: src/components/core/LoginPage.jsx:96 msgid "Login form" msgstr "Formulario de inicio de sesión" -#: src/components/core/LoginPage.jsx:104 +#: src/components/core/LoginPage.jsx:102 msgid "Password input" msgstr "Entrada de contraseña" -#: src/components/core/LoginPage.jsx:113 +#: src/components/core/LoginPage.jsx:111 msgid "Log in" msgstr "Iniciar sesión" -#: src/components/core/LoginPage.jsx:124 +#: src/components/core/LoginPage.jsx:121 msgid "More about this" msgstr "Más acerca de esto" -#: src/components/core/LogsButton.jsx:103 +#: src/components/core/LogsButton.jsx:101 msgid "Collecting logs..." msgstr "Recolectando registros..." -#: src/components/core/LogsButton.jsx:103 -#: src/components/core/LogsButton.jsx:106 +#: src/components/core/LogsButton.jsx:101 +#: src/components/core/LogsButton.jsx:104 msgid "Download logs" msgstr "Descargar los registros" -#: src/components/core/LogsButton.jsx:112 +#: src/components/core/LogsButton.jsx:111 msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." @@ -274,31 +261,31 @@ msgstr "" "El navegador ejecutará la descarga de registros tan pronto como estén " "listos. Por favor tenga paciencia." -#: src/components/core/LogsButton.jsx:120 +#: src/components/core/LogsButton.jsx:121 msgid "Something went wrong while collecting logs. Please, try again." msgstr "Algo salió mal al recolectar los registros. Inténtelo de nuevo." -#: src/components/core/PasswordAndConfirmationInput.jsx:48 +#: src/components/core/PasswordAndConfirmationInput.jsx:55 msgid "Passwords do not match" msgstr "Las contraseñas no coinciden" -#: src/components/core/PasswordAndConfirmationInput.jsx:72 -#: src/components/network/WifiConnectionForm.jsx:120 -#: src/components/storage/iscsi/AuthFields.jsx:95 -#: src/components/storage/iscsi/AuthFields.jsx:100 -#: src/components/users/RootAuthMethods.jsx:163 +#: src/components/core/PasswordAndConfirmationInput.jsx:79 +#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/storage/iscsi/AuthFields.jsx:90 +#: src/components/storage/iscsi/AuthFields.jsx:94 +#: src/components/users/RootAuthMethods.jsx:165 msgid "Password" msgstr "Contraseña" -#: src/components/core/PasswordAndConfirmationInput.jsx:85 +#: src/components/core/PasswordAndConfirmationInput.jsx:90 msgid "Password confirmation" msgstr "Confirmación de contraseña" -#: src/components/core/PasswordInput.jsx:64 +#: src/components/core/PasswordInput.jsx:61 msgid "Password visibility button" msgstr "Botón de visibilidad de contraseña" -#: src/components/core/Popup.jsx:100 +#: src/components/core/Popup.jsx:92 msgid "Confirm" msgstr "Confirmar" @@ -315,117 +302,106 @@ msgstr "Finalizado" msgid "In progress" msgstr "En progreso" -#: src/components/core/ProgressReport.jsx:70 +#: src/components/core/ProgressReport.jsx:74 msgid "Pending" msgstr "Pendiente" -#: src/components/core/ProgressReport.jsx:134 +#: src/components/core/ProgressReport.jsx:138 msgid "Waiting for progress status..." msgstr "Esperando el estado del progreso..." #: src/components/core/RowActions.jsx:64 -#: src/components/storage/PartitionsField.jsx:454 -#: src/components/storage/ProposalActionsSummary.jsx:226 +#: src/components/storage/PartitionsField.jsx:491 +#: src/components/storage/ProposalActionsSummary.jsx:233 msgid "Actions" msgstr "Acciones" -#: src/components/core/SectionSkeleton.jsx:29 +#: src/components/core/SectionSkeleton.jsx:27 msgid "Waiting" msgstr "Esperar" -#: src/components/core/Selector.jsx:126 -#: src/components/software/SoftwarePatternsSelection.jsx:212 -msgid "auto selected" -msgstr "seleccionado automáticamente" - -#. TRANSLATORS: page title -#: src/components/core/ServerError.jsx:34 -msgid "Agama Error" -msgstr "Error de Agama" - -#: src/components/core/ServerError.jsx:38 +#: src/components/core/ServerError.jsx:47 msgid "Cannot connect to Agama server" msgstr "No se pud conectar al servidor de Agama" -#: src/components/core/ServerError.jsx:43 +#: src/components/core/ServerError.jsx:51 msgid "Please, check whether it is running." msgstr "Por favor, compruebe si está funcionando." -#. TRANSLATORS: button label -#: src/components/core/ServerError.jsx:51 +#: src/components/core/ServerError.jsx:56 msgid "Reload" msgstr "Recargar" -#: src/components/l10n/KeyboardSelection.jsx:45 +#: src/components/l10n/KeyboardSelection.jsx:41 msgid "Filter by description or keymap code" msgstr "Filtrar por descripción o código de mapa de teclas" -#: src/components/l10n/KeyboardSelection.jsx:85 +#: src/components/l10n/KeyboardSelection.jsx:71 msgid "None of the keymaps match the filter." msgstr "Ninguno de los mapas de teclas coincide con el filtro." -#: src/components/l10n/KeyboardSelection.jsx:92 +#: src/components/l10n/KeyboardSelection.jsx:77 msgid "Keyboard selection" msgstr "Selección de teclado" -#: src/components/l10n/KeyboardSelection.jsx:107 -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/l10n/LocaleSelection.jsx:107 -#: src/components/l10n/TimezoneSelection.jsx:145 -#: src/components/product/ProductSelectionPage.jsx:101 +#: src/components/l10n/KeyboardSelection.jsx:90 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/l10n/LocaleSelection.jsx:92 +#: src/components/l10n/TimezoneSelection.jsx:125 +#: src/components/product/ProductSelectionPage.jsx:90 msgid "Select" msgstr "Seleccionar" -#: src/components/l10n/L10nPage.jsx:60 src/components/l10n/routes.js:34 -#: src/components/overview/L10nSection.jsx:42 +#: src/components/l10n/L10nPage.jsx:53 +#: src/components/overview/L10nSection.jsx:37 src/routes/l10n.js:38 msgid "Localization" msgstr "Localización" -#: src/components/l10n/L10nPage.jsx:68 src/components/l10n/L10nPage.jsx:79 -#: src/components/l10n/L10nPage.jsx:90 +#: src/components/l10n/L10nPage.jsx:61 src/components/l10n/L10nPage.jsx:70 +#: src/components/l10n/L10nPage.jsx:80 msgid "Not selected yet" msgstr "Aún no seleccionado" -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/network/NetworkPage.jsx:102 -#: src/components/storage/InstallationDeviceField.jsx:105 -#: src/components/storage/ProposalActionsSummary.jsx:228 -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/storage/InstallationDeviceField.jsx:108 +#: src/components/storage/ProposalActionsSummary.jsx:238 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Change" msgstr "Cambiar" -#: src/components/l10n/L10nPage.jsx:78 +#: src/components/l10n/L10nPage.jsx:70 msgid "Keyboard" msgstr "Teclado" -#: src/components/l10n/L10nPage.jsx:89 +#: src/components/l10n/L10nPage.jsx:79 msgid "Time zone" msgstr "Zona horaria" -#: src/components/l10n/LocaleSelection.jsx:44 +#: src/components/l10n/LocaleSelection.jsx:39 msgid "Filter by language, territory or locale code" msgstr "Filtrar por idioma, territorio o código local" -#: src/components/l10n/LocaleSelection.jsx:84 +#: src/components/l10n/LocaleSelection.jsx:72 msgid "None of the locales match the filter." msgstr "Ninguna de las configuraciones regionales coincide con el filtro." -#: src/components/l10n/LocaleSelection.jsx:91 +#: src/components/l10n/LocaleSelection.jsx:78 msgid "Locale selection" msgstr "Selección de configuración regional" -#: src/components/l10n/TimezoneSelection.jsx:71 +#: src/components/l10n/TimezoneSelection.jsx:64 msgid "Filter by territory, time zone code or UTC offset" msgstr "Filtrar por territorio, código de zona horaria o compensación UTC" -#: src/components/l10n/TimezoneSelection.jsx:122 +#: src/components/l10n/TimezoneSelection.jsx:101 msgid "None of the time zones match the filter." msgstr "Ninguna de las zonas horarias coincide con el filtro." -#: src/components/l10n/TimezoneSelection.jsx:129 +#: src/components/l10n/TimezoneSelection.jsx:107 msgid " Timezone selection" msgstr " Selección de zona horaria" @@ -434,111 +410,111 @@ msgid "Loading installation environment, please wait." msgstr "Cargando el entorno de instalación, espere por favor." #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:78 -#: src/components/network/DnsDataList.jsx:84 +#: src/components/network/AddressesDataList.jsx:88 +#: src/components/network/DnsDataList.jsx:95 msgid "Remove" msgstr "Eliminar" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:90 -#: src/components/network/AddressesDataList.jsx:91 +#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:101 #: src/components/network/IpAddressInput.jsx:33 msgid "IP Address" msgstr "Dirección IP" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:99 -#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:109 +#: src/components/network/AddressesDataList.jsx:110 msgid "Prefix length or netmask" msgstr "Longitud del prefijo o máscara de red" -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add an address" msgstr "Añadir una dirección" #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add another address" msgstr "Añadir otras direcciones" -#: src/components/network/AddressesDataList.jsx:121 +#: src/components/network/AddressesDataList.jsx:131 msgid "Addresses" msgstr "Direcciones" -#: src/components/network/AddressesDataList.jsx:123 +#: src/components/network/AddressesDataList.jsx:133 msgid "Addresses data list" msgstr "Lista de datos de direcciones" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:67 -#: src/components/network/ConnectionsTable.jsx:95 -#: src/components/storage/ZFCPPage.jsx:361 +#: src/components/network/ConnectionsTable.jsx:64 +#: src/components/network/ConnectionsTable.jsx:92 +#: src/components/storage/ZFCPPage.jsx:381 #: src/components/storage/iscsi/InitiatorForm.jsx:52 #: src/components/storage/iscsi/InitiatorPresenter.jsx:68 #: src/components/storage/iscsi/InitiatorPresenter.jsx:85 -#: src/components/storage/iscsi/NodesPresenter.jsx:100 -#: src/components/storage/iscsi/NodesPresenter.jsx:121 +#: src/components/storage/iscsi/NodesPresenter.jsx:98 +#: src/components/storage/iscsi/NodesPresenter.jsx:119 msgid "Name" msgstr "Nombre" #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:69 -#: src/components/network/ConnectionsTable.jsx:96 +#: src/components/network/ConnectionsTable.jsx:66 +#: src/components/network/ConnectionsTable.jsx:93 msgid "IP addresses" msgstr "Direcciones IP" -#: src/components/network/ConnectionsTable.jsx:77 -#: src/components/network/WifiNetworksListPage.jsx:100 -#: src/components/network/WifiNetworksListPage.jsx:124 -#: src/components/storage/PartitionsField.jsx:320 +#: src/components/network/ConnectionsTable.jsx:74 +#: src/components/network/WifiNetworksListPage.jsx:107 +#: src/components/network/WifiNetworksListPage.jsx:130 +#: src/components/storage/PartitionsField.jsx:347 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 -#: src/components/users/FirstUser.jsx:116 +#: src/components/users/FirstUser.jsx:120 msgid "Edit" msgstr "Editar" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:80 -#: src/components/network/IpSettingsForm.jsx:136 +#: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/IpSettingsForm.jsx:151 #, c-format msgid "Edit connection %s" msgstr "Editar conexión %s" -#: src/components/network/ConnectionsTable.jsx:84 -#: src/components/network/WifiNetworksListPage.jsx:103 -#: src/components/network/WifiNetworksListPage.jsx:127 +#: src/components/network/ConnectionsTable.jsx:81 +#: src/components/network/WifiNetworksListPage.jsx:109 +#: src/components/network/WifiNetworksListPage.jsx:137 msgid "Forget" msgstr "Olvidar" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:86 +#: src/components/network/ConnectionsTable.jsx:83 #, c-format msgid "Forget connection %s" msgstr "Olvidar conexión %s" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:101 +#: src/components/network/ConnectionsTable.jsx:98 #, c-format msgid "Actions for connection %s" msgstr "Acciones para la conexión %s" #. TRANSLATORS: input field name -#: src/components/network/DnsDataList.jsx:75 -#: src/components/network/DnsDataList.jsx:76 +#: src/components/network/DnsDataList.jsx:81 +#: src/components/network/DnsDataList.jsx:82 msgid "Server IP" msgstr "Servidor IP" -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add DNS" msgstr "Añadir DNS" #. TRANSLATORS: button label -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add another DNS" msgstr "Añadir otro DNS" -#: src/components/network/DnsDataList.jsx:98 +#: src/components/network/DnsDataList.jsx:109 msgid "DNS" msgstr "DNS" @@ -548,42 +524,42 @@ msgid "IP prefix or netmask" msgstr "Prefijo IP o máscara de red" #. TRANSLATORS: error message -#: src/components/network/IpSettingsForm.jsx:90 +#: src/components/network/IpSettingsForm.jsx:104 msgid "At least one address must be provided for selected mode" msgstr "Se debe proporcionar al menos una dirección para el modo seleccionado" #. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) -#: src/components/network/IpSettingsForm.jsx:145 -#: src/components/network/IpSettingsForm.jsx:150 -#: src/components/network/IpSettingsForm.jsx:152 +#: src/components/network/IpSettingsForm.jsx:160 +#: src/components/network/IpSettingsForm.jsx:165 +#: src/components/network/IpSettingsForm.jsx:167 msgid "Mode" msgstr "Modo" -#: src/components/network/IpSettingsForm.jsx:156 +#: src/components/network/IpSettingsForm.jsx:174 msgid "Automatic (DHCP)" msgstr "Automático (DHCP)" #. TRANSLATORS: manual network configuration mode with a static IP address -#: src/components/network/IpSettingsForm.jsx:158 +#: src/components/network/IpSettingsForm.jsx:177 #: src/components/storage/iscsi/NodeStartupOptions.js:25 msgid "Manual" msgstr "Manual" #. TRANSLATORS: network gateway configuration -#: src/components/network/IpSettingsForm.jsx:166 -#: src/components/network/IpSettingsForm.jsx:169 +#: src/components/network/IpSettingsForm.jsx:185 +#: src/components/network/IpSettingsForm.jsx:188 msgid "Gateway" msgstr "Puerta de enlace" -#: src/components/network/IpSettingsForm.jsx:178 +#: src/components/network/IpSettingsForm.jsx:196 msgid "Gateway can be defined only in 'Manual' mode" msgstr "La puerta de enlace sólo se puede definir en modo 'Manual'" -#: src/components/network/NetworkPage.jsx:85 +#: src/components/network/NetworkPage.jsx:93 msgid "No Wi-Fi supported" msgstr "Wi-Fi no admitida" -#: src/components/network/NetworkPage.jsx:86 +#: src/components/network/NetworkPage.jsx:95 msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." @@ -591,40 +567,40 @@ msgstr "" "El sistema no admite conexiones WiFi, probablemente debido a que falta " "hardware o está deshabilitado." -#: src/components/network/NetworkPage.jsx:99 +#: src/components/network/NetworkPage.jsx:109 msgid "Wi-Fi" msgstr "WiFi" #. TRANSLATORS: button label, connect to a WiFi network -#: src/components/network/NetworkPage.jsx:102 -#: src/components/network/WifiConnectionForm.jsx:128 -#: src/components/network/WifiNetworksListPage.jsx:97 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:130 +#: src/components/network/WifiNetworksListPage.jsx:105 msgid "Connect" msgstr "Conectar" -#: src/components/network/NetworkPage.jsx:109 +#: src/components/network/NetworkPage.jsx:119 #, c-format msgid "Conected to %s" msgstr "Conectado a %s" -#: src/components/network/NetworkPage.jsx:114 +#: src/components/network/NetworkPage.jsx:126 msgid "No connected yet" msgstr "Aún no conectado" -#: src/components/network/NetworkPage.jsx:115 +#: src/components/network/NetworkPage.jsx:127 msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." msgstr "El sistema aún no se ha configurado para conectarse a una red WiFi." -#: src/components/network/NetworkPage.jsx:136 +#: src/components/network/NetworkPage.jsx:156 msgid "Wired" msgstr "Cableada" -#: src/components/network/NetworkPage.jsx:139 +#: src/components/network/NetworkPage.jsx:160 msgid "No wired connections found" msgstr "No se encontraron conexiones por cable" -#: src/components/network/NetworkPage.jsx:149 +#: src/components/network/NetworkPage.jsx:173 #: src/components/network/routes.js:59 msgid "Network" msgstr "Red" @@ -640,16 +616,16 @@ msgstr "Ninguno" msgid "WPA & WPA2 Personal" msgstr "WPA y WPA2 personales" -#: src/components/network/WifiConnectionForm.jsx:86 -#: src/components/product/ProductRegistrationPage.jsx:69 -#: src/components/storage/ZFCPDiskForm.jsx:108 -#: src/components/storage/iscsi/DiscoverForm.jsx:110 -#: src/components/storage/iscsi/LoginForm.jsx:72 -#: src/components/users/FirstUserForm.jsx:203 +#: src/components/network/WifiConnectionForm.jsx:85 +#: src/components/product/ProductRegistrationPage.jsx:68 +#: src/components/storage/ZFCPDiskForm.jsx:105 +#: src/components/storage/iscsi/DiscoverForm.jsx:98 +#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/users/FirstUserForm.jsx:217 msgid "Something went wrong" msgstr "Algo salió mal" -#: src/components/network/WifiConnectionForm.jsx:87 +#: src/components/network/WifiConnectionForm.jsx:86 msgid "Please, review provided settings and try again." msgstr "" "Por favor, revise la configuración proporcionada y vuelva a intentarlo." @@ -661,97 +637,97 @@ msgid "SSID" msgstr "SSID" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:104 -#: src/components/network/WifiConnectionForm.jsx:107 +#: src/components/network/WifiConnectionForm.jsx:105 +#: src/components/network/WifiConnectionForm.jsx:108 msgid "Security" msgstr "Seguridad" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:116 +#: src/components/network/WifiConnectionForm.jsx:117 msgid "WPA Password" msgstr "Contraseña WPA" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:51 -#: src/components/network/WifiNetworksListPage.jsx:111 +#: src/components/network/WifiNetworksListPage.jsx:63 +#: src/components/network/WifiNetworksListPage.jsx:117 msgid "Connecting" msgstr "Conectando" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:54 -#: src/components/network/WifiNetworksListPage.jsx:115 -#: src/components/network/WifiNetworksListPage.jsx:154 +#: src/components/network/WifiNetworksListPage.jsx:66 +#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:164 msgid "Connected" msgstr "Conectado" #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:59 -#: src/components/network/WifiNetworksListPage.jsx:113 +#: src/components/network/WifiNetworksListPage.jsx:71 +#: src/components/network/WifiNetworksListPage.jsx:119 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" msgstr "Desconectado" -#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:127 msgid "Disconnect" msgstr "Desconectar" -#: src/components/network/WifiNetworksListPage.jsx:142 +#: src/components/network/WifiNetworksListPage.jsx:150 msgid "Connect to a hidden network" msgstr "Conectar a una red oculta" -#: src/components/network/WifiNetworksListPage.jsx:153 +#: src/components/network/WifiNetworksListPage.jsx:161 msgid "configured" msgstr "Configurado" -#: src/components/network/WifiNetworksListPage.jsx:244 +#: src/components/network/WifiNetworksListPage.jsx:265 msgid "Connect to hidden network" msgstr "Conectar a una red oculta" -#: src/components/network/WifiSelectorPage.jsx:131 +#: src/components/network/WifiSelectorPage.jsx:136 msgid "Connect to a Wi-Fi network" msgstr "Conectado a una red WIFI" #. TRANSLATORS: %s will be replaced by a language name and territory, example: #. "English (United States)". -#: src/components/overview/L10nSection.jsx:38 +#: src/components/overview/L10nSection.jsx:33 #, c-format msgid "The system will use %s as its default language." msgstr "El sistema utilizará %s como su idioma predeterminado." -#: src/components/overview/OverviewPage.jsx:45 +#: src/components/overview/OverviewPage.jsx:47 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "Usuarios" -#: src/components/overview/OverviewPage.jsx:46 +#: src/components/overview/OverviewPage.jsx:48 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:290 +#: src/components/storage/ProposalPage.jsx:307 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "Almacenamiento" -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/overview/SoftwareSection.jsx:86 #: src/components/software/SoftwarePage.jsx:155 #: src/components/software/routes.js:32 msgid "Software" msgstr "Software" -#: src/components/overview/OverviewPage.jsx:52 +#: src/components/overview/OverviewPage.jsx:54 msgid "Ready for installation" msgstr "Preparado para la instalación" -#: src/components/overview/OverviewPage.jsx:102 +#: src/components/overview/OverviewPage.jsx:104 msgid "Installation" msgstr "Instalación" -#: src/components/overview/OverviewPage.jsx:103 +#: src/components/overview/OverviewPage.jsx:105 msgid "Before installing, please check the following problems." msgstr "Antes de instalar, verifique los siguientes problemas." -#: src/components/overview/OverviewPage.jsx:114 +#: src/components/overview/OverviewPage.jsx:116 msgid "" "Take your time to check your configuration before starting the installation " "process." @@ -759,7 +735,7 @@ msgstr "" "Dedica un tiempo para verificar la configuración antes de iniciar el proceso " "de instalación." -#: src/components/overview/OverviewPage.jsx:123 +#: src/components/overview/OverviewPage.jsx:125 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -847,14 +823,14 @@ msgstr "" "Instalar en un nuevo grupo de volúmenes de Logical Volume Manager (LVM) en " "%s usando una estrategia personalizada para encontrar el espacio necesario" -#: src/components/overview/StorageSection.jsx:175 -#: src/components/storage/InstallationDeviceField.jsx:63 +#: src/components/overview/StorageSection.jsx:179 +#: src/components/storage/InstallationDeviceField.jsx:66 msgid "No device selected yet" msgstr "Ningún dispositivo seleccionado todavía" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:182 +#: src/components/overview/StorageSection.jsx:186 #, c-format msgid "Install using device %s shrinking existing partitions as needed" msgstr "" @@ -863,7 +839,7 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:186 +#: src/components/overview/StorageSection.jsx:190 #, c-format msgid "Install using device %s without modifying existing partitions" msgstr "" @@ -872,14 +848,14 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:190 +#: src/components/overview/StorageSection.jsx:194 #, c-format msgid "Install using device %s and deleting all its content" msgstr "Instalar utilizando el dispositivo %s y eliminar todo su contenido" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:195 +#: src/components/overview/StorageSection.jsx:199 #, c-format msgid "Install using device %s with a custom strategy to find the needed space" msgstr "" @@ -895,19 +871,15 @@ msgstr "Descripción general" msgid "Register %s" msgstr "Registro %s" -#: src/components/product/ProductRegistrationPage.jsx:74 +#: src/components/product/ProductRegistrationPage.jsx:73 msgid "Registration code" msgstr "Código de registro" -#: src/components/product/ProductRegistrationPage.jsx:77 +#: src/components/product/ProductRegistrationPage.jsx:76 msgid "Email" msgstr "Correo electrónico" -#: src/components/product/ProductSelectionPage.jsx:58 -msgid "Loading available products, please wait..." -msgstr "Cargando productos disponibles, por favor espere..." - -#: src/components/product/ProductSelectionProgress.jsx:53 +#: src/components/product/ProductSelectionProgress.jsx:49 msgid "Configuring the product, please wait ..." msgstr "Configurando el producto, por favor espere..." @@ -926,7 +898,7 @@ msgid "Encrypted Device" msgstr "Dispositivo cifrado" #. TRANSLATORS: field label -#: src/components/questions/LuksActivationQuestion.jsx:69 +#: src/components/questions/LuksActivationQuestion.jsx:67 msgid "Encryption Password" msgstr "Contraseña de cifrado" @@ -947,17 +919,21 @@ msgstr "Seleccione los patrones" msgid "Change selection" msgstr "Cambiar selección" -#: src/components/software/SoftwarePatternsSelection.jsx:230 +#: src/components/software/SoftwarePatternsSelection.jsx:223 +msgid "auto selected" +msgstr "seleccionado automáticamente" + +#: src/components/software/SoftwarePatternsSelection.jsx:241 msgid "None of the patterns match the filter." msgstr "Ninguno de los patrones coincide con el filtro." -#: src/components/software/SoftwarePatternsSelection.jsx:238 +#: src/components/software/SoftwarePatternsSelection.jsx:248 msgid "Software selection" msgstr "Selección de software" #. TRANSLATORS: search field placeholder text -#: src/components/software/SoftwarePatternsSelection.jsx:241 -#: src/components/software/SoftwarePatternsSelection.jsx:242 +#: src/components/software/SoftwarePatternsSelection.jsx:251 +#: src/components/software/SoftwarePatternsSelection.jsx:252 msgid "Filter by pattern title or description" msgstr "Filtrar por título o descripción del patrón" @@ -968,7 +944,7 @@ msgstr "Filtrar por título o descripción del patrón" msgid "Installation will take %s." msgstr "La instalación ocupará %s." -#: src/components/software/UsedSize.jsx:38 +#: src/components/software/UsedSize.jsx:37 msgid "This space includes the base system and the selected software patterns." msgstr "" "Este espacio incluye el sistema base y los patrones de software " @@ -978,24 +954,23 @@ msgstr "" msgid "Change boot options" msgstr "Cambiar opciones de arranque" -#: src/components/storage/BootConfigField.jsx:87 +#: src/components/storage/BootConfigField.jsx:81 msgid "Installation will not configure partitions for booting." msgstr "La instalación no configurará particiones para el arranque." -#: src/components/storage/BootConfigField.jsx:89 +#: src/components/storage/BootConfigField.jsx:85 msgid "" "Installation will configure partitions for booting at the installation disk." msgstr "" "La instalación configurará las particiones para arrancar en el disco de " "instalación." -#. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/BootConfigField.jsx:92 +#: src/components/storage/BootConfigField.jsx:89 #, c-format msgid "Installation will configure partitions for booting at %s." msgstr "La instalación configurará las particiones para arrancar en %s." -#: src/components/storage/BootSelection.jsx:127 +#: src/components/storage/BootSelection.jsx:132 msgid "" "To ensure the new system is able to boot, the installer may need to create " "or configure some partitions in the appropriate disk." @@ -1003,44 +978,44 @@ msgstr "" "Para garantizar que el nuevo sistema pueda iniciarse, es posible que el " "instalador deba crear o configurar algunas particiones en el disco apropiado." -#: src/components/storage/BootSelection.jsx:133 +#: src/components/storage/BootSelection.jsx:138 msgid "Partitions to boot will be allocated at the installation disk." msgstr "Las particiones para arrancar se asignarán en el disco de instalación." #. TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") -#: src/components/storage/BootSelection.jsx:138 +#: src/components/storage/BootSelection.jsx:143 #, c-format msgid "Partitions to boot will be allocated at the installation disk (%s)." msgstr "" "Las particiones para arrancar se asignarán en el disco de instalación (%s)." -#: src/components/storage/BootSelection.jsx:154 +#: src/components/storage/BootSelection.jsx:159 msgid "Select booting partition" msgstr "Seleccion la partición de arranque" -#: src/components/storage/BootSelection.jsx:170 +#: src/components/storage/BootSelection.jsx:180 #: src/components/storage/iscsi/NodeStartupOptions.js:27 msgid "Automatic" msgstr "Automático" -#: src/components/storage/BootSelection.jsx:183 +#: src/components/storage/BootSelection.jsx:198 msgid "Select a disk" msgstr "Seleccionar un disco" -#: src/components/storage/BootSelection.jsx:189 +#: src/components/storage/BootSelection.jsx:204 msgid "Partitions to boot will be allocated at the following device." msgstr "" "Las particiones para arrancar se asignarán en el siguiente dispositivo." -#: src/components/storage/BootSelection.jsx:192 +#: src/components/storage/BootSelection.jsx:207 msgid "Choose a disk for placing the boot loader" msgstr "Escoger un disco para colocar el cargador de arranque" -#: src/components/storage/BootSelection.jsx:210 +#: src/components/storage/BootSelection.jsx:230 msgid "Do not configure" msgstr "No configurar" -#: src/components/storage/BootSelection.jsx:215 +#: src/components/storage/BootSelection.jsx:236 msgid "" "No partitions will be automatically configured for booting. Use with caution." msgstr "" @@ -1051,125 +1026,117 @@ msgstr "" msgid "Waiting for progress report" msgstr "Esperando el informe de progreso" -#: src/components/storage/DASDFormatProgress.jsx:68 +#: src/components/storage/DASDFormatProgress.jsx:67 msgid "Formatting DASD devices" msgstr "Formatear dispositivos DASD" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "No" msgstr "No" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "Yes" msgstr "Sí" -#: src/components/storage/DASDTable.jsx:63 -#: src/components/storage/ZFCPDiskForm.jsx:112 -#: src/components/storage/ZFCPPage.jsx:302 -#: src/components/storage/ZFCPPage.jsx:362 +#: src/components/storage/DASDTable.jsx:69 +#: src/components/storage/ZFCPDiskForm.jsx:110 +#: src/components/storage/ZFCPPage.jsx:324 +#: src/components/storage/ZFCPPage.jsx:382 msgid "Channel ID" msgstr "Canal ID" #. TRANSLATORS: table header -#: src/components/storage/DASDTable.jsx:64 -#: src/components/storage/ZFCPPage.jsx:303 -#: src/components/storage/iscsi/NodesPresenter.jsx:104 -#: src/components/storage/iscsi/NodesPresenter.jsx:125 -#: src/components/users/RootAuthMethods.jsx:157 +#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/ZFCPPage.jsx:325 +#: src/components/storage/iscsi/NodesPresenter.jsx:102 +#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/users/RootAuthMethods.jsx:159 msgid "Status" msgstr "Estado" -#: src/components/storage/DASDTable.jsx:65 -#: src/components/storage/DeviceSelectorTable.jsx:186 -#: src/components/storage/ProposalResultTable.jsx:120 -#: src/components/storage/SpaceActionsTable.jsx:141 -#: src/components/storage/VolumeLocationSelectorTable.jsx:100 +#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DeviceSelectorTable.jsx:197 +#: src/components/storage/ProposalResultTable.jsx:130 +#: src/components/storage/SpaceActionsTable.jsx:200 +#: src/components/storage/VolumeLocationSelectorTable.jsx:107 msgid "Device" msgstr "Dispositivo" -#: src/components/storage/DASDTable.jsx:66 +#: src/components/storage/DASDTable.jsx:72 msgid "Type" msgstr "Tipo" #. TRANSLATORS: table header, the column contains "Yes"/"No" values #. for the DIAG access mode (special disk access mode on IBM mainframes), #. usually keep untranslated -#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/DASDTable.jsx:76 msgid "DIAG" msgstr "DIAG" -#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DASDTable.jsx:77 msgid "Formatted" msgstr "Formateado" -#: src/components/storage/DASDTable.jsx:72 +#: src/components/storage/DASDTable.jsx:78 msgid "Partition Info" msgstr "Información de la partición" #. TRANSLATORS: drop down menu label -#: src/components/storage/DASDTable.jsx:107 +#: src/components/storage/DASDTable.jsx:115 msgid "Perform an action" msgstr "Realizar una acción" -#. TRANSLATORS: drop down menu action, activate the device -#: src/components/storage/DASDTable.jsx:113 -#: src/components/storage/ZFCPPage.jsx:333 +#: src/components/storage/DASDTable.jsx:122 +#: src/components/storage/ZFCPPage.jsx:353 msgid "Activate" msgstr "Activar" -#. TRANSLATORS: drop down menu action, deactivate the device -#: src/components/storage/DASDTable.jsx:115 -#: src/components/storage/ZFCPPage.jsx:375 +#: src/components/storage/DASDTable.jsx:126 +#: src/components/storage/ZFCPPage.jsx:395 msgid "Deactivate" msgstr "Desactivar" -#. TRANSLATORS: drop down menu action, enable DIAG access method -#: src/components/storage/DASDTable.jsx:118 +#: src/components/storage/DASDTable.jsx:131 msgid "Set DIAG On" msgstr "Activar DIAG" -#. TRANSLATORS: drop down menu action, disable DIAG access method -#: src/components/storage/DASDTable.jsx:120 +#: src/components/storage/DASDTable.jsx:135 msgid "Set DIAG Off" msgstr "Desactivar DIAG" -#. TRANSLATORS: drop down menu action, format the disk -#: src/components/storage/DASDTable.jsx:123 +#: src/components/storage/DASDTable.jsx:140 msgid "Format" msgstr "Formatear" -#: src/components/storage/DASDTable.jsx:223 -#: src/components/storage/DASDTable.jsx:224 +#: src/components/storage/DASDTable.jsx:261 +#: src/components/storage/DASDTable.jsx:262 msgid "Filter by min channel" msgstr "Filtrar por canal mínimo" -#: src/components/storage/DASDTable.jsx:231 +#: src/components/storage/DASDTable.jsx:269 msgid "Remove min channel filter" msgstr "Eliminar filtro de canal mínimo" -#: src/components/storage/DASDTable.jsx:244 -#: src/components/storage/DASDTable.jsx:245 +#: src/components/storage/DASDTable.jsx:283 +#: src/components/storage/DASDTable.jsx:284 msgid "Filter by max channel" msgstr "Filtrar por canal máximo" -#: src/components/storage/DASDTable.jsx:252 +#: src/components/storage/DASDTable.jsx:291 msgid "Remove max channel filter" msgstr "Eliminar filtro de canal máximo" -#: src/components/storage/DeviceSelection.jsx:101 +#: src/components/storage/DeviceSelection.jsx:108 msgid "Loading data, please wait a second..." msgstr "Cargando los datos, por favor espere..." -#. TRANSLATORS: description for using plain partitions for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:136 +#: src/components/storage/DeviceSelection.jsx:144 msgid "" "The file systems will be allocated by default as [new partitions in the " "selected device]." @@ -1177,10 +1144,7 @@ msgstr "" "Los sistemas de archivos se asignarán de forma predeterminada como [nuevas " "particiones en el dispositivo seleccionado]." -#. TRANSLATORS: description for using logical volumes for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:141 +#: src/components/storage/DeviceSelection.jsx:151 msgid "" "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 " @@ -1191,135 +1155,135 @@ msgstr "" "físicos correspondientes se crearán según demanda como nuevas particiones en " "los dispositivos seleccionados." -#: src/components/storage/DeviceSelection.jsx:149 +#: src/components/storage/DeviceSelection.jsx:160 msgid "Select installation device" msgstr "Seleccionar el dispositivo de instalación" -#: src/components/storage/DeviceSelection.jsx:155 +#: src/components/storage/DeviceSelection.jsx:166 msgid "Install new system on" msgstr "Instalar nuevo sistema en" -#: src/components/storage/DeviceSelection.jsx:158 +#: src/components/storage/DeviceSelection.jsx:169 msgid "An existing disk" msgstr "Un disco existente" -#: src/components/storage/DeviceSelection.jsx:167 +#: src/components/storage/DeviceSelection.jsx:178 msgid "A new LVM Volume Group" msgstr "Un nuevo Grupo de Volúmen LVM" -#: src/components/storage/DeviceSelection.jsx:192 +#: src/components/storage/DeviceSelection.jsx:203 msgid "Device selector for target disk" msgstr "Selector de dispositivo para disco de destino" -#: src/components/storage/DeviceSelection.jsx:215 +#: src/components/storage/DeviceSelection.jsx:226 msgid "Device selector for new LVM volume group" msgstr "Selector de dispositivo para nuevo grupo de volúmenes LVM" -#: src/components/storage/DeviceSelection.jsx:228 +#: src/components/storage/DeviceSelection.jsx:242 msgid "Prepare more devices by configuring advanced" msgstr "Preparar más dispositivos configurando de forma avanzada" -#: src/components/storage/DeviceSelection.jsx:229 +#: src/components/storage/DeviceSelection.jsx:243 msgid "storage techs" msgstr "tecnologías de almacenamiento" #. TRANSLATORS: multipath device type -#: src/components/storage/DeviceSelectorTable.jsx:57 +#: src/components/storage/DeviceSelectorTable.jsx:61 msgid "Multipath" msgstr "Ruta múltiple" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/DeviceSelectorTable.jsx:62 +#: src/components/storage/DeviceSelectorTable.jsx:66 #, c-format msgid "DASD %s" msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/DeviceSelectorTable.jsx:67 +#: src/components/storage/DeviceSelectorTable.jsx:71 #, c-format msgid "Software %s" msgstr "Software %s" -#: src/components/storage/DeviceSelectorTable.jsx:72 +#: src/components/storage/DeviceSelectorTable.jsx:76 msgid "SD Card" msgstr "Tarjeta SD" #. TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" -#: src/components/storage/DeviceSelectorTable.jsx:77 +#: src/components/storage/DeviceSelectorTable.jsx:81 #, c-format msgid "%s disk" msgstr "disco %s" -#: src/components/storage/DeviceSelectorTable.jsx:78 +#: src/components/storage/DeviceSelectorTable.jsx:82 msgid "Disk" msgstr "Disco" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:98 +#: src/components/storage/DeviceSelectorTable.jsx:102 #, c-format msgid "Members: %s" msgstr "Miembros: %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:107 +#: src/components/storage/DeviceSelectorTable.jsx:111 #, c-format msgid "Devices: %s" msgstr "Dispositivos: %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/DeviceSelectorTable.jsx:116 +#: src/components/storage/DeviceSelectorTable.jsx:120 #, c-format msgid "Wires: %s" msgstr "Wires: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/DeviceSelectorTable.jsx:152 +#: src/components/storage/DeviceSelectorTable.jsx:155 #, c-format msgid "%s with %d partitions" msgstr "%s con %d particiones" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/DeviceSelectorTable.jsx:158 -#: src/components/storage/SpaceActionsTable.jsx:114 +#: src/components/storage/DeviceSelectorTable.jsx:161 +#: src/components/storage/SpaceActionsTable.jsx:175 msgid "No content found" msgstr "No se encontró contenido" -#: src/components/storage/DeviceSelectorTable.jsx:187 -#: src/components/storage/PartitionsField.jsx:450 -#: src/components/storage/ProposalResultTable.jsx:122 -#: src/components/storage/SpaceActionsTable.jsx:142 -#: src/components/storage/VolumeLocationSelectorTable.jsx:101 +#: src/components/storage/DeviceSelectorTable.jsx:198 +#: src/components/storage/PartitionsField.jsx:487 +#: src/components/storage/ProposalResultTable.jsx:132 +#: src/components/storage/SpaceActionsTable.jsx:201 +#: src/components/storage/VolumeLocationSelectorTable.jsx:108 msgid "Details" msgstr "Detalles" -#: src/components/storage/DeviceSelectorTable.jsx:188 -#: src/components/storage/PartitionsField.jsx:451 -#: src/components/storage/ProposalResultTable.jsx:123 -#: src/components/storage/SpaceActionsTable.jsx:143 -#: src/components/storage/VolumeFields.jsx:474 -#: src/components/storage/VolumeLocationSelectorTable.jsx:103 +#: src/components/storage/DeviceSelectorTable.jsx:199 +#: src/components/storage/PartitionsField.jsx:488 +#: src/components/storage/ProposalResultTable.jsx:133 +#: src/components/storage/SpaceActionsTable.jsx:202 +#: src/components/storage/VolumeFields.jsx:488 +#: src/components/storage/VolumeLocationSelectorTable.jsx:113 msgid "Size" msgstr "Tamaño" -#: src/components/storage/DevicesTechMenu.jsx:44 +#: src/components/storage/DevicesTechMenu.jsx:38 msgid "Manage and format" msgstr "Administrar y formatear" -#: src/components/storage/DevicesTechMenu.jsx:62 +#: src/components/storage/DevicesTechMenu.jsx:52 msgid "Activate disks" msgstr "Activar discos" -#: src/components/storage/DevicesTechMenu.jsx:64 +#: src/components/storage/DevicesTechMenu.jsx:53 msgid "zFCP" msgstr "zFCP" -#: src/components/storage/DevicesTechMenu.jsx:80 +#: src/components/storage/DevicesTechMenu.jsx:66 msgid "Connect to iSCSI targets" msgstr "Conectar a objetivos iSCSI" -#: src/components/storage/DevicesTechMenu.jsx:82 +#: src/components/storage/DevicesTechMenu.jsx:67 #: src/components/storage/routes.js:37 msgid "iSCSI" msgstr "iSCSI" @@ -1329,7 +1293,7 @@ msgstr "iSCSI" msgid "Encryption" msgstr "Cifrado" -#: src/components/storage/EncryptionField.jsx:39 +#: src/components/storage/EncryptionField.jsx:40 msgid "" "Protection for the information stored at the device, including data, " "programs, and system files." @@ -1337,27 +1301,27 @@ msgstr "" "Protección de la información almacenada en el dispositivo, incluidos datos, " "programas y archivos del sistema." -#: src/components/storage/EncryptionField.jsx:42 +#: src/components/storage/EncryptionField.jsx:44 msgid "disabled" msgstr "desactivado" -#: src/components/storage/EncryptionField.jsx:43 +#: src/components/storage/EncryptionField.jsx:45 msgid "enabled" msgstr "activado" -#: src/components/storage/EncryptionField.jsx:44 +#: src/components/storage/EncryptionField.jsx:46 msgid "using TPM unlocking" msgstr "usando el desbloqueo TPM" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Enable" msgstr "Habilitado" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Modify" msgstr "Modificar" -#: src/components/storage/EncryptionSettingsDialog.jsx:37 +#: src/components/storage/EncryptionSettingsDialog.jsx:38 msgid "" "Full Disk Encryption (FDE) allows to protect the information stored at the " "device, including data, programs, and system files." @@ -1366,16 +1330,14 @@ msgstr "" "dispositivo, incluidos datos, programas y archivos del sistema." #. TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation -#: src/components/storage/EncryptionSettingsDialog.jsx:40 +#: src/components/storage/EncryptionSettingsDialog.jsx:42 msgid "" "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot" msgstr "" "Utilizar Trusted Platform Module(TPM) para descifrar automáticamente en cada " "arranque" -#. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing -#. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. -#: src/components/storage/EncryptionSettingsDialog.jsx:43 +#: src/components/storage/EncryptionSettingsDialog.jsx:46 msgid "" "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 " @@ -1385,12 +1347,12 @@ msgstr "" "puede verificar la integridad del sistema. El sellado TPM requiere que el " "nuevo sistema se inicie directamente en su primera ejecución." -#: src/components/storage/EncryptionSettingsDialog.jsx:114 +#: src/components/storage/EncryptionSettingsDialog.jsx:129 msgid "Encrypt the system" msgstr "Cifrar el sistema" #: src/components/storage/InstallationDeviceField.jsx:36 -#: src/components/storage/VolumeLocationSelectorTable.jsx:58 +#: src/components/storage/VolumeLocationSelectorTable.jsx:61 msgid "Installation device" msgstr "Dispositivo de instalación" @@ -1409,71 +1371,70 @@ msgstr "Sistemas de archivos creados como particiones nuevas en %s" msgid "File systems created at a new LVM volume group" msgstr "Sistemas de archivos creados en un nuevo grupo de volúmenes LVM" -#. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) -#: src/components/storage/InstallationDeviceField.jsx:59 +#: src/components/storage/InstallationDeviceField.jsx:60 #, c-format msgid "File systems created at a new LVM volume group on %s" msgstr "Sistemas de archivos creados en un nuevo grupo de volúmenes LVM en %s" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:73 +#: src/components/storage/PartitionsField.jsx:84 #, c-format msgid "at least %s" msgstr "al menos %s" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:97 +#: src/components/storage/PartitionsField.jsx:108 #, c-format msgid "Transactional Btrfs root volume (%s)" msgstr "Volumen raíz transaccional de Btrfs (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:99 +#: src/components/storage/PartitionsField.jsx:110 #, c-format msgid "Transactional Btrfs root partition (%s)" msgstr "Partición raíz transaccional Btrfs (%s)" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:104 +#: src/components/storage/PartitionsField.jsx:115 #, c-format msgid "Btrfs root volume with snapshots (%s)" msgstr "Volumen raíz Btrfs con instantáneas (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:106 +#: src/components/storage/PartitionsField.jsx:117 #, c-format msgid "Btrfs root partition with snapshots (%s)" msgstr "Partición raíz Btrfs con instantáneas (%s)" #. TRANSLATORS: This results in something like "Mount /dev/sda3 at /home (25 GiB)" since #. %1$s is replaced by the device name, %2$s by the mount point and %3$s by the size -#: src/components/storage/PartitionsField.jsx:115 +#: src/components/storage/PartitionsField.jsx:126 #, c-format msgid "Mount %1$s at %2$s (%3$s)" msgstr "Montar %1$s en %2$s (%3$s)" #. TRANSLATORS: This results in something like "Swap at /dev/sda3 (2 GiB)" since #. %1$s is replaced by the device name, and %2$s by the size -#: src/components/storage/PartitionsField.jsx:121 +#: src/components/storage/PartitionsField.jsx:132 #, c-format msgid "Swap at %1$s (%2$s)" msgstr "Intercambiar en %1$s (%2$s)" #. TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:125 +#: src/components/storage/PartitionsField.jsx:136 #, c-format msgid "Swap volume (%s)" msgstr "Volumen de intercambio (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:127 +#: src/components/storage/PartitionsField.jsx:138 #, c-format msgid "Swap partition (%s)" msgstr "Partición de intercambio (%s)" #. TRANSLATORS: This results in something like "Btrfs root at /dev/sda3 (20 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size -#: src/components/storage/PartitionsField.jsx:136 +#: src/components/storage/PartitionsField.jsx:147 #, c-format msgid "%1$s root at %2$s (%3$s)" msgstr "%1$s raíz en %2$s (%3$s)" @@ -1481,21 +1442,21 @@ msgstr "%1$s raíz en %2$s (%3$s)" #. TRANSLATORS: "/" is in an LVM logical volume. #. Results in something like "Btrfs root volume (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:142 +#: src/components/storage/PartitionsField.jsx:153 #, c-format msgid "%1$s root volume (%2$s)" msgstr "%1$s volumen raíz (%2$s)" #. TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:145 +#: src/components/storage/PartitionsField.jsx:156 #, c-format msgid "%1$s root partition (%2$s)" msgstr "%1$s partición raíz (%2$s)" #. TRANSLATORS: This results in something like "Ext4 /home at /dev/sda3 (20 GiB)" since #. %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size -#: src/components/storage/PartitionsField.jsx:151 +#: src/components/storage/PartitionsField.jsx:162 #, c-format msgid "%1$s %2$s at %3$s (%4$s)" msgstr "%1$s %2$s en %3$s (%4$s)" @@ -1503,143 +1464,139 @@ msgstr "%1$s %2$s en %3$s (%4$s)" #. TRANSLATORS: The filesystem is in an LVM logical volume. #. Results in something like "Ext4 /home volume (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:157 +#: src/components/storage/PartitionsField.jsx:168 #, c-format msgid "%1$s %2$s volume (%3$s)" msgstr "%1$s %2$s volumen (%3$s)" #. TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:160 +#: src/components/storage/PartitionsField.jsx:171 #, c-format msgid "%1$s %2$s partition (%3$s)" msgstr "%1$s partición %2$s (%3$s)" -#: src/components/storage/PartitionsField.jsx:172 +#: src/components/storage/PartitionsField.jsx:182 msgid "Do not configure partitions for booting" msgstr "No configurar particiones para el arranque" -#: src/components/storage/PartitionsField.jsx:175 +#: src/components/storage/PartitionsField.jsx:184 msgid "Boot partitions at installation disk" msgstr "Particiones de arranque en el disco de instalación" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/PartitionsField.jsx:178 +#: src/components/storage/PartitionsField.jsx:187 #, c-format msgid "Boot partitions at %s" msgstr "Arrancar particiones en %s" #. TRANSLATORS: header for a list of items referring to size limits for file systems -#: src/components/storage/PartitionsField.jsx:200 +#: src/components/storage/PartitionsField.jsx:209 msgid "These limits are affected by:" msgstr "Estos límites se ven afectados por:" #. TRANSLATORS: list item, this affects the computed partition size limits -#: src/components/storage/PartitionsField.jsx:204 +#: src/components/storage/PartitionsField.jsx:213 msgid "The configuration of snapshots" msgstr "La configuración de instantáneas" -#. TRANSLATORS: list item, this affects the computed partition size limits -#. %s is replaced by a list of the volumes (like "/home, /boot") -#: src/components/storage/PartitionsField.jsx:208 +#: src/components/storage/PartitionsField.jsx:219 #, c-format msgid "Presence of other volumes (%s)" msgstr "Presencia de otros volúmenes (%s)" #. TRANSLATORS: list item, describes a factor that affects the computed size of a #. file system; eg. adjusting the size of the swap -#: src/components/storage/PartitionsField.jsx:212 +#: src/components/storage/PartitionsField.jsx:225 msgid "The amount of RAM in the system" msgstr "La cantidad de memoria RAM en el sistema" -#. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/PartitionsField.jsx:263 +#: src/components/storage/PartitionsField.jsx:292 msgid "auto" msgstr "automático" #. TRANSLATORS: %s will be replaced by a file-system type like "Btrfs" or "Ext4" -#: src/components/storage/PartitionsField.jsx:279 +#: src/components/storage/PartitionsField.jsx:309 #, c-format msgid "Reused %s" msgstr "Reutilizado %s" -#: src/components/storage/PartitionsField.jsx:281 +#: src/components/storage/PartitionsField.jsx:310 msgid "Transactional Btrfs" msgstr "Transaccional Brtfs" -#: src/components/storage/PartitionsField.jsx:283 +#: src/components/storage/PartitionsField.jsx:311 msgid "Btrfs with snapshots" msgstr "Brtfs con instantáneas" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:297 +#: src/components/storage/PartitionsField.jsx:325 #, c-format msgid "Partition at %s" msgstr "Partición en %s" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:300 +#: src/components/storage/PartitionsField.jsx:328 #, c-format msgid "Separate LVM at %s" msgstr "Separar LVM en %s" -#: src/components/storage/PartitionsField.jsx:304 +#: src/components/storage/PartitionsField.jsx:331 msgid "Logical volume at system LVM" msgstr "Volumen lógico en el sistema LVM" -#: src/components/storage/PartitionsField.jsx:306 +#: src/components/storage/PartitionsField.jsx:333 msgid "Partition at installation disk" msgstr "Partición en el disco de instalación" -#: src/components/storage/PartitionsField.jsx:321 +#: src/components/storage/PartitionsField.jsx:348 msgid "Reset location" msgstr "Reiniciar localización" -#: src/components/storage/PartitionsField.jsx:322 +#: src/components/storage/PartitionsField.jsx:349 msgid "Change location" msgstr "Cambiar ubicación" -#: src/components/storage/PartitionsField.jsx:323 -#: src/components/storage/SpaceActionsTable.jsx:78 +#: src/components/storage/PartitionsField.jsx:350 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Eliminar" -#: src/components/storage/PartitionsField.jsx:449 -#: src/components/storage/VolumeFields.jsx:61 -#: src/components/storage/VolumeFields.jsx:70 +#: src/components/storage/PartitionsField.jsx:486 +#: src/components/storage/VolumeFields.jsx:66 #: src/components/storage/VolumeFields.jsx:75 +#: src/components/storage/VolumeFields.jsx:80 msgid "Mount point" msgstr "Punto de montaje" #. TRANSLATORS: where (and how) the file-system is going to be created -#: src/components/storage/PartitionsField.jsx:453 +#: src/components/storage/PartitionsField.jsx:490 msgid "Location" msgstr "Ubicación" -#: src/components/storage/PartitionsField.jsx:495 +#: src/components/storage/PartitionsField.jsx:532 msgid "Table with mount points" msgstr "Tabla con puntos de montaje" -#: src/components/storage/PartitionsField.jsx:566 -#: src/components/storage/PartitionsField.jsx:585 -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/PartitionsField.jsx:604 +#: src/components/storage/PartitionsField.jsx:624 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Add file system" msgstr "Agregar sistema de archivos" -#: src/components/storage/PartitionsField.jsx:596 +#: src/components/storage/PartitionsField.jsx:636 msgid "Other" msgstr "Otro" -#: src/components/storage/PartitionsField.jsx:731 +#: src/components/storage/PartitionsField.jsx:777 msgid "Reset to defaults" msgstr "Restablecer los valores predeterminados" -#: src/components/storage/PartitionsField.jsx:801 +#: src/components/storage/PartitionsField.jsx:849 msgid "Partitions and file systems" msgstr "Particiones y sistemas de archivos" -#: src/components/storage/PartitionsField.jsx:802 +#: src/components/storage/PartitionsField.jsx:851 msgid "" "Structure of the new system, including any additional partition needed for " "booting" @@ -1647,20 +1604,18 @@ msgstr "" "Estructura del nuevo sistema, incluida cualquier partición adicional " "necesaria para el arranque" -#: src/components/storage/PartitionsField.jsx:808 +#: src/components/storage/PartitionsField.jsx:858 msgid "Show partitions and file-systems actions" msgstr "Mostrar acciones de particiones y sistemas de archivos" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:62 +#: src/components/storage/ProposalActionsDialog.jsx:65 #, c-format msgid "Hide %d subvolume action" msgid_plural "Hide %d subvolume actions" msgstr[0] "Ocultar %d acción de subvolumen" msgstr[1] "Ocultar %d acciones de subvolumen" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:64 +#: src/components/storage/ProposalActionsDialog.jsx:70 #, c-format msgid "Show %d subvolume action" msgid_plural "Show %d subvolume actions" @@ -1675,85 +1630,78 @@ msgstr "No se permiten acciones destructivas" msgid "Destructive actions are allowed" msgstr "Se permiten acciones destructivas" -#: src/components/storage/ProposalActionsSummary.jsx:66 -#, c-format -msgid "There is %d destructive action planned" -msgid_plural "There are %d destructive actions planned" -msgstr[0] "Hay %d acción destructiva planeada" -msgstr[1] "Hay %d acciones destructivas planeadas" - -#: src/components/storage/ProposalActionsSummary.jsx:79 -#: src/components/storage/ProposalActionsSummary.jsx:126 +#: src/components/storage/ProposalActionsSummary.jsx:82 +#: src/components/storage/ProposalActionsSummary.jsx:132 msgid "affecting" msgstr "afectados" -#: src/components/storage/ProposalActionsSummary.jsx:107 +#: src/components/storage/ProposalActionsSummary.jsx:112 msgid "Shrinking partitions is not allowed" msgstr "No se permite reducir las particiones existentes" -#: src/components/storage/ProposalActionsSummary.jsx:111 +#: src/components/storage/ProposalActionsSummary.jsx:116 msgid "Shrinking partitions is allowed" msgstr "Se permite reducir las particiones existentes" -#: src/components/storage/ProposalActionsSummary.jsx:113 +#: src/components/storage/ProposalActionsSummary.jsx:118 msgid "Shrinking some partitions is allowed but not needed" msgstr "Se permite reducir algunas particiones, pero no es necesario" -#: src/components/storage/ProposalActionsSummary.jsx:116 +#: src/components/storage/ProposalActionsSummary.jsx:121 #, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" msgstr[0] "%d partición se reducirá" msgstr[1] "%d particiones se reducirán" -#: src/components/storage/ProposalActionsSummary.jsx:151 +#: src/components/storage/ProposalActionsSummary.jsx:159 msgid "Cannot accommodate the required file systems for installation" msgstr "" "No se pueden acomodar los sistemas de archivos necesarios para la instalación" -#: src/components/storage/ProposalActionsSummary.jsx:160 +#: src/components/storage/ProposalActionsSummary.jsx:167 #, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" msgstr[0] "Comprueba la acción planeada" msgstr[1] "Comprueba las %d acciones planeadas" -#: src/components/storage/ProposalActionsSummary.jsx:179 +#: src/components/storage/ProposalActionsSummary.jsx:182 msgid "Waiting for actions information..." msgstr "Esperando información de acciones..." -#: src/components/storage/ProposalPage.jsx:314 +#: src/components/storage/ProposalPage.jsx:329 msgid "Planned Actions" msgstr "Acciones planeadas" -#: src/components/storage/ProposalResultSection.jsx:42 +#: src/components/storage/ProposalResultSection.jsx:43 msgid "Waiting for information about storage configuration" msgstr "Esperando información sobre la configuración de almacenamiento" -#: src/components/storage/ProposalResultSection.jsx:70 +#: src/components/storage/ProposalResultSection.jsx:73 msgid "Final layout" msgstr "Diseño final" -#: src/components/storage/ProposalResultSection.jsx:71 +#: src/components/storage/ProposalResultSection.jsx:74 msgid "The systems will be configured as displayed below." msgstr "Los sistemas se configurarán como se muestra a continuación." -#: src/components/storage/ProposalResultSection.jsx:78 +#: src/components/storage/ProposalResultSection.jsx:83 msgid "Storage proposal not possible" msgstr "Propuesta de almacenamiento no posible" -#: src/components/storage/ProposalResultTable.jsx:74 +#: src/components/storage/ProposalResultTable.jsx:79 msgid "New" msgstr "Nuevo" #. TRANSLATORS: Label to indicate the device size before resizing, where %s is #. replaced by the original size (e.g., 3.00 GiB). -#: src/components/storage/ProposalResultTable.jsx:98 +#: src/components/storage/ProposalResultTable.jsx:105 #, c-format msgid "Before %s" msgstr "Antes %s" -#: src/components/storage/ProposalResultTable.jsx:121 +#: src/components/storage/ProposalResultTable.jsx:131 msgid "Mount Point" msgstr "Punto de montaje" @@ -1761,7 +1709,7 @@ msgstr "Punto de montaje" msgid "Transactional root file system" msgstr "Sistema de archivos raíz transaccional" -#: src/components/storage/ProposalTransactionalInfo.jsx:48 +#: src/components/storage/ProposalTransactionalInfo.jsx:49 #, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " @@ -1774,7 +1722,7 @@ msgstr "" msgid "Use Btrfs snapshots for the root file system" msgstr "Utilizar instantáneas de Btrfs para el sistema de archivos raíz" -#: src/components/storage/SnapshotsField.jsx:37 +#: src/components/storage/SnapshotsField.jsx:38 msgid "" "Allows to boot to a previous version of the system after configuration " "changes or software upgrades." @@ -1782,118 +1730,111 @@ msgstr "" "Permitir iniciar una versión anterior del sistema después de cambios de " "configuración o actualizaciones de software." -#. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) -#: src/components/storage/SpaceActionsTable.jsx:75 +#: src/components/storage/SpaceActionsTable.jsx:68 #, c-format -msgid "Space action selector for %s" -msgstr "Selector de acción en el espacio para %s" +msgid "Up to %s can be recovered by shrinking the device." +msgstr "Se pueden recuperar hasta %s reduciendo el dispositivo." -#: src/components/storage/SpaceActionsTable.jsx:79 -msgid "Allow resize" -msgstr "Permitir cambio de tamaño" +#: src/components/storage/SpaceActionsTable.jsx:77 +msgid "The device cannot be shrunk:" +msgstr "El dispositivo no se puede reducir:" -#: src/components/storage/SpaceActionsTable.jsx:80 -msgid "Do not modify" -msgstr "No modificar" +#: src/components/storage/SpaceActionsTable.jsx:98 +#, c-format +msgid "Show information about %s" +msgstr "Mostrar información sobre %s" -#: src/components/storage/SpaceActionsTable.jsx:111 +#: src/components/storage/SpaceActionsTable.jsx:172 msgid "The content may be deleted" msgstr "El contenido puede ser eliminado" -#: src/components/storage/SpaceActionsTable.jsx:144 -msgid "Shrinkable" -msgstr "Se puede reducir" - -#: src/components/storage/SpaceActionsTable.jsx:146 +#: src/components/storage/SpaceActionsTable.jsx:204 msgid "Action" msgstr "Acción" -#: src/components/storage/SpaceActionsTable.jsx:162 +#: src/components/storage/SpaceActionsTable.jsx:215 msgid "Actions to find space" msgstr "Acciones para encontrar espacio" -#: src/components/storage/SpacePolicySelection.jsx:170 +#: src/components/storage/SpacePolicySelection.jsx:172 msgid "Space policy" msgstr "Política de espacio" -#: src/components/storage/VolumeDialog.jsx:78 +#: src/components/storage/VolumeDialog.jsx:83 #, c-format msgid "Add %s file system" msgstr "Agregar %s sistema de archivos" -#: src/components/storage/VolumeDialog.jsx:79 +#: src/components/storage/VolumeDialog.jsx:84 #, c-format msgid "Edit %s file system" msgstr "Editar %s sistema de archivos" -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Edit file system" msgstr "Editar sistema de archivos" #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:96 +#: src/components/storage/VolumeDialog.jsx:101 msgid "The type and size of the file system cannot be edited." msgstr "El tipo y el tamaño del sistema de archivos no puede ser editado." -#. TRANSLATORS: Description of a warning. The first %s is replaced by a device name (e.g., -#. /dev/vda) and the second %s is replaced by a mount path (e.g., /home). -#: src/components/storage/VolumeDialog.jsx:99 +#: src/components/storage/VolumeDialog.jsx:105 #, c-format msgid "The current file system on %s is selected to be mounted at %s." msgstr "" "El actual sistema de archivos en %s está seleccionado para ser montado en %s." #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:105 +#: src/components/storage/VolumeDialog.jsx:113 msgid "The size of the file system cannot be edited" msgstr "El tamaño del sistema de archivos no puede ser editado" #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). -#: src/components/storage/VolumeDialog.jsx:107 +#: src/components/storage/VolumeDialog.jsx:115 #, c-format msgid "The file system is allocated at the device %s." msgstr "El sistema de archivos está asignado en el dispositivo %s." -#: src/components/storage/VolumeDialog.jsx:152 +#: src/components/storage/VolumeDialog.jsx:163 msgid "A mount point is required" msgstr "Se requiere un punto de montaje" -#: src/components/storage/VolumeDialog.jsx:179 +#: src/components/storage/VolumeDialog.jsx:190 msgid "The mount point is invalid" msgstr "El punto de montaje no es válido" -#: src/components/storage/VolumeDialog.jsx:207 +#: src/components/storage/VolumeDialog.jsx:218 msgid "A size value is required" msgstr "Se requiere un valor de tamaño" -#: src/components/storage/VolumeDialog.jsx:235 +#: src/components/storage/VolumeDialog.jsx:246 msgid "Minimum size is required" msgstr "Se requiere un tamaño mínimo" -#: src/components/storage/VolumeDialog.jsx:267 +#: src/components/storage/VolumeDialog.jsx:278 msgid "Maximum must be greater than minimum" msgstr "El máximo debe ser mayor que el mínimo" -#: src/components/storage/VolumeDialog.jsx:309 +#: src/components/storage/VolumeDialog.jsx:320 #, c-format msgid "There is already a file system for %s." msgstr "Ya existe un sistema de archivos para %s." -#: src/components/storage/VolumeDialog.jsx:311 +#: src/components/storage/VolumeDialog.jsx:322 msgid "Do you want to edit it?" msgstr "¿Quieres editarlo?" -#: src/components/storage/VolumeDialog.jsx:356 +#: src/components/storage/VolumeDialog.jsx:367 #, c-format msgid "There is a predefined file system for %s." msgstr "Hay un sistema de archivos predefinido para %s." -#: src/components/storage/VolumeDialog.jsx:358 +#: src/components/storage/VolumeDialog.jsx:369 msgid "Do you want to add it?" msgstr "¿Quieres añadirlo?" -#. TRANSLATORS: info about possible file system types. -#: src/components/storage/VolumeFields.jsx:217 +#: src/components/storage/VolumeFields.jsx:225 msgid "" "The options for the file system type depends on the product and the mount " "point." @@ -1901,68 +1842,64 @@ msgstr "" "Las opciones para el tipo de sistema de archivos dependen del producto y del " "punto de montaje." -#: src/components/storage/VolumeFields.jsx:223 +#: src/components/storage/VolumeFields.jsx:232 msgid "More info for file system types" msgstr "Más información para los tipos de sistemas de archivos" #. TRANSLATORS: label for the file system selector. -#: src/components/storage/VolumeFields.jsx:234 +#: src/components/storage/VolumeFields.jsx:243 msgid "File system type" msgstr "Tipo de sistema de archivos" #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:265 +#: src/components/storage/VolumeFields.jsx:274 msgid "the configuration of snapshots" msgstr "la configuración de las instantáneas" -#. TRANSLATORS: item which affects the final computed partition size -#. %s is replaced by a list of mount points like "/home, /boot" -#: src/components/storage/VolumeFields.jsx:270 +#: src/components/storage/VolumeFields.jsx:281 #, c-format msgid "the presence of the file system for %s" msgstr "la presencia del sistema de archivos para %s" #. TRANSLATORS: conjunction for merging two list items -#: src/components/storage/VolumeFields.jsx:272 +#: src/components/storage/VolumeFields.jsx:283 msgid ", " msgstr ", " #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:276 +#: src/components/storage/VolumeFields.jsx:289 msgid "the amount of RAM in the system" msgstr "la cantidad de memoria RAM en el sistema" -#. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeFields.jsx:279 +#: src/components/storage/VolumeFields.jsx:293 #, c-format msgid "The final size depends on %s." msgstr "El tamaño final depende de %s." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeFields.jsx:281 +#: src/components/storage/VolumeFields.jsx:295 msgid " and " msgstr " y " -#. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeFields.jsx:286 +#: src/components/storage/VolumeFields.jsx:302 msgid "Automatically calculated size according to the selected product." msgstr "Tamaño calculado automáticamente según el producto seleccionado." -#: src/components/storage/VolumeFields.jsx:305 +#: src/components/storage/VolumeFields.jsx:321 msgid "Exact size for the file system." msgstr "Tamaño exacto para el sistema de archivos." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeFields.jsx:318 +#: src/components/storage/VolumeFields.jsx:330 msgid "Exact size" msgstr "Tamaño exacto" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeFields.jsx:335 +#: src/components/storage/VolumeFields.jsx:347 msgid "Size unit" msgstr "Unidad de tamaño" -#: src/components/storage/VolumeFields.jsx:363 +#: src/components/storage/VolumeFields.jsx:376 msgid "" "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 " @@ -1973,50 +1910,49 @@ msgstr "" "de archivos será lo más grande posible." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeFields.jsx:370 +#: src/components/storage/VolumeFields.jsx:384 msgid "Minimum" msgstr "Mínimo" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeFields.jsx:381 +#: src/components/storage/VolumeFields.jsx:395 msgid "Minimum desired size" msgstr "Tamaño mínimo deseado" -#: src/components/storage/VolumeFields.jsx:392 +#: src/components/storage/VolumeFields.jsx:406 msgid "Unit for the minimum size" msgstr "Unidad para el tamaño mínimo" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:404 +#: src/components/storage/VolumeFields.jsx:418 msgid "Maximum" msgstr "Máximo" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:416 +#: src/components/storage/VolumeFields.jsx:430 msgid "Maximum desired size" msgstr "Tamaño máximo deseado" -#: src/components/storage/VolumeFields.jsx:426 +#: src/components/storage/VolumeFields.jsx:440 msgid "Unit for the maximum size" msgstr "Unidad para el tamaño máximo" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeFields.jsx:444 +#: src/components/storage/VolumeFields.jsx:458 msgid "Auto" msgstr "Automático" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeFields.jsx:446 +#: src/components/storage/VolumeFields.jsx:460 msgid "Fixed" msgstr "Fijado" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeFields.jsx:448 +#: src/components/storage/VolumeFields.jsx:462 msgid "Range" msgstr "Rango" -#. TRANSLATORS: Description of the dialog for changing the location of a file system. -#: src/components/storage/VolumeLocationDialog.jsx:40 +#: src/components/storage/VolumeLocationDialog.jsx:41 msgid "" "The file systems are allocated at the installation device by default. " "Indicate a custom location to create the file system at a specific device." @@ -2027,39 +1963,39 @@ msgstr "" #. TRANSLATORS: Title of the dialog for changing the location of a file system. %s is replaced #. by a mount path (e.g., /home). -#: src/components/storage/VolumeLocationDialog.jsx:135 +#: src/components/storage/VolumeLocationDialog.jsx:137 #, c-format msgid "Location for %s file system" msgstr "Ubicación del sistema de archivos %s" -#: src/components/storage/VolumeLocationDialog.jsx:145 +#: src/components/storage/VolumeLocationDialog.jsx:147 msgid "Select in which device to allocate the file system" msgstr "Seleccione en qué dispositivo asignar el sistema de archivos" -#: src/components/storage/VolumeLocationDialog.jsx:148 +#: src/components/storage/VolumeLocationDialog.jsx:150 msgid "Select a location" msgstr "Seleccionar una ubicación" -#: src/components/storage/VolumeLocationDialog.jsx:160 +#: src/components/storage/VolumeLocationDialog.jsx:162 msgid "Select how to allocate the file system" msgstr "Seleccionar cómo asignar el sistema de archivos" -#: src/components/storage/VolumeLocationDialog.jsx:165 +#: src/components/storage/VolumeLocationDialog.jsx:167 msgid "Create a new partition" msgstr "Crear una nueva partición" -#: src/components/storage/VolumeLocationDialog.jsx:166 +#: src/components/storage/VolumeLocationDialog.jsx:169 msgid "" "The file system will be allocated as a new partition at the selected disk." msgstr "" "El sistema de archivos se asignará como una nueva partición en el disco " "seleccionado." -#: src/components/storage/VolumeLocationDialog.jsx:175 +#: src/components/storage/VolumeLocationDialog.jsx:179 msgid "Create a dedicated LVM volume group" msgstr "Crear un grupo de volúmenes LVM dedicado" -#: src/components/storage/VolumeLocationDialog.jsx:176 +#: src/components/storage/VolumeLocationDialog.jsx:181 msgid "" "A new volume group will be allocated in the selected disk and the file " "system will be created as a logical volume." @@ -2067,22 +2003,21 @@ msgstr "" "Se asignará un nuevo grupo de volúmenes en el disco seleccionado y el " "sistema de archivos se creará como un volumen lógico." -#: src/components/storage/VolumeLocationDialog.jsx:185 +#: src/components/storage/VolumeLocationDialog.jsx:191 msgid "Format the device" msgstr "Formatear el dispositivo" -#. TRANSLATORS: %s is replaced by a file system type (e.g., Ext4). -#: src/components/storage/VolumeLocationDialog.jsx:188 +#: src/components/storage/VolumeLocationDialog.jsx:195 #, c-format msgid "The selected device will be formatted as %s file system." msgstr "" "El dispositivo seleccionado se formateará como un sistema de archivos %s." -#: src/components/storage/VolumeLocationDialog.jsx:198 +#: src/components/storage/VolumeLocationDialog.jsx:206 msgid "Mount the file system" msgstr "Montar el sistema de archivos" -#: src/components/storage/VolumeLocationDialog.jsx:199 +#: src/components/storage/VolumeLocationDialog.jsx:208 msgid "" "The current file system on the selected device will be mounted without " "formatting the device." @@ -2090,53 +2025,51 @@ msgstr "" "El actual sistema de archivos en el dispositivo seleccionado se montará sin " "formatear el dispositivo." -#: src/components/storage/VolumeLocationSelectorTable.jsx:102 +#: src/components/storage/VolumeLocationSelectorTable.jsx:110 msgid "Usage" msgstr "Uso" -#: src/components/storage/ZFCPDiskForm.jsx:109 +#: src/components/storage/ZFCPDiskForm.jsx:106 msgid "The zFCP disk was not activated." msgstr "El disco zFCP no estaba activado." #. TRANSLATORS: abbrev. World Wide Port Name #: src/components/storage/ZFCPDiskForm.jsx:123 -#: src/components/storage/ZFCPPage.jsx:363 +#: src/components/storage/ZFCPPage.jsx:383 msgid "WWPN" msgstr "WWPN" #. TRANSLATORS: abbrev. Logical Unit Number -#: src/components/storage/ZFCPDiskForm.jsx:134 -#: src/components/storage/ZFCPPage.jsx:364 +#: src/components/storage/ZFCPDiskForm.jsx:131 +#: src/components/storage/ZFCPPage.jsx:384 msgid "LUN" msgstr "LUN" -#: src/components/storage/ZFCPPage.jsx:304 +#: src/components/storage/ZFCPPage.jsx:326 msgid "Auto LUNs Scan" msgstr "Escaneo automático de LUN" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Activated" msgstr "Activado" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Deactivated" msgstr "Desactivado" -#: src/components/storage/ZFCPPage.jsx:418 +#: src/components/storage/ZFCPPage.jsx:437 msgid "No zFCP controllers found." msgstr "No se encontraron controladores zFCP." -#: src/components/storage/ZFCPPage.jsx:419 +#: src/components/storage/ZFCPPage.jsx:438 msgid "Please, try to read the zFCP devices again." msgstr "Por favor, intente leer los dispositivos zFCP nuevamente." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:421 +#: src/components/storage/ZFCPPage.jsx:441 msgid "Read zFCP devices" msgstr "Leer dispositivos zFCP" -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:430 +#: src/components/storage/ZFCPPage.jsx:452 msgid "" "Automatic LUN scan is [enabled]. Activating a controller which is running in " "NPIV mode will automatically configures all its LUNs." @@ -2145,8 +2078,7 @@ msgstr "" "controlador que se ejecuta en modo NPIV configurará automáticamente todos " "sus LUN." -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:433 +#: src/components/storage/ZFCPPage.jsx:457 msgid "" "Automatic LUN scan is [disabled]. LUNs have to be manually configured after " "activating a controller." @@ -2154,38 +2086,36 @@ msgstr "" "La exploración automática de LUN está [deshabilitada]. Los LUN deben " "configurarse manualmente después de activar un controlador." -#: src/components/storage/ZFCPPage.jsx:490 +#: src/components/storage/ZFCPPage.jsx:519 msgid "Activate a zFCP disk" msgstr "Activar un disco zFCP" -#: src/components/storage/ZFCPPage.jsx:529 +#: src/components/storage/ZFCPPage.jsx:553 msgid "Please, try to activate a zFCP controller." msgstr "Por favor, intente activar un controlador zFCP." -#: src/components/storage/ZFCPPage.jsx:536 +#: src/components/storage/ZFCPPage.jsx:559 msgid "Please, try to activate a zFCP disk." msgstr "Por favor, intente activar un disco zFCP." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:538 +#: src/components/storage/ZFCPPage.jsx:562 msgid "Activate zFCP disk" msgstr "Activar disco zFCP" -#: src/components/storage/ZFCPPage.jsx:545 +#: src/components/storage/ZFCPPage.jsx:570 msgid "No zFCP disks found." msgstr "No se encontraron discos zFCP." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:560 +#: src/components/storage/ZFCPPage.jsx:586 msgid "Activate new disk" msgstr "Activar nuevo disco" #. TRANSLATORS: section title -#: src/components/storage/ZFCPPage.jsx:572 +#: src/components/storage/ZFCPPage.jsx:599 msgid "Disks" msgstr "Discos" -#: src/components/storage/device-utils.jsx:88 +#: src/components/storage/device-utils.jsx:92 msgid "Unused space" msgstr "Espacio no utilizado" @@ -2197,70 +2127,70 @@ msgstr "Solo disponible si se proporciona autenticación por destino" msgid "Authentication by target" msgstr "Autenticación por objetivo" -#: src/components/storage/iscsi/AuthFields.jsx:80 -#: src/components/storage/iscsi/AuthFields.jsx:85 -#: src/components/storage/iscsi/AuthFields.jsx:87 -#: src/components/storage/iscsi/AuthFields.jsx:112 -#: src/components/storage/iscsi/AuthFields.jsx:117 -#: src/components/storage/iscsi/AuthFields.jsx:119 +#: src/components/storage/iscsi/AuthFields.jsx:78 +#: src/components/storage/iscsi/AuthFields.jsx:82 +#: src/components/storage/iscsi/AuthFields.jsx:84 +#: src/components/storage/iscsi/AuthFields.jsx:104 +#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:110 msgid "User name" msgstr "Nombre de usuario" -#: src/components/storage/iscsi/AuthFields.jsx:91 -#: src/components/storage/iscsi/AuthFields.jsx:124 +#: src/components/storage/iscsi/AuthFields.jsx:88 +#: src/components/storage/iscsi/AuthFields.jsx:116 msgid "Incorrect user name" msgstr "Nombre de usuario incorrecto" -#: src/components/storage/iscsi/AuthFields.jsx:105 -#: src/components/storage/iscsi/AuthFields.jsx:139 +#: src/components/storage/iscsi/AuthFields.jsx:99 +#: src/components/storage/iscsi/AuthFields.jsx:130 msgid "Incorrect password" msgstr "Contraseña incorrecta" -#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:102 msgid "Authentication by initiator" msgstr "Autenticación por iniciador" -#: src/components/storage/iscsi/AuthFields.jsx:133 +#: src/components/storage/iscsi/AuthFields.jsx:123 msgid "Target Password" msgstr "Contraseña de destino" #. TRANSLATORS: popup title -#: src/components/storage/iscsi/DiscoverForm.jsx:102 +#: src/components/storage/iscsi/DiscoverForm.jsx:94 msgid "Discover iSCSI Targets" msgstr "Descubrir los objetivos iSCSI" -#: src/components/storage/iscsi/DiscoverForm.jsx:112 -#: src/components/storage/iscsi/LoginForm.jsx:73 +#: src/components/storage/iscsi/DiscoverForm.jsx:99 +#: src/components/storage/iscsi/LoginForm.jsx:70 msgid "Make sure you provide the correct values" msgstr "Asegúrese de proporcionar los valores correctos" -#: src/components/storage/iscsi/DiscoverForm.jsx:118 +#: src/components/storage/iscsi/DiscoverForm.jsx:103 msgid "IP address" msgstr "Dirección IP" #. TRANSLATORS: network address -#: src/components/storage/iscsi/DiscoverForm.jsx:125 -#: src/components/storage/iscsi/DiscoverForm.jsx:127 +#: src/components/storage/iscsi/DiscoverForm.jsx:108 +#: src/components/storage/iscsi/DiscoverForm.jsx:110 msgid "Address" msgstr "Dirección" -#: src/components/storage/iscsi/DiscoverForm.jsx:132 +#: src/components/storage/iscsi/DiscoverForm.jsx:115 msgid "Incorrect IP address" msgstr "Dirección IP incorrecta" #. TRANSLATORS: network port number -#: src/components/storage/iscsi/DiscoverForm.jsx:136 -#: src/components/storage/iscsi/DiscoverForm.jsx:143 -#: src/components/storage/iscsi/DiscoverForm.jsx:145 +#: src/components/storage/iscsi/DiscoverForm.jsx:117 +#: src/components/storage/iscsi/DiscoverForm.jsx:122 +#: src/components/storage/iscsi/DiscoverForm.jsx:124 msgid "Port" msgstr "Puerto" -#: src/components/storage/iscsi/DiscoverForm.jsx:150 +#: src/components/storage/iscsi/DiscoverForm.jsx:129 msgid "Incorrect port" msgstr "Puerto incorrecto" #. TRANSLATORS: %s is replaced by the iSCSI target node name -#: src/components/storage/iscsi/EditNodeForm.jsx:50 +#: src/components/storage/iscsi/EditNodeForm.jsx:48 #, c-format msgid "Edit %s" msgstr "Editar %s" @@ -2278,8 +2208,8 @@ msgstr "Nombre del iniciador" #. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 #: src/components/storage/iscsi/InitiatorPresenter.jsx:86 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 -#: src/components/storage/iscsi/NodesPresenter.jsx:124 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 +#: src/components/storage/iscsi/NodesPresenter.jsx:122 msgid "iBFT" msgstr "iBFT" @@ -2294,14 +2224,14 @@ msgid "Initiator" msgstr "Iniciador" #. TRANSLATORS: %s is replaced by the iSCSI target name -#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/storage/iscsi/LoginForm.jsx:66 #, c-format msgid "Login %s" msgstr "Iniciar sesión %s" #. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) -#: src/components/storage/iscsi/LoginForm.jsx:76 -#: src/components/storage/iscsi/LoginForm.jsx:79 +#: src/components/storage/iscsi/LoginForm.jsx:74 +#: src/components/storage/iscsi/LoginForm.jsx:77 msgid "Startup" msgstr "Puesta en marcha" @@ -2323,28 +2253,27 @@ msgstr "Acceder" msgid "Logout" msgstr "Cerrar sesión" -#: src/components/storage/iscsi/NodesPresenter.jsx:101 -#: src/components/storage/iscsi/NodesPresenter.jsx:122 +#: src/components/storage/iscsi/NodesPresenter.jsx:99 +#: src/components/storage/iscsi/NodesPresenter.jsx:120 msgid "Portal" msgstr "Portal" -#: src/components/storage/iscsi/NodesPresenter.jsx:102 -#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/storage/iscsi/NodesPresenter.jsx:100 +#: src/components/storage/iscsi/NodesPresenter.jsx:121 msgid "Interface" msgstr "Interfaz" -#: src/components/storage/iscsi/TargetsSection.jsx:142 +#: src/components/storage/iscsi/TargetsSection.jsx:138 msgid "No iSCSI targets found." msgstr "No se encontraron objetivos iSCSI." -#: src/components/storage/iscsi/TargetsSection.jsx:143 +#: src/components/storage/iscsi/TargetsSection.jsx:140 msgid "" "Please, perform an iSCSI discovery in order to find available iSCSI targets." msgstr "" "Realice una exploación de iSCSI para encontrar objetivos iSCSI disponibles." -#. TRANSLATORS: button label, starts iSCSI discovery -#: src/components/storage/iscsi/TargetsSection.jsx:145 +#: src/components/storage/iscsi/TargetsSection.jsx:144 msgid "Discover iSCSI targets" msgstr "Descubrir objetivos iSCSI" @@ -2354,7 +2283,7 @@ msgid "Discover" msgstr "Descubrir" #. TRANSLATORS: iSCSI targets section title -#: src/components/storage/iscsi/TargetsSection.jsx:170 +#: src/components/storage/iscsi/TargetsSection.jsx:167 msgid "Targets" msgstr "Objetivos" @@ -2449,7 +2378,7 @@ msgstr "con acciones personalizadas" msgid "No user defined yet." msgstr "Ningún usuario definido todavía." -#: src/components/users/FirstUser.jsx:38 +#: src/components/users/FirstUser.jsx:39 msgid "" "Please, be aware that a user must be defined before installing the system to " "be able to log into it." @@ -2457,68 +2386,68 @@ msgstr "" "Tenga en cuenta que se debe definir un usuario antes de instalar el sistema " "para poder iniciar sesión en él." -#: src/components/users/FirstUser.jsx:42 +#: src/components/users/FirstUser.jsx:45 msgid "Define a user now" msgstr "Definir un usuario ahora" -#: src/components/users/FirstUser.jsx:54 -#: src/components/users/FirstUserForm.jsx:210 +#: src/components/users/FirstUser.jsx:58 +#: src/components/users/FirstUserForm.jsx:227 msgid "Full name" msgstr "Nombre completo" -#: src/components/users/FirstUser.jsx:55 -#: src/components/users/FirstUserForm.jsx:224 -#: src/components/users/FirstUserForm.jsx:229 -#: src/components/users/FirstUserForm.jsx:232 +#: src/components/users/FirstUser.jsx:59 +#: src/components/users/FirstUserForm.jsx:241 +#: src/components/users/FirstUserForm.jsx:246 +#: src/components/users/FirstUserForm.jsx:249 msgid "Username" msgstr "Nombre de usuario" -#: src/components/users/FirstUser.jsx:120 -#: src/components/users/RootAuthMethods.jsx:99 -#: src/components/users/RootAuthMethods.jsx:111 +#: src/components/users/FirstUser.jsx:124 +#: src/components/users/RootAuthMethods.jsx:104 +#: src/components/users/RootAuthMethods.jsx:116 msgid "Discard" msgstr "Descartar" -#: src/components/users/FirstUserForm.jsx:46 +#: src/components/users/FirstUserForm.jsx:57 msgid "Username suggestion dropdown" msgstr "Menú desplegable de sugerencias de nombre de usuario" #. TRANSLATORS: dropdown username suggestions -#: src/components/users/FirstUserForm.jsx:61 +#: src/components/users/FirstUserForm.jsx:72 msgid "Use suggested username" msgstr "Usar nombre de usuario sugerido" -#: src/components/users/FirstUserForm.jsx:140 +#: src/components/users/FirstUserForm.jsx:151 msgid "All fields are required" msgstr "Todos los campos son obligatorios" -#: src/components/users/FirstUserForm.jsx:147 +#: src/components/users/FirstUserForm.jsx:158 msgid "Please, try again." msgstr "Por favor, inténtelo de nuevo." -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Create user" msgstr "Crear usuario" -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 msgid "Edit user" msgstr "Editar usuario" -#: src/components/users/FirstUserForm.jsx:214 -#: src/components/users/FirstUserForm.jsx:216 +#: src/components/users/FirstUserForm.jsx:231 +#: src/components/users/FirstUserForm.jsx:233 msgid "User full name" msgstr "Nombre completo del usuario" -#: src/components/users/FirstUserForm.jsx:254 +#: src/components/users/FirstUserForm.jsx:271 msgid "Edit password too" msgstr "Editar contraseña también" -#: src/components/users/FirstUserForm.jsx:269 +#: src/components/users/FirstUserForm.jsx:287 msgid "user autologin" msgstr "inicio de sesión automático del usuario" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:273 +#: src/components/users/FirstUserForm.jsx:291 msgid "Auto-login" msgstr "Inicio de sesión automático" @@ -2526,7 +2455,7 @@ msgstr "Inicio de sesión automático" msgid "No root authentication method defined yet." msgstr "Aún no se ha definido ningún método de autenticación de root." -#: src/components/users/RootAuthMethods.jsx:38 +#: src/components/users/RootAuthMethods.jsx:39 msgid "" "Please, define at least one authentication method for logging into the " "system as root." @@ -2534,56 +2463,54 @@ msgstr "" "Por favor, defina al menos un método de autenticación para iniciar sesión en " "el sistema como root." -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:43 +#: src/components/users/RootAuthMethods.jsx:46 msgid "Set a password" msgstr "Establecer una contraseña" -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:45 +#: src/components/users/RootAuthMethods.jsx:50 msgid "Upload a SSH Public Key" msgstr "Cargar una clave pública SSH" -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Set" msgstr "Establecer" -#: src/components/users/RootAuthMethods.jsx:129 +#: src/components/users/RootAuthMethods.jsx:132 msgid "Already set" msgstr "Ya establecida" -#: src/components/users/RootAuthMethods.jsx:130 -#: src/components/users/RootAuthMethods.jsx:134 +#: src/components/users/RootAuthMethods.jsx:132 +#: src/components/users/RootAuthMethods.jsx:136 msgid "Not set" msgstr "No establecida" #. TRANSLATORS: table header, user authentication method -#: src/components/users/RootAuthMethods.jsx:155 +#: src/components/users/RootAuthMethods.jsx:157 msgid "Method" msgstr "Método" -#: src/components/users/RootAuthMethods.jsx:170 +#: src/components/users/RootAuthMethods.jsx:174 msgid "SSH Key" msgstr "Clave SSH" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Change the root password" msgstr "Cambiar la contraseña de root" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Set a root password" msgstr "Establecer una contraseña de root" -#: src/components/users/RootAuthMethods.jsx:194 -msgid "Add a SSH Public Key for root" -msgstr "Añadir una clave pública SSH para root" - -#: src/components/users/RootAuthMethods.jsx:194 +#: src/components/users/RootAuthMethods.jsx:203 msgid "Edit the SSH Public Key for root" msgstr "Editar la clave pública SSH para root" -#: src/components/users/RootPasswordPopup.jsx:42 +#: src/components/users/RootAuthMethods.jsx:204 +msgid "Add a SSH Public Key for root" +msgstr "Añadir una clave pública SSH para root" + +#: src/components/users/RootPasswordPopup.jsx:43 msgid "Root password" msgstr "Contraseña de root" @@ -2617,6 +2544,37 @@ msgstr "Primer usuario" msgid "Root authentication" msgstr "Autenticación de root" +#~ msgid "Reading file..." +#~ msgstr "Leyendo el archivo..." + +#~ msgid "Cannot read the file" +#~ msgstr "No se puede leer el archivo" + +#~ msgid "Agama Error" +#~ msgstr "Error de Agama" + +#~ msgid "Loading available products, please wait..." +#~ msgstr "Cargando productos disponibles, por favor espere..." + +#, c-format +#~ msgid "There is %d destructive action planned" +#~ msgid_plural "There are %d destructive actions planned" +#~ msgstr[0] "Hay %d acción destructiva planeada" +#~ msgstr[1] "Hay %d acciones destructivas planeadas" + +#, c-format +#~ msgid "Space action selector for %s" +#~ msgstr "Selector de acción en el espacio para %s" + +#~ msgid "Allow resize" +#~ msgstr "Permitir cambio de tamaño" + +#~ msgid "Do not modify" +#~ msgstr "No modificar" + +#~ msgid "Shrinkable" +#~ msgstr "Se puede reducir" + #, fuzzy #~ msgid "Choose a language" #~ msgstr "Cambiar idioma" @@ -2979,9 +2937,6 @@ msgstr "Autenticación de root" #~ msgid "Waiting for information about selected device" #~ msgstr "Esperando información sobre el dispositivo seleccionado" -#~ msgid "Waiting for information about space policy" -#~ msgstr "Esperando información sobre la política de espacio" - #, c-format #~ msgid "" #~ "The filesystem will be allocated as a new partition at the installation " diff --git a/web/po/fr.po b/web/po/fr.po index c9b3ba9f18..ed8256d29a 100644 --- a/web/po/fr.po +++ b/web/po/fr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-30 02:27+0000\n" +"POT-Creation-Date: 2024-07-14 02:32+0000\n" "PO-Revision-Date: 2024-05-30 20:46+0000\n" "Last-Translator: faila fail \n" "Language-Team: French 1;\n" "X-Generator: Weblate 5.5.5\n" -#: src/MainLayout.jsx:40 +#: src/MainLayout.jsx:52 #, fuzzy msgid "Agama" msgstr "À propos d'Agama" -#: src/MainLayout.jsx:82 +#: src/MainLayout.jsx:94 msgid "Change product" msgstr "Changer de produit" @@ -33,12 +33,11 @@ msgstr "Changer de produit" msgid "About" msgstr "À propos d'Agama" -#: src/components/core/About.jsx:71 +#: src/components/core/About.jsx:69 msgid "About Agama" msgstr "À propos d'Agama" -#. TRANSLATORS: content of the "About" popup (1/2) -#: src/components/core/About.jsx:76 +#: src/components/core/About.jsx:74 msgid "" "Agama is an experimental installer for (open)SUSE systems. It is still under " "development so, please, do not use it in production environments. If you " @@ -53,25 +52,16 @@ msgstr "" #. TRANSLATORS: content of the "About" popup (2/2) #. %s is replaced by the project URL -#: src/components/core/About.jsx:88 +#: src/components/core/About.jsx:86 #, c-format msgid "For more information, please visit the project's repository at %s." msgstr "Pour plus d'informations, veuillez consulter le dépôt du projet à %s." -#: src/components/core/About.jsx:94 src/components/core/FileViewer.jsx:81 -#: src/components/core/LogsButton.jsx:123 -#: src/components/software/SoftwarePatternsSelection.jsx:260 +#: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 +#: src/components/software/SoftwarePatternsSelection.jsx:268 msgid "Close" msgstr "Fermer" -#: src/components/core/FileViewer.jsx:66 -msgid "Reading file..." -msgstr "Lecture du fichier..." - -#: src/components/core/FileViewer.jsx:72 -msgid "Cannot read the file" -msgstr "Lecture du fichier impossible" - #: src/components/core/InstallButton.jsx:32 msgid "Confirm Installation" msgstr "Confirmer l'installation" @@ -94,9 +84,9 @@ msgid "Continue" msgstr "Continuer" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:97 -#: src/components/core/Popup.jsx:136 -#: src/components/network/WifiConnectionForm.jsx:131 +#: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 +#: src/components/core/Popup.jsx:132 +#: src/components/network/WifiConnectionForm.jsx:134 msgid "Cancel" msgstr "Annuler" @@ -105,12 +95,12 @@ msgstr "Annuler" msgid "Install" msgstr "Installer" -#: src/components/core/InstallationFinished.jsx:42 +#: src/components/core/InstallationFinished.jsx:48 msgid "TPM sealing requires the new system to be booted directly." msgstr "" "Le scellement via TPM impose que le nouveau système soit démarré directement." -#: src/components/core/InstallationFinished.jsx:47 +#: src/components/core/InstallationFinished.jsx:53 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." @@ -118,16 +108,15 @@ msgstr "" "Si un périphérique local a été utilisé pour exécuter ce programme " "d'installation, retirez-le avant le prochain démarrage." -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "Hide details" msgstr "Masquer les détails" -#: src/components/core/InstallationFinished.jsx:51 +#: src/components/core/InstallationFinished.jsx:57 msgid "See more details" msgstr "Plus de détails" -#. TRANSLATORS: "Trusted Platform Module" is the name of the technology and "TPM" its abbreviation -#: src/components/core/InstallationFinished.jsx:55 +#: src/components/core/InstallationFinished.jsx:62 msgid "" "The final step to configure the Trusted Platform Module (TPM) to " "automatically open encrypted devices will take place during the first boot " @@ -139,29 +128,29 @@ msgstr "" "premier démarrage du nouveau système. Pour que cela fonctionne, la machine " "doit démarrer directement avec le nouveau chargeur d'amorçage." -#: src/components/core/InstallationFinished.jsx:97 +#: src/components/core/InstallationFinished.jsx:107 msgid "Congratulations!" msgstr "Félicitations!" -#: src/components/core/InstallationFinished.jsx:102 +#: src/components/core/InstallationFinished.jsx:116 msgid "The installation on your machine is complete." msgstr "L'installation sur votre machine est terminée." -#: src/components/core/InstallationFinished.jsx:105 +#: src/components/core/InstallationFinished.jsx:119 msgid "At this point you can power off the machine." msgstr "Vous pouvez à présent éteindre la machine." -#: src/components/core/InstallationFinished.jsx:106 +#: src/components/core/InstallationFinished.jsx:121 msgid "At this point you can reboot the machine to log in to the new system." msgstr "" "Vous pouvez à présent redémarrer la machine pour vous connecter au nouveau " "système." -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Finish" msgstr "Terminer" -#: src/components/core/InstallationFinished.jsx:113 +#: src/components/core/InstallationFinished.jsx:130 msgid "Reboot" msgstr "Redémarrer" @@ -170,46 +159,46 @@ msgstr "Redémarrer" msgid "Installing the system, please wait ..." msgstr "Chargement des produits disponibles, veuillez patienter..." -#: src/components/core/InstallerOptions.jsx:83 +#: src/components/core/InstallerOptions.jsx:92 #, fuzzy msgid "Show installer options" msgstr "Masquer les options d'installation" -#: src/components/core/InstallerOptions.jsx:88 +#: src/components/core/InstallerOptions.jsx:95 #, fuzzy msgid "Installer options" msgstr "Options de l'installateur" -#: src/components/core/InstallerOptions.jsx:94 -#: src/components/core/InstallerOptions.jsx:99 -#: src/components/core/InstallerOptions.jsx:100 -#: src/components/l10n/L10nPage.jsx:67 +#: src/components/core/InstallerOptions.jsx:98 +#: src/components/core/InstallerOptions.jsx:102 +#: src/components/core/InstallerOptions.jsx:103 +#: src/components/l10n/L10nPage.jsx:60 msgid "Language" msgstr "Langue" -#: src/components/core/InstallerOptions.jsx:114 -#: src/components/core/InstallerOptions.jsx:121 +#: src/components/core/InstallerOptions.jsx:115 +#: src/components/core/InstallerOptions.jsx:120 #, fuzzy msgid "Keyboard layout" msgstr "Choisir un produit" -#: src/components/core/InstallerOptions.jsx:130 +#: src/components/core/InstallerOptions.jsx:129 #, fuzzy msgid "Cannot be changed in remote installation" msgstr "" "La disposition du clavier ne peut pas être modifiée via l'installation à " "distance" -#: src/components/core/InstallerOptions.jsx:135 -#: src/components/network/IpSettingsForm.jsx:210 -#: src/components/product/ProductRegistrationPage.jsx:89 -#: src/components/storage/BootSelection.jsx:228 -#: src/components/storage/DeviceSelection.jsx:240 -#: src/components/storage/EncryptionSettingsDialog.jsx:138 -#: src/components/storage/SpacePolicySelection.jsx:198 -#: src/components/storage/VolumeDialog.jsx:781 -#: src/components/storage/ZFCPPage.jsx:503 -#: src/components/users/FirstUserForm.jsx:285 +#: src/components/core/InstallerOptions.jsx:142 +#: src/components/network/IpSettingsForm.jsx:228 +#: src/components/product/ProductRegistrationPage.jsx:85 +#: src/components/storage/BootSelection.jsx:250 +#: src/components/storage/DeviceSelection.jsx:254 +#: src/components/storage/EncryptionSettingsDialog.jsx:155 +#: src/components/storage/SpacePolicySelection.jsx:200 +#: src/components/storage/VolumeDialog.jsx:794 +#: src/components/storage/ZFCPPage.jsx:528 +#: src/components/users/FirstUserForm.jsx:303 msgid "Accept" msgstr "Accepter" @@ -218,30 +207,27 @@ msgid "" "Before starting the installation, you need to address the following problems:" msgstr "" -#: src/components/core/ListSearch.jsx:51 +#: src/components/core/ListSearch.jsx:48 msgid "Search" msgstr "Rechercher" -#: src/components/core/LoginPage.jsx:61 +#: src/components/core/LoginPage.jsx:64 msgid "Could not log in. Please, make sure that the password is correct." msgstr "" "Connexion impossible. Veuillez vous assurer que le mot de passe soit correct." -#: src/components/core/LoginPage.jsx:63 +#: src/components/core/LoginPage.jsx:66 msgid "Could not authenticate against the server, please check it." msgstr "" #. TRANSLATORS: Title for a form to provide the password for the root user. %s #. will be replaced by "root" -#: src/components/core/LoginPage.jsx:71 +#: src/components/core/LoginPage.jsx:74 #, c-format msgid "Log in as %s" msgstr "Se connecter en tant que %s" -#. TRANSLATORS: description why root password is needed. The text in the -#. square brackets [] is displayed in bold, use only please, do not translate -#. it and keep the brackets. -#: src/components/core/LoginPage.jsx:76 +#: src/components/core/LoginPage.jsx:80 msgid "The installer requires [root] user privileges." msgstr "" @@ -252,34 +238,34 @@ msgstr "" "Le programme d'installation requiert les privilèges de l'utilisateur %s. " "Veuillez fournir son mot de passe pour vous connecter au système." -#: src/components/core/LoginPage.jsx:98 +#: src/components/core/LoginPage.jsx:96 #, fuzzy msgid "Login form" msgstr "Connexion %s" -#: src/components/core/LoginPage.jsx:104 +#: src/components/core/LoginPage.jsx:102 #, fuzzy msgid "Password input" msgstr "Mot de passe" -#: src/components/core/LoginPage.jsx:113 +#: src/components/core/LoginPage.jsx:111 msgid "Log in" msgstr "Se connecter" -#: src/components/core/LoginPage.jsx:124 +#: src/components/core/LoginPage.jsx:121 msgid "More about this" msgstr "" -#: src/components/core/LogsButton.jsx:103 +#: src/components/core/LogsButton.jsx:101 msgid "Collecting logs..." msgstr "Collecte des journaux..." -#: src/components/core/LogsButton.jsx:103 -#: src/components/core/LogsButton.jsx:106 +#: src/components/core/LogsButton.jsx:101 +#: src/components/core/LogsButton.jsx:104 msgid "Download logs" msgstr "Télécharger les journaux" -#: src/components/core/LogsButton.jsx:112 +#: src/components/core/LogsButton.jsx:111 msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." @@ -287,33 +273,33 @@ msgstr "" "Le navigateur lancera le téléchargement des journaux dès qu'ils seront " "prêts. Veuillez patienter." -#: src/components/core/LogsButton.jsx:120 +#: src/components/core/LogsButton.jsx:121 msgid "Something went wrong while collecting logs. Please, try again." msgstr "" "Un problème s'est produit lors de la collecte des journaux. Veuillez " "réessayer." -#: src/components/core/PasswordAndConfirmationInput.jsx:48 +#: src/components/core/PasswordAndConfirmationInput.jsx:55 msgid "Passwords do not match" msgstr "Les mots de passe ne correspondent pas" -#: src/components/core/PasswordAndConfirmationInput.jsx:72 -#: src/components/network/WifiConnectionForm.jsx:120 -#: src/components/storage/iscsi/AuthFields.jsx:95 -#: src/components/storage/iscsi/AuthFields.jsx:100 -#: src/components/users/RootAuthMethods.jsx:163 +#: src/components/core/PasswordAndConfirmationInput.jsx:79 +#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/storage/iscsi/AuthFields.jsx:90 +#: src/components/storage/iscsi/AuthFields.jsx:94 +#: src/components/users/RootAuthMethods.jsx:165 msgid "Password" msgstr "Mot de passe" -#: src/components/core/PasswordAndConfirmationInput.jsx:85 +#: src/components/core/PasswordAndConfirmationInput.jsx:90 msgid "Password confirmation" msgstr "Confirmation du mot de passe" -#: src/components/core/PasswordInput.jsx:64 +#: src/components/core/PasswordInput.jsx:61 msgid "Password visibility button" msgstr "Bouton de visibilité du mot de passe" -#: src/components/core/Popup.jsx:100 +#: src/components/core/Popup.jsx:92 msgid "Confirm" msgstr "Confirmer" @@ -332,129 +318,117 @@ msgstr "Terminer" msgid "In progress" msgstr "" -#: src/components/core/ProgressReport.jsx:70 +#: src/components/core/ProgressReport.jsx:74 msgid "Pending" msgstr "" -#: src/components/core/ProgressReport.jsx:134 +#: src/components/core/ProgressReport.jsx:138 #, fuzzy msgid "Waiting for progress status..." msgstr "En attente d'un rapport d'avancement" #: src/components/core/RowActions.jsx:64 -#: src/components/storage/PartitionsField.jsx:454 -#: src/components/storage/ProposalActionsSummary.jsx:226 +#: src/components/storage/PartitionsField.jsx:491 +#: src/components/storage/ProposalActionsSummary.jsx:233 msgid "Actions" msgstr "Actions" -#: src/components/core/SectionSkeleton.jsx:29 +#: src/components/core/SectionSkeleton.jsx:27 msgid "Waiting" msgstr "En attente" -#: src/components/core/Selector.jsx:126 -#: src/components/software/SoftwarePatternsSelection.jsx:212 -#, fuzzy -msgid "auto selected" -msgstr "non sélectionné" - -#. TRANSLATORS: page title -#: src/components/core/ServerError.jsx:34 -msgid "Agama Error" -msgstr "Erreur d'Agama" - -#: src/components/core/ServerError.jsx:38 +#: src/components/core/ServerError.jsx:47 #, fuzzy msgid "Cannot connect to Agama server" msgstr "Connexion au serveur Cockpit impossible" -#: src/components/core/ServerError.jsx:43 +#: src/components/core/ServerError.jsx:51 #, fuzzy msgid "Please, check whether it is running." msgstr "" "Impossible de se connecter au service D-Bus. Veuillez vérifier s'il est en " "cours d'exécution." -#. TRANSLATORS: button label -#: src/components/core/ServerError.jsx:51 +#: src/components/core/ServerError.jsx:56 msgid "Reload" msgstr "Recharger" -#: src/components/l10n/KeyboardSelection.jsx:45 +#: src/components/l10n/KeyboardSelection.jsx:41 msgid "Filter by description or keymap code" msgstr "Filtrer par description ou par mappage de clavier" -#: src/components/l10n/KeyboardSelection.jsx:85 +#: src/components/l10n/KeyboardSelection.jsx:71 #, fuzzy msgid "None of the keymaps match the filter." msgstr "Aucun des schémas ne correspond au filtre." -#: src/components/l10n/KeyboardSelection.jsx:92 +#: src/components/l10n/KeyboardSelection.jsx:77 #, fuzzy msgid "Keyboard selection" msgstr "Logiciel %s" -#: src/components/l10n/KeyboardSelection.jsx:107 -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/l10n/LocaleSelection.jsx:107 -#: src/components/l10n/TimezoneSelection.jsx:145 -#: src/components/product/ProductSelectionPage.jsx:101 +#: src/components/l10n/KeyboardSelection.jsx:90 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/l10n/LocaleSelection.jsx:92 +#: src/components/l10n/TimezoneSelection.jsx:125 +#: src/components/product/ProductSelectionPage.jsx:90 msgid "Select" msgstr "Sélectionner" -#: src/components/l10n/L10nPage.jsx:60 src/components/l10n/routes.js:34 -#: src/components/overview/L10nSection.jsx:42 +#: src/components/l10n/L10nPage.jsx:53 +#: src/components/overview/L10nSection.jsx:37 src/routes/l10n.js:38 msgid "Localization" msgstr "Localisation" -#: src/components/l10n/L10nPage.jsx:68 src/components/l10n/L10nPage.jsx:79 -#: src/components/l10n/L10nPage.jsx:90 +#: src/components/l10n/L10nPage.jsx:61 src/components/l10n/L10nPage.jsx:70 +#: src/components/l10n/L10nPage.jsx:80 #, fuzzy msgid "Not selected yet" msgstr "Aucun périphérique n'a encore été sélectionné" -#: src/components/l10n/L10nPage.jsx:71 src/components/l10n/L10nPage.jsx:82 -#: src/components/l10n/L10nPage.jsx:93 -#: src/components/network/NetworkPage.jsx:102 -#: src/components/storage/InstallationDeviceField.jsx:105 -#: src/components/storage/ProposalActionsSummary.jsx:228 -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 +#: src/components/l10n/L10nPage.jsx:83 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/storage/InstallationDeviceField.jsx:108 +#: src/components/storage/ProposalActionsSummary.jsx:238 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Change" msgstr "Changer" -#: src/components/l10n/L10nPage.jsx:78 +#: src/components/l10n/L10nPage.jsx:70 msgid "Keyboard" msgstr "Clavier" -#: src/components/l10n/L10nPage.jsx:89 +#: src/components/l10n/L10nPage.jsx:79 msgid "Time zone" msgstr "Fuseau horaire" -#: src/components/l10n/LocaleSelection.jsx:44 +#: src/components/l10n/LocaleSelection.jsx:39 msgid "Filter by language, territory or locale code" msgstr "Filtrer par langue, territoire ou code local" -#: src/components/l10n/LocaleSelection.jsx:84 +#: src/components/l10n/LocaleSelection.jsx:72 #, fuzzy msgid "None of the locales match the filter." msgstr "Aucun des schémas ne correspond au filtre." -#: src/components/l10n/LocaleSelection.jsx:91 +#: src/components/l10n/LocaleSelection.jsx:78 #, fuzzy msgid "Locale selection" msgstr "Logiciel %s" -#: src/components/l10n/TimezoneSelection.jsx:71 +#: src/components/l10n/TimezoneSelection.jsx:64 msgid "Filter by territory, time zone code or UTC offset" msgstr "Filtrer par territoire, code de fuseau horaire ou décalage UTC" -#: src/components/l10n/TimezoneSelection.jsx:122 +#: src/components/l10n/TimezoneSelection.jsx:101 #, fuzzy msgid "None of the time zones match the filter." msgstr "Aucun des schémas ne correspond au filtre." -#: src/components/l10n/TimezoneSelection.jsx:129 +#: src/components/l10n/TimezoneSelection.jsx:107 #, fuzzy msgid " Timezone selection" msgstr "Changer de fuseau horaire" @@ -464,111 +438,111 @@ msgid "Loading installation environment, please wait." msgstr "Chargement de l'environnement d'installation, veuillez patienter." #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:78 -#: src/components/network/DnsDataList.jsx:84 +#: src/components/network/AddressesDataList.jsx:88 +#: src/components/network/DnsDataList.jsx:95 msgid "Remove" msgstr "Supprimer" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:90 -#: src/components/network/AddressesDataList.jsx:91 +#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:101 #: src/components/network/IpAddressInput.jsx:33 msgid "IP Address" msgstr "Adresse IP" #. TRANSLATORS: input field name -#: src/components/network/AddressesDataList.jsx:99 -#: src/components/network/AddressesDataList.jsx:100 +#: src/components/network/AddressesDataList.jsx:109 +#: src/components/network/AddressesDataList.jsx:110 msgid "Prefix length or netmask" msgstr "Longueur du préfixe ou masque de sous-réseau" -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add an address" msgstr "Ajouter une adresse" #. TRANSLATORS: button label -#: src/components/network/AddressesDataList.jsx:116 +#: src/components/network/AddressesDataList.jsx:126 msgid "Add another address" msgstr "Ajouter une autre adresse" -#: src/components/network/AddressesDataList.jsx:121 +#: src/components/network/AddressesDataList.jsx:131 msgid "Addresses" msgstr "Adresses" -#: src/components/network/AddressesDataList.jsx:123 +#: src/components/network/AddressesDataList.jsx:133 msgid "Addresses data list" msgstr "Liste des données d'adresses" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:67 -#: src/components/network/ConnectionsTable.jsx:95 -#: src/components/storage/ZFCPPage.jsx:361 +#: src/components/network/ConnectionsTable.jsx:64 +#: src/components/network/ConnectionsTable.jsx:92 +#: src/components/storage/ZFCPPage.jsx:381 #: src/components/storage/iscsi/InitiatorForm.jsx:52 #: src/components/storage/iscsi/InitiatorPresenter.jsx:68 #: src/components/storage/iscsi/InitiatorPresenter.jsx:85 -#: src/components/storage/iscsi/NodesPresenter.jsx:100 -#: src/components/storage/iscsi/NodesPresenter.jsx:121 +#: src/components/storage/iscsi/NodesPresenter.jsx:98 +#: src/components/storage/iscsi/NodesPresenter.jsx:119 msgid "Name" msgstr "Nom" #. TRANSLATORS: table header -#: src/components/network/ConnectionsTable.jsx:69 -#: src/components/network/ConnectionsTable.jsx:96 +#: src/components/network/ConnectionsTable.jsx:66 +#: src/components/network/ConnectionsTable.jsx:93 msgid "IP addresses" msgstr "Adresses IP" -#: src/components/network/ConnectionsTable.jsx:77 -#: src/components/network/WifiNetworksListPage.jsx:100 -#: src/components/network/WifiNetworksListPage.jsx:124 -#: src/components/storage/PartitionsField.jsx:320 +#: src/components/network/ConnectionsTable.jsx:74 +#: src/components/network/WifiNetworksListPage.jsx:107 +#: src/components/network/WifiNetworksListPage.jsx:130 +#: src/components/storage/PartitionsField.jsx:347 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 -#: src/components/users/FirstUser.jsx:116 +#: src/components/users/FirstUser.jsx:120 msgid "Edit" msgstr "Modifier" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:80 -#: src/components/network/IpSettingsForm.jsx:136 +#: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/IpSettingsForm.jsx:151 #, c-format msgid "Edit connection %s" msgstr "Modifier la connexion %s" -#: src/components/network/ConnectionsTable.jsx:84 -#: src/components/network/WifiNetworksListPage.jsx:103 -#: src/components/network/WifiNetworksListPage.jsx:127 +#: src/components/network/ConnectionsTable.jsx:81 +#: src/components/network/WifiNetworksListPage.jsx:109 +#: src/components/network/WifiNetworksListPage.jsx:137 msgid "Forget" msgstr "Oublier" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:86 +#: src/components/network/ConnectionsTable.jsx:83 #, c-format msgid "Forget connection %s" msgstr "Oublier la connexion %s" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:101 +#: src/components/network/ConnectionsTable.jsx:98 #, c-format msgid "Actions for connection %s" msgstr "Actions concernant la connexion %s" #. TRANSLATORS: input field name -#: src/components/network/DnsDataList.jsx:75 -#: src/components/network/DnsDataList.jsx:76 +#: src/components/network/DnsDataList.jsx:81 +#: src/components/network/DnsDataList.jsx:82 msgid "Server IP" msgstr "IP serveur" -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add DNS" msgstr "Ajouter un DNS" #. TRANSLATORS: button label -#: src/components/network/DnsDataList.jsx:93 +#: src/components/network/DnsDataList.jsx:104 msgid "Add another DNS" msgstr "Ajouter un autre DNS" -#: src/components/network/DnsDataList.jsx:98 +#: src/components/network/DnsDataList.jsx:109 msgid "DNS" msgstr "DNS" @@ -578,42 +552,42 @@ msgid "IP prefix or netmask" msgstr "Préfixe IP ou masque de sous-réseau" #. TRANSLATORS: error message -#: src/components/network/IpSettingsForm.jsx:90 +#: src/components/network/IpSettingsForm.jsx:104 msgid "At least one address must be provided for selected mode" msgstr "Au moins une adresse doit être fournie pour le mode sélectionné" #. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) -#: src/components/network/IpSettingsForm.jsx:145 -#: src/components/network/IpSettingsForm.jsx:150 -#: src/components/network/IpSettingsForm.jsx:152 +#: src/components/network/IpSettingsForm.jsx:160 +#: src/components/network/IpSettingsForm.jsx:165 +#: src/components/network/IpSettingsForm.jsx:167 msgid "Mode" msgstr "Mode" -#: src/components/network/IpSettingsForm.jsx:156 +#: src/components/network/IpSettingsForm.jsx:174 msgid "Automatic (DHCP)" msgstr "Automatique (DHCP)" #. TRANSLATORS: manual network configuration mode with a static IP address -#: src/components/network/IpSettingsForm.jsx:158 +#: src/components/network/IpSettingsForm.jsx:177 #: src/components/storage/iscsi/NodeStartupOptions.js:25 msgid "Manual" msgstr "Manuel" #. TRANSLATORS: network gateway configuration -#: src/components/network/IpSettingsForm.jsx:166 -#: src/components/network/IpSettingsForm.jsx:169 +#: src/components/network/IpSettingsForm.jsx:185 +#: src/components/network/IpSettingsForm.jsx:188 msgid "Gateway" msgstr "Passerelle" -#: src/components/network/IpSettingsForm.jsx:178 +#: src/components/network/IpSettingsForm.jsx:196 msgid "Gateway can be defined only in 'Manual' mode" msgstr "" -#: src/components/network/NetworkPage.jsx:85 +#: src/components/network/NetworkPage.jsx:93 msgid "No Wi-Fi supported" msgstr "" -#: src/components/network/NetworkPage.jsx:86 +#: src/components/network/NetworkPage.jsx:95 #, fuzzy msgid "" "The system does not support Wi-Fi connections, probably because of missing " @@ -622,44 +596,44 @@ msgstr "" "Le système ne prend pas en charge les connexions WiFi, probablement en " "raison d'un matériel manquant ou désactivé." -#: src/components/network/NetworkPage.jsx:99 +#: src/components/network/NetworkPage.jsx:109 msgid "Wi-Fi" msgstr "" #. TRANSLATORS: button label, connect to a WiFi network -#: src/components/network/NetworkPage.jsx:102 -#: src/components/network/WifiConnectionForm.jsx:128 -#: src/components/network/WifiNetworksListPage.jsx:97 +#: src/components/network/NetworkPage.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:130 +#: src/components/network/WifiNetworksListPage.jsx:105 msgid "Connect" msgstr "Se connecter" -#: src/components/network/NetworkPage.jsx:109 +#: src/components/network/NetworkPage.jsx:119 #, fuzzy, c-format msgid "Conected to %s" msgstr "Connecté (%s)" -#: src/components/network/NetworkPage.jsx:114 +#: src/components/network/NetworkPage.jsx:126 #, fuzzy msgid "No connected yet" msgstr "Aucun périphérique n'a encore été sélectionné" -#: src/components/network/NetworkPage.jsx:115 +#: src/components/network/NetworkPage.jsx:127 #, fuzzy msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." msgstr "" "Le système n'a pas encore été configuré pour se connecter à un réseau WiFi." -#: src/components/network/NetworkPage.jsx:136 +#: src/components/network/NetworkPage.jsx:156 msgid "Wired" msgstr "" -#: src/components/network/NetworkPage.jsx:139 +#: src/components/network/NetworkPage.jsx:160 #, fuzzy msgid "No wired connections found" msgstr "Aucune connexion filaire n'a été trouvée." -#: src/components/network/NetworkPage.jsx:149 +#: src/components/network/NetworkPage.jsx:173 #: src/components/network/routes.js:59 msgid "Network" msgstr "Réseau" @@ -675,16 +649,16 @@ msgstr "Aucun" msgid "WPA & WPA2 Personal" msgstr "WPA & WPA2 Personnel" -#: src/components/network/WifiConnectionForm.jsx:86 -#: src/components/product/ProductRegistrationPage.jsx:69 -#: src/components/storage/ZFCPDiskForm.jsx:108 -#: src/components/storage/iscsi/DiscoverForm.jsx:110 -#: src/components/storage/iscsi/LoginForm.jsx:72 -#: src/components/users/FirstUserForm.jsx:203 +#: src/components/network/WifiConnectionForm.jsx:85 +#: src/components/product/ProductRegistrationPage.jsx:68 +#: src/components/storage/ZFCPDiskForm.jsx:105 +#: src/components/storage/iscsi/DiscoverForm.jsx:98 +#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/users/FirstUserForm.jsx:217 msgid "Something went wrong" msgstr "Quelque chose n'a pas fonctionné" -#: src/components/network/WifiConnectionForm.jsx:87 +#: src/components/network/WifiConnectionForm.jsx:86 msgid "Please, review provided settings and try again." msgstr "Veuillez vérifier les paramètres fournis et réessayer." @@ -695,109 +669,109 @@ msgid "SSID" msgstr "SSID" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:104 -#: src/components/network/WifiConnectionForm.jsx:107 +#: src/components/network/WifiConnectionForm.jsx:105 +#: src/components/network/WifiConnectionForm.jsx:108 msgid "Security" msgstr "Sécurité" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:116 +#: src/components/network/WifiConnectionForm.jsx:117 msgid "WPA Password" msgstr "Mot de passe WPA" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:51 -#: src/components/network/WifiNetworksListPage.jsx:111 +#: src/components/network/WifiNetworksListPage.jsx:63 +#: src/components/network/WifiNetworksListPage.jsx:117 msgid "Connecting" msgstr "Connexion" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:54 -#: src/components/network/WifiNetworksListPage.jsx:115 -#: src/components/network/WifiNetworksListPage.jsx:154 +#: src/components/network/WifiNetworksListPage.jsx:66 +#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:164 msgid "Connected" msgstr "Connecté" #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:59 -#: src/components/network/WifiNetworksListPage.jsx:113 +#: src/components/network/WifiNetworksListPage.jsx:71 +#: src/components/network/WifiNetworksListPage.jsx:119 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" msgstr "Déconnecté" -#: src/components/network/WifiNetworksListPage.jsx:121 +#: src/components/network/WifiNetworksListPage.jsx:127 #, fuzzy msgid "Disconnect" msgstr "Déconnecté" -#: src/components/network/WifiNetworksListPage.jsx:142 +#: src/components/network/WifiNetworksListPage.jsx:150 #, fuzzy msgid "Connect to a hidden network" msgstr "Se connecter à un réseau caché" -#: src/components/network/WifiNetworksListPage.jsx:153 +#: src/components/network/WifiNetworksListPage.jsx:161 #, fuzzy msgid "configured" msgstr "Ne pas modifier" -#: src/components/network/WifiNetworksListPage.jsx:244 +#: src/components/network/WifiNetworksListPage.jsx:265 msgid "Connect to hidden network" msgstr "Se connecter à un réseau caché" -#: src/components/network/WifiSelectorPage.jsx:131 +#: src/components/network/WifiSelectorPage.jsx:136 msgid "Connect to a Wi-Fi network" msgstr "Se connecter à un réseau Wi-Fi" #. TRANSLATORS: %s will be replaced by a language name and territory, example: #. "English (United States)". -#: src/components/overview/L10nSection.jsx:38 +#: src/components/overview/L10nSection.jsx:33 #, c-format msgid "The system will use %s as its default language." msgstr "Le système utilisera %s comme langue par défaut." -#: src/components/overview/OverviewPage.jsx:45 +#: src/components/overview/OverviewPage.jsx:47 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "Utilisateurs" -#: src/components/overview/OverviewPage.jsx:46 +#: src/components/overview/OverviewPage.jsx:48 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:290 +#: src/components/storage/ProposalPage.jsx:307 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "Stockage" -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/overview/SoftwareSection.jsx:86 #: src/components/software/SoftwarePage.jsx:155 #: src/components/software/routes.js:32 msgid "Software" msgstr "Logiciel" -#: src/components/overview/OverviewPage.jsx:52 +#: src/components/overview/OverviewPage.jsx:54 #, fuzzy msgid "Ready for installation" msgstr "Confirmer l'installation" -#: src/components/overview/OverviewPage.jsx:102 +#: src/components/overview/OverviewPage.jsx:104 #, fuzzy msgid "Installation" msgstr "Installation" -#: src/components/overview/OverviewPage.jsx:103 +#: src/components/overview/OverviewPage.jsx:105 msgid "Before installing, please check the following problems." msgstr "" -#: src/components/overview/OverviewPage.jsx:114 +#: src/components/overview/OverviewPage.jsx:116 #, fuzzy msgid "" "Take your time to check your configuration before starting the installation " "process." msgstr "réduction des partitions du périphérique d'installation" -#: src/components/overview/OverviewPage.jsx:123 +#: src/components/overview/OverviewPage.jsx:125 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -883,14 +857,14 @@ msgstr "" "Installer en utilisant le périphérique %s en réduisant les partitions " "existantes si nécessaire" -#: src/components/overview/StorageSection.jsx:175 -#: src/components/storage/InstallationDeviceField.jsx:63 +#: src/components/overview/StorageSection.jsx:179 +#: src/components/storage/InstallationDeviceField.jsx:66 msgid "No device selected yet" msgstr "Aucun périphérique n'a encore été sélectionné" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:182 +#: src/components/overview/StorageSection.jsx:186 #, c-format msgid "Install using device %s shrinking existing partitions as needed" msgstr "" @@ -899,7 +873,7 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:186 +#: src/components/overview/StorageSection.jsx:190 #, c-format msgid "Install using device %s without modifying existing partitions" msgstr "" @@ -908,7 +882,7 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:190 +#: src/components/overview/StorageSection.jsx:194 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" @@ -916,7 +890,7 @@ msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:195 +#: src/components/overview/StorageSection.jsx:199 #, fuzzy, c-format msgid "Install using device %s with a custom strategy to find the needed space" msgstr "" @@ -932,19 +906,15 @@ msgstr "" msgid "Register %s" msgstr "Enregistrer %s" -#: src/components/product/ProductRegistrationPage.jsx:74 +#: src/components/product/ProductRegistrationPage.jsx:73 msgid "Registration code" msgstr "Code d'inscription" -#: src/components/product/ProductRegistrationPage.jsx:77 +#: src/components/product/ProductRegistrationPage.jsx:76 msgid "Email" msgstr "E-mail" -#: src/components/product/ProductSelectionPage.jsx:58 -msgid "Loading available products, please wait..." -msgstr "Chargement des produits disponibles, veuillez patienter..." - -#: src/components/product/ProductSelectionProgress.jsx:53 +#: src/components/product/ProductSelectionProgress.jsx:49 #, fuzzy msgid "Configuring the product, please wait ..." msgstr "Chargement des produits disponibles, veuillez patienter..." @@ -964,7 +934,7 @@ msgid "Encrypted Device" msgstr "Appareil chiffré" #. TRANSLATORS: field label -#: src/components/questions/LuksActivationQuestion.jsx:69 +#: src/components/questions/LuksActivationQuestion.jsx:67 msgid "Encryption Password" msgstr "Mot de passe de chiffrement" @@ -986,18 +956,23 @@ msgstr "Choisir le fuseau horaire" msgid "Change selection" msgstr "Changer de fuseau horaire" -#: src/components/software/SoftwarePatternsSelection.jsx:230 +#: src/components/software/SoftwarePatternsSelection.jsx:223 +#, fuzzy +msgid "auto selected" +msgstr "non sélectionné" + +#: src/components/software/SoftwarePatternsSelection.jsx:241 msgid "None of the patterns match the filter." msgstr "Aucun des schémas ne correspond au filtre." -#: src/components/software/SoftwarePatternsSelection.jsx:238 +#: src/components/software/SoftwarePatternsSelection.jsx:248 #, fuzzy msgid "Software selection" msgstr "Logiciel %s" #. TRANSLATORS: search field placeholder text -#: src/components/software/SoftwarePatternsSelection.jsx:241 -#: src/components/software/SoftwarePatternsSelection.jsx:242 +#: src/components/software/SoftwarePatternsSelection.jsx:251 +#: src/components/software/SoftwarePatternsSelection.jsx:252 msgid "Filter by pattern title or description" msgstr "" @@ -1008,7 +983,7 @@ msgstr "" msgid "Installation will take %s." msgstr "L'installation utilisera %s" -#: src/components/software/UsedSize.jsx:38 +#: src/components/software/UsedSize.jsx:37 msgid "This space includes the base system and the selected software patterns." msgstr "" @@ -1017,23 +992,22 @@ msgstr "" msgid "Change boot options" msgstr "Changer de fuseau horaire" -#: src/components/storage/BootConfigField.jsx:87 +#: src/components/storage/BootConfigField.jsx:81 msgid "Installation will not configure partitions for booting." msgstr "L'installation ne configure pas les partitions de démarrage." -#: src/components/storage/BootConfigField.jsx:89 +#: src/components/storage/BootConfigField.jsx:85 #, fuzzy msgid "" "Installation will configure partitions for booting at the installation disk." msgstr "réduction des partitions du périphérique d'installation" -#. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/BootConfigField.jsx:92 +#: src/components/storage/BootConfigField.jsx:89 #, c-format msgid "Installation will configure partitions for booting at %s." msgstr "L'installation configurera les partitions pour amorçer à partir de %s." -#: src/components/storage/BootSelection.jsx:127 +#: src/components/storage/BootSelection.jsx:132 msgid "" "To ensure the new system is able to boot, the installer may need to create " "or configure some partitions in the appropriate disk." @@ -1042,49 +1016,49 @@ msgstr "" "d'installation peut avoir besoin de créer ou de configurer certaines " "partitions sur le disque approprié." -#: src/components/storage/BootSelection.jsx:133 +#: src/components/storage/BootSelection.jsx:138 msgid "Partitions to boot will be allocated at the installation disk." msgstr "" "Les partitions pour le démarrage seront allouées sur le disque " "d'installation." #. TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") -#: src/components/storage/BootSelection.jsx:138 +#: src/components/storage/BootSelection.jsx:143 #, c-format msgid "Partitions to boot will be allocated at the installation disk (%s)." msgstr "" "Les partitions de démarrage seront allouées sur le disque d'installation " "(%s)." -#: src/components/storage/BootSelection.jsx:154 +#: src/components/storage/BootSelection.jsx:159 #, fuzzy msgid "Select booting partition" msgstr "Sélectionnez ce qu'il faut faire pour chaque partition." -#: src/components/storage/BootSelection.jsx:170 +#: src/components/storage/BootSelection.jsx:180 #: src/components/storage/iscsi/NodeStartupOptions.js:27 msgid "Automatic" msgstr "Automatique" -#: src/components/storage/BootSelection.jsx:183 +#: src/components/storage/BootSelection.jsx:198 #, fuzzy msgid "Select a disk" msgstr "Sélectionner une valeur" -#: src/components/storage/BootSelection.jsx:189 +#: src/components/storage/BootSelection.jsx:204 msgid "Partitions to boot will be allocated at the following device." msgstr "Les partitions à amorcer seront attribuées au périphérique suivant." -#: src/components/storage/BootSelection.jsx:192 +#: src/components/storage/BootSelection.jsx:207 msgid "Choose a disk for placing the boot loader" msgstr "Choisir un disque pour placer le chargeur d'amorçage" -#: src/components/storage/BootSelection.jsx:210 +#: src/components/storage/BootSelection.jsx:230 #, fuzzy msgid "Do not configure" msgstr "Ne pas modifier" -#: src/components/storage/BootSelection.jsx:215 +#: src/components/storage/BootSelection.jsx:236 msgid "" "No partitions will be automatically configured for booting. Use with caution." msgstr "" @@ -1095,126 +1069,118 @@ msgstr "" msgid "Waiting for progress report" msgstr "En attente d'un rapport d'avancement" -#: src/components/storage/DASDFormatProgress.jsx:68 +#: src/components/storage/DASDFormatProgress.jsx:67 msgid "Formatting DASD devices" msgstr "Formatage des périphériques DASD" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "No" msgstr "Non" -#: src/components/storage/DASDTable.jsx:56 -#: src/components/storage/ZFCPPage.jsx:319 +#: src/components/storage/DASDTable.jsx:62 +#: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "Yes" msgstr "Oui" -#: src/components/storage/DASDTable.jsx:63 -#: src/components/storage/ZFCPDiskForm.jsx:112 -#: src/components/storage/ZFCPPage.jsx:302 -#: src/components/storage/ZFCPPage.jsx:362 +#: src/components/storage/DASDTable.jsx:69 +#: src/components/storage/ZFCPDiskForm.jsx:110 +#: src/components/storage/ZFCPPage.jsx:324 +#: src/components/storage/ZFCPPage.jsx:382 msgid "Channel ID" msgstr "ID du canal" #. TRANSLATORS: table header -#: src/components/storage/DASDTable.jsx:64 -#: src/components/storage/ZFCPPage.jsx:303 -#: src/components/storage/iscsi/NodesPresenter.jsx:104 -#: src/components/storage/iscsi/NodesPresenter.jsx:125 -#: src/components/users/RootAuthMethods.jsx:157 +#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/ZFCPPage.jsx:325 +#: src/components/storage/iscsi/NodesPresenter.jsx:102 +#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/users/RootAuthMethods.jsx:159 msgid "Status" msgstr "Statut" -#: src/components/storage/DASDTable.jsx:65 -#: src/components/storage/DeviceSelectorTable.jsx:186 -#: src/components/storage/ProposalResultTable.jsx:120 -#: src/components/storage/SpaceActionsTable.jsx:141 -#: src/components/storage/VolumeLocationSelectorTable.jsx:100 +#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DeviceSelectorTable.jsx:197 +#: src/components/storage/ProposalResultTable.jsx:130 +#: src/components/storage/SpaceActionsTable.jsx:200 +#: src/components/storage/VolumeLocationSelectorTable.jsx:107 msgid "Device" msgstr "Périphérique" -#: src/components/storage/DASDTable.jsx:66 +#: src/components/storage/DASDTable.jsx:72 msgid "Type" msgstr "Type" #. TRANSLATORS: table header, the column contains "Yes"/"No" values #. for the DIAG access mode (special disk access mode on IBM mainframes), #. usually keep untranslated -#: src/components/storage/DASDTable.jsx:70 +#: src/components/storage/DASDTable.jsx:76 msgid "DIAG" msgstr "DIAG" -#: src/components/storage/DASDTable.jsx:71 +#: src/components/storage/DASDTable.jsx:77 msgid "Formatted" msgstr "Formaté" -#: src/components/storage/DASDTable.jsx:72 +#: src/components/storage/DASDTable.jsx:78 msgid "Partition Info" msgstr "Informations sur la partition" #. TRANSLATORS: drop down menu label -#: src/components/storage/DASDTable.jsx:107 +#: src/components/storage/DASDTable.jsx:115 msgid "Perform an action" msgstr "Effectuer une action" -#. TRANSLATORS: drop down menu action, activate the device -#: src/components/storage/DASDTable.jsx:113 -#: src/components/storage/ZFCPPage.jsx:333 +#: src/components/storage/DASDTable.jsx:122 +#: src/components/storage/ZFCPPage.jsx:353 msgid "Activate" msgstr "Activer" -#. TRANSLATORS: drop down menu action, deactivate the device -#: src/components/storage/DASDTable.jsx:115 -#: src/components/storage/ZFCPPage.jsx:375 +#: src/components/storage/DASDTable.jsx:126 +#: src/components/storage/ZFCPPage.jsx:395 msgid "Deactivate" msgstr "Désactiver" -#. TRANSLATORS: drop down menu action, enable DIAG access method -#: src/components/storage/DASDTable.jsx:118 +#: src/components/storage/DASDTable.jsx:131 msgid "Set DIAG On" msgstr "Activer le diagnostic" -#. TRANSLATORS: drop down menu action, disable DIAG access method -#: src/components/storage/DASDTable.jsx:120 +#: src/components/storage/DASDTable.jsx:135 msgid "Set DIAG Off" msgstr "Désactiver le diagnostic" -#. TRANSLATORS: drop down menu action, format the disk -#: src/components/storage/DASDTable.jsx:123 +#: src/components/storage/DASDTable.jsx:140 msgid "Format" msgstr "Format" -#: src/components/storage/DASDTable.jsx:223 -#: src/components/storage/DASDTable.jsx:224 +#: src/components/storage/DASDTable.jsx:261 +#: src/components/storage/DASDTable.jsx:262 msgid "Filter by min channel" msgstr "Filtrer par canal minimal" -#: src/components/storage/DASDTable.jsx:231 +#: src/components/storage/DASDTable.jsx:269 msgid "Remove min channel filter" msgstr "Supprimer le filtre canal minimal" -#: src/components/storage/DASDTable.jsx:244 -#: src/components/storage/DASDTable.jsx:245 +#: src/components/storage/DASDTable.jsx:283 +#: src/components/storage/DASDTable.jsx:284 msgid "Filter by max channel" msgstr "Filtrer par canal maximal" -#: src/components/storage/DASDTable.jsx:252 +#: src/components/storage/DASDTable.jsx:291 msgid "Remove max channel filter" msgstr "Supprimer le filtre du canal maximal" -#: src/components/storage/DeviceSelection.jsx:101 +#: src/components/storage/DeviceSelection.jsx:108 #, fuzzy msgid "Loading data, please wait a second..." msgstr "Chargement des produits disponibles, veuillez patienter..." -#. TRANSLATORS: description for using plain partitions for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:136 +#: src/components/storage/DeviceSelection.jsx:144 #, fuzzy msgid "" "The file systems will be allocated by default as [new partitions in the " @@ -1223,10 +1189,7 @@ msgstr "" "Configuration du groupe de volumes système. Tous les systèmes de fichiers " "seront créés dans un volume logique du groupe de volume système." -#. TRANSLATORS: description for using logical volumes for installing the -#. system, the text in the square brackets [] is displayed in bold, use only -#. one pair in the translation -#: src/components/storage/DeviceSelection.jsx:141 +#: src/components/storage/DeviceSelection.jsx:151 msgid "" "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 " @@ -1237,142 +1200,142 @@ msgstr "" "correspondants seront créés à la demande en tant que nouvelles partitions " "sur les périphériques sélectionnés." -#: src/components/storage/DeviceSelection.jsx:149 +#: src/components/storage/DeviceSelection.jsx:160 #, fuzzy msgid "Select installation device" msgstr "Périphérique d'installation" -#: src/components/storage/DeviceSelection.jsx:155 +#: src/components/storage/DeviceSelection.jsx:166 #, fuzzy msgid "Install new system on" msgstr "Options de l'installateur" -#: src/components/storage/DeviceSelection.jsx:158 +#: src/components/storage/DeviceSelection.jsx:169 #, fuzzy msgid "An existing disk" msgstr "Réduire les partitions existantes" -#: src/components/storage/DeviceSelection.jsx:167 +#: src/components/storage/DeviceSelection.jsx:178 #, fuzzy msgid "A new LVM Volume Group" msgstr "Groupe de volume système" -#: src/components/storage/DeviceSelection.jsx:192 +#: src/components/storage/DeviceSelection.jsx:203 msgid "Device selector for target disk" msgstr "Sélecteur de périphérique pour le disque cible" -#: src/components/storage/DeviceSelection.jsx:215 +#: src/components/storage/DeviceSelection.jsx:226 #, fuzzy msgid "Device selector for new LVM volume group" msgstr "Périphériques pour la création du groupe de volumes" -#: src/components/storage/DeviceSelection.jsx:228 +#: src/components/storage/DeviceSelection.jsx:242 msgid "Prepare more devices by configuring advanced" msgstr "" -#: src/components/storage/DeviceSelection.jsx:229 +#: src/components/storage/DeviceSelection.jsx:243 #, fuzzy msgid "storage techs" msgstr "technologies de stockage" #. TRANSLATORS: multipath device type -#: src/components/storage/DeviceSelectorTable.jsx:57 +#: src/components/storage/DeviceSelectorTable.jsx:61 msgid "Multipath" msgstr "Chemins multiples" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/DeviceSelectorTable.jsx:62 +#: src/components/storage/DeviceSelectorTable.jsx:66 #, c-format msgid "DASD %s" msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/DeviceSelectorTable.jsx:67 +#: src/components/storage/DeviceSelectorTable.jsx:71 #, c-format msgid "Software %s" msgstr "Logiciel %s" -#: src/components/storage/DeviceSelectorTable.jsx:72 +#: src/components/storage/DeviceSelectorTable.jsx:76 msgid "SD Card" msgstr "Carte SD" #. TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" -#: src/components/storage/DeviceSelectorTable.jsx:77 +#: src/components/storage/DeviceSelectorTable.jsx:81 #, c-format msgid "%s disk" msgstr "Disque %s" -#: src/components/storage/DeviceSelectorTable.jsx:78 +#: src/components/storage/DeviceSelectorTable.jsx:82 #, fuzzy msgid "Disk" msgstr "Disques" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:98 +#: src/components/storage/DeviceSelectorTable.jsx:102 #, c-format msgid "Members: %s" msgstr "Membres : %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/DeviceSelectorTable.jsx:107 +#: src/components/storage/DeviceSelectorTable.jsx:111 #, c-format msgid "Devices: %s" msgstr "Périphériques : %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/DeviceSelectorTable.jsx:116 +#: src/components/storage/DeviceSelectorTable.jsx:120 #, c-format msgid "Wires: %s" msgstr "Chemins: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/DeviceSelectorTable.jsx:152 +#: src/components/storage/DeviceSelectorTable.jsx:155 #, c-format msgid "%s with %d partitions" msgstr "%s avec %d partitions" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/DeviceSelectorTable.jsx:158 -#: src/components/storage/SpaceActionsTable.jsx:114 +#: src/components/storage/DeviceSelectorTable.jsx:161 +#: src/components/storage/SpaceActionsTable.jsx:175 msgid "No content found" msgstr "Aucun contenu n'a été trouvé" -#: src/components/storage/DeviceSelectorTable.jsx:187 -#: src/components/storage/PartitionsField.jsx:450 -#: src/components/storage/ProposalResultTable.jsx:122 -#: src/components/storage/SpaceActionsTable.jsx:142 -#: src/components/storage/VolumeLocationSelectorTable.jsx:101 +#: src/components/storage/DeviceSelectorTable.jsx:198 +#: src/components/storage/PartitionsField.jsx:487 +#: src/components/storage/ProposalResultTable.jsx:132 +#: src/components/storage/SpaceActionsTable.jsx:201 +#: src/components/storage/VolumeLocationSelectorTable.jsx:108 msgid "Details" msgstr "Détails" -#: src/components/storage/DeviceSelectorTable.jsx:188 -#: src/components/storage/PartitionsField.jsx:451 -#: src/components/storage/ProposalResultTable.jsx:123 -#: src/components/storage/SpaceActionsTable.jsx:143 -#: src/components/storage/VolumeFields.jsx:474 -#: src/components/storage/VolumeLocationSelectorTable.jsx:103 +#: src/components/storage/DeviceSelectorTable.jsx:199 +#: src/components/storage/PartitionsField.jsx:488 +#: src/components/storage/ProposalResultTable.jsx:133 +#: src/components/storage/SpaceActionsTable.jsx:202 +#: src/components/storage/VolumeFields.jsx:488 +#: src/components/storage/VolumeLocationSelectorTable.jsx:113 msgid "Size" msgstr "Taille" -#: src/components/storage/DevicesTechMenu.jsx:44 +#: src/components/storage/DevicesTechMenu.jsx:38 msgid "Manage and format" msgstr "Gérer et formater" -#: src/components/storage/DevicesTechMenu.jsx:62 +#: src/components/storage/DevicesTechMenu.jsx:52 msgid "Activate disks" msgstr "Activer les disques" -#: src/components/storage/DevicesTechMenu.jsx:64 +#: src/components/storage/DevicesTechMenu.jsx:53 msgid "zFCP" msgstr "zFCP" -#: src/components/storage/DevicesTechMenu.jsx:80 +#: src/components/storage/DevicesTechMenu.jsx:66 msgid "Connect to iSCSI targets" msgstr "Se connecter aux cibles iSCSI" -#: src/components/storage/DevicesTechMenu.jsx:82 +#: src/components/storage/DevicesTechMenu.jsx:67 #: src/components/storage/routes.js:37 msgid "iSCSI" msgstr "iSCSI" @@ -1383,7 +1346,7 @@ msgstr "iSCSI" msgid "Encryption" msgstr "Utiliser le chiffrement" -#: src/components/storage/EncryptionField.jsx:39 +#: src/components/storage/EncryptionField.jsx:40 #, fuzzy msgid "" "Protection for the information stored at the device, including data, " @@ -1393,28 +1356,28 @@ msgstr "" "stockées sur l'appareil, y compris les données, les programmes et les " "fichiers système." -#: src/components/storage/EncryptionField.jsx:42 +#: src/components/storage/EncryptionField.jsx:44 msgid "disabled" msgstr "désactivé" -#: src/components/storage/EncryptionField.jsx:43 +#: src/components/storage/EncryptionField.jsx:45 msgid "enabled" msgstr "activée" -#: src/components/storage/EncryptionField.jsx:44 +#: src/components/storage/EncryptionField.jsx:46 msgid "using TPM unlocking" msgstr "utiliser le déverrouillage TPM" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 #, fuzzy msgid "Enable" msgstr "activée" -#: src/components/storage/EncryptionField.jsx:58 +#: src/components/storage/EncryptionField.jsx:60 msgid "Modify" msgstr "" -#: src/components/storage/EncryptionSettingsDialog.jsx:37 +#: src/components/storage/EncryptionSettingsDialog.jsx:38 msgid "" "Full Disk Encryption (FDE) allows to protect the information stored at the " "device, including data, programs, and system files." @@ -1424,15 +1387,13 @@ msgstr "" "fichiers système." #. TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation -#: src/components/storage/EncryptionSettingsDialog.jsx:40 +#: src/components/storage/EncryptionSettingsDialog.jsx:42 #, fuzzy msgid "" "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot" msgstr "Utiliser le TPM pour décrypter automatiquement à chaque démarrage" -#. TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing -#. 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear. -#: src/components/storage/EncryptionSettingsDialog.jsx:43 +#: src/components/storage/EncryptionSettingsDialog.jsx:46 #, fuzzy msgid "" "The password will not be needed to boot and access the data if the TPM can " @@ -1444,13 +1405,13 @@ msgstr "" "l'intégrité du système. Le scellement par TPM exige que le nouveau système " "soit démarré directement lors de sa première exécution." -#: src/components/storage/EncryptionSettingsDialog.jsx:114 +#: src/components/storage/EncryptionSettingsDialog.jsx:129 #, fuzzy msgid "Encrypt the system" msgstr "Modifier le système de fichiers" #: src/components/storage/InstallationDeviceField.jsx:36 -#: src/components/storage/VolumeLocationSelectorTable.jsx:58 +#: src/components/storage/VolumeLocationSelectorTable.jsx:61 msgid "Installation device" msgstr "Périphérique d'installation" @@ -1473,71 +1434,70 @@ msgstr "Créer une nouvelle partition" msgid "File systems created at a new LVM volume group" msgstr "Périphériques pour la création du groupe de volumes" -#. TRANSLATORS: %s is the disk used for the LVM physical volumes (eg. "/dev/sda, 80 GiB) -#: src/components/storage/InstallationDeviceField.jsx:59 +#: src/components/storage/InstallationDeviceField.jsx:60 #, fuzzy, c-format msgid "File systems created at a new LVM volume group on %s" msgstr "nouveau groupe de volume LVM sur %s" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:73 +#: src/components/storage/PartitionsField.jsx:84 #, fuzzy, c-format msgid "at least %s" msgstr "Au moins %s" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:97 +#: src/components/storage/PartitionsField.jsx:108 #, fuzzy, c-format msgid "Transactional Btrfs root volume (%s)" msgstr "Système de fichiers root transactionnel" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:99 +#: src/components/storage/PartitionsField.jsx:110 #, fuzzy, c-format msgid "Transactional Btrfs root partition (%s)" msgstr "transactionnel" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:104 +#: src/components/storage/PartitionsField.jsx:115 #, fuzzy, c-format msgid "Btrfs root volume with snapshots (%s)" msgstr "avec des clichés" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" -#: src/components/storage/PartitionsField.jsx:106 +#: src/components/storage/PartitionsField.jsx:117 #, fuzzy, c-format msgid "Btrfs root partition with snapshots (%s)" msgstr "avec des clichés" #. TRANSLATORS: This results in something like "Mount /dev/sda3 at /home (25 GiB)" since #. %1$s is replaced by the device name, %2$s by the mount point and %3$s by the size -#: src/components/storage/PartitionsField.jsx:115 +#: src/components/storage/PartitionsField.jsx:126 #, c-format msgid "Mount %1$s at %2$s (%3$s)" msgstr "Monter %1$s à %2$s (%3$s)" #. TRANSLATORS: This results in something like "Swap at /dev/sda3 (2 GiB)" since #. %1$s is replaced by the device name, and %2$s by the size -#: src/components/storage/PartitionsField.jsx:121 +#: src/components/storage/PartitionsField.jsx:132 #, c-format msgid "Swap at %1$s (%2$s)" msgstr "" #. TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:125 +#: src/components/storage/PartitionsField.jsx:136 #, c-format msgid "Swap volume (%s)" msgstr "" #. TRANSLATORS: %s replaced by size string, e.g. "8 GiB" -#: src/components/storage/PartitionsField.jsx:127 +#: src/components/storage/PartitionsField.jsx:138 #, fuzzy, c-format msgid "Swap partition (%s)" msgstr "Informations sur la partition" #. TRANSLATORS: This results in something like "Btrfs root at /dev/sda3 (20 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size -#: src/components/storage/PartitionsField.jsx:136 +#: src/components/storage/PartitionsField.jsx:147 #, c-format msgid "%1$s root at %2$s (%3$s)" msgstr "" @@ -1545,21 +1505,21 @@ msgstr "" #. TRANSLATORS: "/" is in an LVM logical volume. #. Results in something like "Btrfs root volume (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:142 +#: src/components/storage/PartitionsField.jsx:153 #, fuzzy, c-format msgid "%1$s root volume (%2$s)" msgstr "Présence d'autres volumes (%s)" #. TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description -#: src/components/storage/PartitionsField.jsx:145 +#: src/components/storage/PartitionsField.jsx:156 #, fuzzy, c-format msgid "%1$s root partition (%2$s)" msgstr "%s avec %d partitions" #. TRANSLATORS: This results in something like "Ext4 /home at /dev/sda3 (20 GiB)" since #. %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size -#: src/components/storage/PartitionsField.jsx:151 +#: src/components/storage/PartitionsField.jsx:162 #, c-format msgid "%1$s %2$s at %3$s (%4$s)" msgstr "" @@ -1567,153 +1527,149 @@ msgstr "" #. TRANSLATORS: The filesystem is in an LVM logical volume. #. Results in something like "Ext4 /home volume (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:157 +#: src/components/storage/PartitionsField.jsx:168 #, c-format msgid "%1$s %2$s volume (%3$s)" msgstr "" #. TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description -#: src/components/storage/PartitionsField.jsx:160 +#: src/components/storage/PartitionsField.jsx:171 #, fuzzy, c-format msgid "%1$s %2$s partition (%3$s)" msgstr "%s avec %d partitions" -#: src/components/storage/PartitionsField.jsx:172 +#: src/components/storage/PartitionsField.jsx:182 #, fuzzy msgid "Do not configure partitions for booting" msgstr "Informations sur la partition" -#: src/components/storage/PartitionsField.jsx:175 +#: src/components/storage/PartitionsField.jsx:184 #, fuzzy msgid "Boot partitions at installation disk" msgstr "réduction des partitions du périphérique d'installation" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) -#: src/components/storage/PartitionsField.jsx:178 +#: src/components/storage/PartitionsField.jsx:187 #, fuzzy, c-format msgid "Boot partitions at %s" msgstr "Informations sur la partition" #. TRANSLATORS: header for a list of items referring to size limits for file systems -#: src/components/storage/PartitionsField.jsx:200 +#: src/components/storage/PartitionsField.jsx:209 msgid "These limits are affected by:" msgstr "Ces limites sont affectées par:" #. TRANSLATORS: list item, this affects the computed partition size limits -#: src/components/storage/PartitionsField.jsx:204 +#: src/components/storage/PartitionsField.jsx:213 msgid "The configuration of snapshots" msgstr "La configuration des clichés" -#. TRANSLATORS: list item, this affects the computed partition size limits -#. %s is replaced by a list of the volumes (like "/home, /boot") -#: src/components/storage/PartitionsField.jsx:208 +#: src/components/storage/PartitionsField.jsx:219 #, c-format msgid "Presence of other volumes (%s)" msgstr "Présence d'autres volumes (%s)" #. TRANSLATORS: list item, describes a factor that affects the computed size of a #. file system; eg. adjusting the size of the swap -#: src/components/storage/PartitionsField.jsx:212 +#: src/components/storage/PartitionsField.jsx:225 msgid "The amount of RAM in the system" msgstr "La quantité de RAM dans le système" -#. TRANSLATORS: device flag, the partition size is automatically computed -#: src/components/storage/PartitionsField.jsx:263 +#: src/components/storage/PartitionsField.jsx:292 msgid "auto" msgstr "auto" #. TRANSLATORS: %s will be replaced by a file-system type like "Btrfs" or "Ext4" -#: src/components/storage/PartitionsField.jsx:279 +#: src/components/storage/PartitionsField.jsx:309 #, c-format msgid "Reused %s" msgstr "%s Réutilisé" -#: src/components/storage/PartitionsField.jsx:281 +#: src/components/storage/PartitionsField.jsx:310 #, fuzzy msgid "Transactional Btrfs" msgstr "transactionnel" -#: src/components/storage/PartitionsField.jsx:283 +#: src/components/storage/PartitionsField.jsx:311 #, fuzzy msgid "Btrfs with snapshots" msgstr "avec des clichés" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:297 +#: src/components/storage/PartitionsField.jsx:325 #, fuzzy, c-format msgid "Partition at %s" msgstr "Informations sur la partition" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") -#: src/components/storage/PartitionsField.jsx:300 +#: src/components/storage/PartitionsField.jsx:328 #, c-format msgid "Separate LVM at %s" msgstr "Séparer le LVM à %s" -#: src/components/storage/PartitionsField.jsx:304 +#: src/components/storage/PartitionsField.jsx:331 #, fuzzy msgid "Logical volume at system LVM" msgstr "Utiliser la gestion des volumes logiques (LVM)" -#: src/components/storage/PartitionsField.jsx:306 +#: src/components/storage/PartitionsField.jsx:333 #, fuzzy msgid "Partition at installation disk" msgstr "réduction des partitions du périphérique d'installation" -#: src/components/storage/PartitionsField.jsx:321 +#: src/components/storage/PartitionsField.jsx:348 #, fuzzy msgid "Reset location" msgstr "Enregistrement" -#: src/components/storage/PartitionsField.jsx:322 +#: src/components/storage/PartitionsField.jsx:349 #, fuzzy msgid "Change location" msgstr "Changer de fuseau horaire" -#: src/components/storage/PartitionsField.jsx:323 -#: src/components/storage/SpaceActionsTable.jsx:78 +#: src/components/storage/PartitionsField.jsx:350 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Supprimer" -#: src/components/storage/PartitionsField.jsx:449 -#: src/components/storage/VolumeFields.jsx:61 -#: src/components/storage/VolumeFields.jsx:70 +#: src/components/storage/PartitionsField.jsx:486 +#: src/components/storage/VolumeFields.jsx:66 #: src/components/storage/VolumeFields.jsx:75 +#: src/components/storage/VolumeFields.jsx:80 msgid "Mount point" msgstr "Point de montage" #. TRANSLATORS: where (and how) the file-system is going to be created -#: src/components/storage/PartitionsField.jsx:453 +#: src/components/storage/PartitionsField.jsx:490 #, fuzzy msgid "Location" msgstr "Localisation" -#: src/components/storage/PartitionsField.jsx:495 +#: src/components/storage/PartitionsField.jsx:532 msgid "Table with mount points" msgstr "Table avec points de montage" -#: src/components/storage/PartitionsField.jsx:566 -#: src/components/storage/PartitionsField.jsx:585 -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/PartitionsField.jsx:604 +#: src/components/storage/PartitionsField.jsx:624 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Add file system" msgstr "Ajouter un système de fichiers" -#: src/components/storage/PartitionsField.jsx:596 +#: src/components/storage/PartitionsField.jsx:636 msgid "Other" msgstr "Autre" -#: src/components/storage/PartitionsField.jsx:731 +#: src/components/storage/PartitionsField.jsx:777 msgid "Reset to defaults" msgstr "Rétablir les valeurs par défaut" -#: src/components/storage/PartitionsField.jsx:801 +#: src/components/storage/PartitionsField.jsx:849 #, fuzzy msgid "Partitions and file systems" msgstr "Taille exacte du système de fichiers." -#: src/components/storage/PartitionsField.jsx:802 +#: src/components/storage/PartitionsField.jsx:851 #, fuzzy msgid "" "Structure of the new system, including any additional partition needed for " @@ -1722,21 +1678,19 @@ msgstr "" "Structure du nouveau système, incluant toute partition supplémentaire " "nécessaire au démarrage," -#: src/components/storage/PartitionsField.jsx:808 +#: src/components/storage/PartitionsField.jsx:858 #, fuzzy msgid "Show partitions and file-systems actions" msgstr "Taille exacte du système de fichiers." -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:62 +#: src/components/storage/ProposalActionsDialog.jsx:65 #, c-format msgid "Hide %d subvolume action" msgid_plural "Hide %d subvolume actions" msgstr[0] "Masquer l'action du sous-volume %d" msgstr[1] "Masquer les actions du sous-volume %d" -#. TRANSLATORS: show/hide toggle action, this is a clickable link -#: src/components/storage/ProposalActionsDialog.jsx:64 +#: src/components/storage/ProposalActionsDialog.jsx:70 #, c-format msgid "Show %d subvolume action" msgid_plural "Show %d subvolume actions" @@ -1753,94 +1707,87 @@ msgstr "Il y a %d action destructrice prévue" msgid "Destructive actions are allowed" msgstr "Il y a %d action destructrice prévue" -#: src/components/storage/ProposalActionsSummary.jsx:66 -#, c-format -msgid "There is %d destructive action planned" -msgid_plural "There are %d destructive actions planned" -msgstr[0] "Il y a %d action destructrice prévue" -msgstr[1] "Il y a %d actions destructrices prévues" - -#: src/components/storage/ProposalActionsSummary.jsx:79 -#: src/components/storage/ProposalActionsSummary.jsx:126 +#: src/components/storage/ProposalActionsSummary.jsx:82 +#: src/components/storage/ProposalActionsSummary.jsx:132 #, fuzzy msgid "affecting" msgstr "Connexion" -#: src/components/storage/ProposalActionsSummary.jsx:107 +#: src/components/storage/ProposalActionsSummary.jsx:112 #, fuzzy msgid "Shrinking partitions is not allowed" msgstr "Réduire les partitions existantes" -#: src/components/storage/ProposalActionsSummary.jsx:111 +#: src/components/storage/ProposalActionsSummary.jsx:116 #, fuzzy msgid "Shrinking partitions is allowed" msgstr "Réduire les partitions existantes" -#: src/components/storage/ProposalActionsSummary.jsx:113 +#: src/components/storage/ProposalActionsSummary.jsx:118 #, fuzzy msgid "Shrinking some partitions is allowed but not needed" msgstr "réduction des partitions du périphérique d'installation" -#: src/components/storage/ProposalActionsSummary.jsx:116 +#: src/components/storage/ProposalActionsSummary.jsx:121 #, fuzzy, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" msgstr[0] "Table de partitionnement %s" msgstr[1] "Table de partitionnement %s" -#: src/components/storage/ProposalActionsSummary.jsx:151 +#: src/components/storage/ProposalActionsSummary.jsx:159 #, fuzzy msgid "Cannot accommodate the required file systems for installation" msgstr "" "La disposition du clavier ne peut pas être modifiée via l'installation à " "distance" -#: src/components/storage/ProposalActionsSummary.jsx:160 +#: src/components/storage/ProposalActionsSummary.jsx:167 #, fuzzy, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" msgstr[0] "Actions planifiées" msgstr[1] "Actions planifiées" -#: src/components/storage/ProposalActionsSummary.jsx:179 +#: src/components/storage/ProposalActionsSummary.jsx:182 #, fuzzy msgid "Waiting for actions information..." msgstr "En attente d'informations sur le LVM" -#: src/components/storage/ProposalPage.jsx:314 +#: src/components/storage/ProposalPage.jsx:329 msgid "Planned Actions" msgstr "Actions planifiées" -#: src/components/storage/ProposalResultSection.jsx:42 +#: src/components/storage/ProposalResultSection.jsx:43 #, fuzzy msgid "Waiting for information about storage configuration" msgstr "En attente d'informations sur le LVM" -#: src/components/storage/ProposalResultSection.jsx:70 +#: src/components/storage/ProposalResultSection.jsx:73 msgid "Final layout" msgstr "" -#: src/components/storage/ProposalResultSection.jsx:71 +#: src/components/storage/ProposalResultSection.jsx:74 msgid "The systems will be configured as displayed below." msgstr "" -#: src/components/storage/ProposalResultSection.jsx:78 +#: src/components/storage/ProposalResultSection.jsx:83 msgid "Storage proposal not possible" msgstr "" -#: src/components/storage/ProposalResultTable.jsx:74 +#: src/components/storage/ProposalResultTable.jsx:79 #, fuzzy msgid "New" msgstr "Réseau" #. TRANSLATORS: Label to indicate the device size before resizing, where %s is #. replaced by the original size (e.g., 3.00 GiB). -#: src/components/storage/ProposalResultTable.jsx:98 +#: src/components/storage/ProposalResultTable.jsx:105 #, fuzzy, c-format msgid "Before %s" msgstr "Logiciel %s" -#: src/components/storage/ProposalResultTable.jsx:121 +#: src/components/storage/ProposalResultTable.jsx:131 #, fuzzy msgid "Mount Point" msgstr "Point de montage" @@ -1849,7 +1796,7 @@ msgstr "Point de montage" msgid "Transactional root file system" msgstr "Système de fichiers root transactionnel" -#: src/components/storage/ProposalTransactionalInfo.jsx:48 +#: src/components/storage/ProposalTransactionalInfo.jsx:49 #, c-format msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " @@ -1863,7 +1810,7 @@ msgstr "" msgid "Use Btrfs snapshots for the root file system" msgstr "Taille exacte du système de fichiers." -#: src/components/storage/SnapshotsField.jsx:37 +#: src/components/storage/SnapshotsField.jsx:38 #, fuzzy msgid "" "Allows to boot to a previous version of the system after configuration " @@ -1873,128 +1820,120 @@ msgstr "" "sur une version précédente du système après des changements de configuration " "ou des mises à jour logicielles." -#. TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) -#: src/components/storage/SpaceActionsTable.jsx:75 +#: src/components/storage/SpaceActionsTable.jsx:68 #, c-format -msgid "Space action selector for %s" -msgstr "Sélecteur d'allocation d'espace pour %s" +msgid "Up to %s can be recovered by shrinking the device." +msgstr "" -#: src/components/storage/SpaceActionsTable.jsx:79 -msgid "Allow resize" -msgstr "Permettre le redimensionnement" +#: src/components/storage/SpaceActionsTable.jsx:77 +msgid "The device cannot be shrunk:" +msgstr "" -#: src/components/storage/SpaceActionsTable.jsx:80 -msgid "Do not modify" -msgstr "Ne pas modifier" +#: src/components/storage/SpaceActionsTable.jsx:98 +#, fuzzy, c-format +msgid "Show information about %s" +msgstr "En attente d'informations sur le périphérique sélectionné" -#: src/components/storage/SpaceActionsTable.jsx:111 +#: src/components/storage/SpaceActionsTable.jsx:172 msgid "The content may be deleted" msgstr "Le contenu pourrait être supprimé" -#: src/components/storage/SpaceActionsTable.jsx:144 -#, fuzzy -msgid "Shrinkable" -msgstr "Rétrécissable de %s" - -#: src/components/storage/SpaceActionsTable.jsx:146 +#: src/components/storage/SpaceActionsTable.jsx:204 msgid "Action" msgstr "Action" -#: src/components/storage/SpaceActionsTable.jsx:162 +#: src/components/storage/SpaceActionsTable.jsx:215 msgid "Actions to find space" msgstr "Actions pour trouver de l'espace" -#: src/components/storage/SpacePolicySelection.jsx:170 +#: src/components/storage/SpacePolicySelection.jsx:172 #, fuzzy msgid "Space policy" msgstr "Politique sur l'espace" -#: src/components/storage/VolumeDialog.jsx:78 +#: src/components/storage/VolumeDialog.jsx:83 #, fuzzy, c-format msgid "Add %s file system" msgstr "Ajouter un système de fichiers" -#: src/components/storage/VolumeDialog.jsx:79 +#: src/components/storage/VolumeDialog.jsx:84 #, fuzzy, c-format msgid "Edit %s file system" msgstr "Modifier le système de fichiers" -#: src/components/storage/VolumeDialog.jsx:81 +#: src/components/storage/VolumeDialog.jsx:86 msgid "Edit file system" msgstr "Modifier le système de fichiers" #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:96 +#: src/components/storage/VolumeDialog.jsx:101 #, fuzzy msgid "The type and size of the file system cannot be edited." msgstr "la présence du système de fichiers pour %s" -#. TRANSLATORS: Description of a warning. The first %s is replaced by a device name (e.g., -#. /dev/vda) and the second %s is replaced by a mount path (e.g., /home). -#: src/components/storage/VolumeDialog.jsx:99 +#: src/components/storage/VolumeDialog.jsx:105 #, c-format msgid "The current file system on %s is selected to be mounted at %s." msgstr "" "Le système de fichiers actuel sur %s est sélectionné pour être monté sur %s." #. TRANSLATORS: Warning when editing a file system. -#: src/components/storage/VolumeDialog.jsx:105 +#: src/components/storage/VolumeDialog.jsx:113 #, fuzzy msgid "The size of the file system cannot be edited" msgstr "la présence du système de fichiers pour %s" #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). -#: src/components/storage/VolumeDialog.jsx:107 +#: src/components/storage/VolumeDialog.jsx:115 #, fuzzy, c-format msgid "The file system is allocated at the device %s." msgstr "" "Configuration du groupe de volumes système. Tous les systèmes de fichiers " "seront créés dans un volume logique du groupe de volume système." -#: src/components/storage/VolumeDialog.jsx:152 +#: src/components/storage/VolumeDialog.jsx:163 #, fuzzy msgid "A mount point is required" msgstr "Une taille minimale est requise" -#: src/components/storage/VolumeDialog.jsx:179 +#: src/components/storage/VolumeDialog.jsx:190 #, fuzzy msgid "The mount point is invalid" msgstr "Table avec points de montage" -#: src/components/storage/VolumeDialog.jsx:207 +#: src/components/storage/VolumeDialog.jsx:218 msgid "A size value is required" msgstr "Une valeur de taille est requise" -#: src/components/storage/VolumeDialog.jsx:235 +#: src/components/storage/VolumeDialog.jsx:246 msgid "Minimum size is required" msgstr "Une taille minimale est requise" -#: src/components/storage/VolumeDialog.jsx:267 +#: src/components/storage/VolumeDialog.jsx:278 msgid "Maximum must be greater than minimum" msgstr "Le maximum doit être supérieur au minimum" -#: src/components/storage/VolumeDialog.jsx:309 +#: src/components/storage/VolumeDialog.jsx:320 #, fuzzy, c-format msgid "There is already a file system for %s." msgstr "la présence du système de fichiers pour %s" -#: src/components/storage/VolumeDialog.jsx:311 +#: src/components/storage/VolumeDialog.jsx:322 #, fuzzy msgid "Do you want to edit it?" msgstr "Souhaitez-vous désinscrire %s ?" -#: src/components/storage/VolumeDialog.jsx:356 +#: src/components/storage/VolumeDialog.jsx:367 #, fuzzy, c-format msgid "There is a predefined file system for %s." msgstr "la présence du système de fichiers pour %s" -#: src/components/storage/VolumeDialog.jsx:358 +#: src/components/storage/VolumeDialog.jsx:369 #, fuzzy msgid "Do you want to add it?" msgstr "Souhaitez-vous désinscrire %s ?" -#. TRANSLATORS: info about possible file system types. -#: src/components/storage/VolumeFields.jsx:217 +#: src/components/storage/VolumeFields.jsx:225 msgid "" "The options for the file system type depends on the product and the mount " "point." @@ -2002,69 +1941,65 @@ msgstr "" "Les options pour le type de système de fichiers varient en fonction du " "produit et du point de montage." -#: src/components/storage/VolumeFields.jsx:223 +#: src/components/storage/VolumeFields.jsx:232 msgid "More info for file system types" msgstr "Plus d'informations sur les différents systèmes de fichiers" #. TRANSLATORS: label for the file system selector. -#: src/components/storage/VolumeFields.jsx:234 +#: src/components/storage/VolumeFields.jsx:243 msgid "File system type" msgstr "Type de système de fichiers" #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:265 +#: src/components/storage/VolumeFields.jsx:274 msgid "the configuration of snapshots" msgstr "la configuration des clichés" -#. TRANSLATORS: item which affects the final computed partition size -#. %s is replaced by a list of mount points like "/home, /boot" -#: src/components/storage/VolumeFields.jsx:270 +#: src/components/storage/VolumeFields.jsx:281 #, c-format msgid "the presence of the file system for %s" msgstr "la présence du système de fichiers pour %s" #. TRANSLATORS: conjunction for merging two list items -#: src/components/storage/VolumeFields.jsx:272 +#: src/components/storage/VolumeFields.jsx:283 msgid ", " msgstr ", " #. TRANSLATORS: item which affects the final computed partition size -#: src/components/storage/VolumeFields.jsx:276 +#: src/components/storage/VolumeFields.jsx:289 msgid "the amount of RAM in the system" msgstr "la quantité de RAM du système" -#. TRANSLATORS: the %s is replaced by the items which affect the computed size -#: src/components/storage/VolumeFields.jsx:279 +#: src/components/storage/VolumeFields.jsx:293 #, c-format msgid "The final size depends on %s." msgstr "La taille définitive dépend de %s." #. TRANSLATORS: conjunction for merging two texts -#: src/components/storage/VolumeFields.jsx:281 +#: src/components/storage/VolumeFields.jsx:295 msgid " and " msgstr " et " -#. TRANSLATORS: the partition size is automatically computed -#: src/components/storage/VolumeFields.jsx:286 +#: src/components/storage/VolumeFields.jsx:302 msgid "Automatically calculated size according to the selected product." msgstr "" "La taille est automatiquement calculée en fonction du produit sélectionné." -#: src/components/storage/VolumeFields.jsx:305 +#: src/components/storage/VolumeFields.jsx:321 msgid "Exact size for the file system." msgstr "Taille exacte du système de fichiers." #. TRANSLATORS: requested partition size -#: src/components/storage/VolumeFields.jsx:318 +#: src/components/storage/VolumeFields.jsx:330 msgid "Exact size" msgstr "Taille exacte" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) -#: src/components/storage/VolumeFields.jsx:335 +#: src/components/storage/VolumeFields.jsx:347 msgid "Size unit" msgstr "Unité de mesure" -#: src/components/storage/VolumeFields.jsx:363 +#: src/components/storage/VolumeFields.jsx:376 msgid "" "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 " @@ -2075,50 +2010,49 @@ msgstr "" "n'est indiqué, le système de fichiers sera aussi grand que possible." #. TRANSLATORS: the minimal partition size -#: src/components/storage/VolumeFields.jsx:370 +#: src/components/storage/VolumeFields.jsx:384 msgid "Minimum" msgstr "Minimum" #. TRANSLATORS: the minium partition size -#: src/components/storage/VolumeFields.jsx:381 +#: src/components/storage/VolumeFields.jsx:395 msgid "Minimum desired size" msgstr "Taille minimale souhaitée" -#: src/components/storage/VolumeFields.jsx:392 +#: src/components/storage/VolumeFields.jsx:406 msgid "Unit for the minimum size" msgstr "Unité de la taille minimale" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:404 +#: src/components/storage/VolumeFields.jsx:418 msgid "Maximum" msgstr "Maximum" #. TRANSLATORS: the maximum partition size -#: src/components/storage/VolumeFields.jsx:416 +#: src/components/storage/VolumeFields.jsx:430 msgid "Maximum desired size" msgstr "Taille maximale désirée" -#: src/components/storage/VolumeFields.jsx:426 +#: src/components/storage/VolumeFields.jsx:440 msgid "Unit for the maximum size" msgstr "Unité de la taille maximale" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input -#: src/components/storage/VolumeFields.jsx:444 +#: src/components/storage/VolumeFields.jsx:458 msgid "Auto" msgstr "Auto" #. TRANSLATORS: radio button label, exact partition size requested by user -#: src/components/storage/VolumeFields.jsx:446 +#: src/components/storage/VolumeFields.jsx:460 msgid "Fixed" msgstr "Fixe" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits -#: src/components/storage/VolumeFields.jsx:448 +#: src/components/storage/VolumeFields.jsx:462 msgid "Range" msgstr "Portée" -#. TRANSLATORS: Description of the dialog for changing the location of a file system. -#: src/components/storage/VolumeLocationDialog.jsx:40 +#: src/components/storage/VolumeLocationDialog.jsx:41 msgid "" "The file systems are allocated at the installation device by default. " "Indicate a custom location to create the file system at a specific device." @@ -2129,33 +2063,33 @@ msgstr "" #. TRANSLATORS: Title of the dialog for changing the location of a file system. %s is replaced #. by a mount path (e.g., /home). -#: src/components/storage/VolumeLocationDialog.jsx:135 +#: src/components/storage/VolumeLocationDialog.jsx:137 #, fuzzy, c-format msgid "Location for %s file system" msgstr "Taille exacte du système de fichiers." -#: src/components/storage/VolumeLocationDialog.jsx:145 +#: src/components/storage/VolumeLocationDialog.jsx:147 #, fuzzy msgid "Select in which device to allocate the file system" msgstr "" "Sélectionnez la manière de libérer de l'espace sur les disques sélectionnés " "pour l'allocation des systèmes de fichiers." -#: src/components/storage/VolumeLocationDialog.jsx:148 +#: src/components/storage/VolumeLocationDialog.jsx:150 #, fuzzy msgid "Select a location" msgstr "Sélectionner une valeur" -#: src/components/storage/VolumeLocationDialog.jsx:160 +#: src/components/storage/VolumeLocationDialog.jsx:162 #, fuzzy msgid "Select how to allocate the file system" msgstr "Taille exacte du système de fichiers." -#: src/components/storage/VolumeLocationDialog.jsx:165 +#: src/components/storage/VolumeLocationDialog.jsx:167 msgid "Create a new partition" msgstr "Créer une nouvelle partition" -#: src/components/storage/VolumeLocationDialog.jsx:166 +#: src/components/storage/VolumeLocationDialog.jsx:169 #, fuzzy msgid "" "The file system will be allocated as a new partition at the selected disk." @@ -2163,12 +2097,12 @@ msgstr "" "Configuration du groupe de volumes système. Tous les systèmes de fichiers " "seront créés dans un volume logique du groupe de volume système." -#: src/components/storage/VolumeLocationDialog.jsx:175 +#: src/components/storage/VolumeLocationDialog.jsx:179 #, fuzzy msgid "Create a dedicated LVM volume group" msgstr "Groupe de volume système" -#: src/components/storage/VolumeLocationDialog.jsx:176 +#: src/components/storage/VolumeLocationDialog.jsx:181 msgid "" "A new volume group will be allocated in the selected disk and the file " "system will be created as a logical volume." @@ -2176,24 +2110,23 @@ msgstr "" "Un nouveau groupe de volumes sera attribué au disque sélectionné et le " "système de fichiers sera créé en tant que volume logique." -#: src/components/storage/VolumeLocationDialog.jsx:185 +#: src/components/storage/VolumeLocationDialog.jsx:191 #, fuzzy msgid "Format the device" msgstr "Formatage des périphériques DASD" -#. TRANSLATORS: %s is replaced by a file system type (e.g., Ext4). -#: src/components/storage/VolumeLocationDialog.jsx:188 +#: src/components/storage/VolumeLocationDialog.jsx:195 #, c-format msgid "The selected device will be formatted as %s file system." msgstr "" "Le périphérique sélectionné sera formaté en tant que système de fichiers %s." -#: src/components/storage/VolumeLocationDialog.jsx:198 +#: src/components/storage/VolumeLocationDialog.jsx:206 #, fuzzy msgid "Mount the file system" msgstr "Modifier le système de fichiers" -#: src/components/storage/VolumeLocationDialog.jsx:199 +#: src/components/storage/VolumeLocationDialog.jsx:208 msgid "" "The current file system on the selected device will be mounted without " "formatting the device." @@ -2201,53 +2134,51 @@ msgstr "" "Le système de fichiers actuel sur le périphérique sélectionné sera monté " "sans formater le périphérique." -#: src/components/storage/VolumeLocationSelectorTable.jsx:102 +#: src/components/storage/VolumeLocationSelectorTable.jsx:110 msgid "Usage" msgstr "Utilisation" -#: src/components/storage/ZFCPDiskForm.jsx:109 +#: src/components/storage/ZFCPDiskForm.jsx:106 msgid "The zFCP disk was not activated." msgstr "Le disque zFCP n'a pas été activé." #. TRANSLATORS: abbrev. World Wide Port Name #: src/components/storage/ZFCPDiskForm.jsx:123 -#: src/components/storage/ZFCPPage.jsx:363 +#: src/components/storage/ZFCPPage.jsx:383 msgid "WWPN" msgstr "WWPN" #. TRANSLATORS: abbrev. Logical Unit Number -#: src/components/storage/ZFCPDiskForm.jsx:134 -#: src/components/storage/ZFCPPage.jsx:364 +#: src/components/storage/ZFCPDiskForm.jsx:131 +#: src/components/storage/ZFCPPage.jsx:384 msgid "LUN" msgstr "LUN" -#: src/components/storage/ZFCPPage.jsx:304 +#: src/components/storage/ZFCPPage.jsx:326 msgid "Auto LUNs Scan" msgstr "Balayage LUN automatique" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Activated" msgstr "Activé" -#: src/components/storage/ZFCPPage.jsx:315 +#: src/components/storage/ZFCPPage.jsx:337 msgid "Deactivated" msgstr "Désactivé" -#: src/components/storage/ZFCPPage.jsx:418 +#: src/components/storage/ZFCPPage.jsx:437 msgid "No zFCP controllers found." msgstr "Aucun contrôleur zFCP n'a été trouvé." -#: src/components/storage/ZFCPPage.jsx:419 +#: src/components/storage/ZFCPPage.jsx:438 msgid "Please, try to read the zFCP devices again." msgstr "Veuillez essayez de lire à nouveau périphériques zFCP." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:421 +#: src/components/storage/ZFCPPage.jsx:441 msgid "Read zFCP devices" msgstr "Lire les périphériques zFCP" -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:430 +#: src/components/storage/ZFCPPage.jsx:452 msgid "" "Automatic LUN scan is [enabled]. Activating a controller which is running in " "NPIV mode will automatically configures all its LUNs." @@ -2255,8 +2186,7 @@ msgstr "" "Le balayage automatique des LUN est [activé]. L'activation d'un contrôleur " "fonctionnant en mode NPIV configurera automatiquement toutes ses LUN." -#. TRANSLATORS: the text in the square brackets [] will be displayed in bold -#: src/components/storage/ZFCPPage.jsx:433 +#: src/components/storage/ZFCPPage.jsx:457 msgid "" "Automatic LUN scan is [disabled]. LUNs have to be manually configured after " "activating a controller." @@ -2264,38 +2194,36 @@ msgstr "" "Le balayage automatique des LUN est [désactivé]. Les LUN doivent être " "configurés manuellement après l'activation d'un contrôleur." -#: src/components/storage/ZFCPPage.jsx:490 +#: src/components/storage/ZFCPPage.jsx:519 msgid "Activate a zFCP disk" msgstr "Activer un disque zFCP" -#: src/components/storage/ZFCPPage.jsx:529 +#: src/components/storage/ZFCPPage.jsx:553 msgid "Please, try to activate a zFCP controller." msgstr "Veuillez essayer d'activer un contrôleur zFCP." -#: src/components/storage/ZFCPPage.jsx:536 +#: src/components/storage/ZFCPPage.jsx:559 msgid "Please, try to activate a zFCP disk." msgstr "Veuillez essayer d'activer un disque zFCP." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:538 +#: src/components/storage/ZFCPPage.jsx:562 msgid "Activate zFCP disk" msgstr "Activer le disque zFCP" -#: src/components/storage/ZFCPPage.jsx:545 +#: src/components/storage/ZFCPPage.jsx:570 msgid "No zFCP disks found." msgstr "Aucun disque zFCP n'a été trouvé." -#. TRANSLATORS: button label -#: src/components/storage/ZFCPPage.jsx:560 +#: src/components/storage/ZFCPPage.jsx:586 msgid "Activate new disk" msgstr "Activer un nouveau disque" #. TRANSLATORS: section title -#: src/components/storage/ZFCPPage.jsx:572 +#: src/components/storage/ZFCPPage.jsx:599 msgid "Disks" msgstr "Disques" -#: src/components/storage/device-utils.jsx:88 +#: src/components/storage/device-utils.jsx:92 #, fuzzy msgid "Unused space" msgstr "Trouver de l'espace" @@ -2308,70 +2236,70 @@ msgstr "Disponible uniquement si l'authentification par la cible est fournie" msgid "Authentication by target" msgstr "Authentification par cible" -#: src/components/storage/iscsi/AuthFields.jsx:80 -#: src/components/storage/iscsi/AuthFields.jsx:85 -#: src/components/storage/iscsi/AuthFields.jsx:87 -#: src/components/storage/iscsi/AuthFields.jsx:112 -#: src/components/storage/iscsi/AuthFields.jsx:117 -#: src/components/storage/iscsi/AuthFields.jsx:119 +#: src/components/storage/iscsi/AuthFields.jsx:78 +#: src/components/storage/iscsi/AuthFields.jsx:82 +#: src/components/storage/iscsi/AuthFields.jsx:84 +#: src/components/storage/iscsi/AuthFields.jsx:104 +#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:110 msgid "User name" msgstr "Nom d'utilisateur" -#: src/components/storage/iscsi/AuthFields.jsx:91 -#: src/components/storage/iscsi/AuthFields.jsx:124 +#: src/components/storage/iscsi/AuthFields.jsx:88 +#: src/components/storage/iscsi/AuthFields.jsx:116 msgid "Incorrect user name" msgstr "Nom d'utilisateur incorrect" -#: src/components/storage/iscsi/AuthFields.jsx:105 -#: src/components/storage/iscsi/AuthFields.jsx:139 +#: src/components/storage/iscsi/AuthFields.jsx:99 +#: src/components/storage/iscsi/AuthFields.jsx:130 msgid "Incorrect password" msgstr "Mot de passe incorrect" -#: src/components/storage/iscsi/AuthFields.jsx:108 +#: src/components/storage/iscsi/AuthFields.jsx:102 msgid "Authentication by initiator" msgstr "Authentification par initiateur" -#: src/components/storage/iscsi/AuthFields.jsx:133 +#: src/components/storage/iscsi/AuthFields.jsx:123 msgid "Target Password" msgstr "Mot de passe cible" #. TRANSLATORS: popup title -#: src/components/storage/iscsi/DiscoverForm.jsx:102 +#: src/components/storage/iscsi/DiscoverForm.jsx:94 msgid "Discover iSCSI Targets" msgstr "Découvrir les cibles iSCSI" -#: src/components/storage/iscsi/DiscoverForm.jsx:112 -#: src/components/storage/iscsi/LoginForm.jsx:73 +#: src/components/storage/iscsi/DiscoverForm.jsx:99 +#: src/components/storage/iscsi/LoginForm.jsx:70 msgid "Make sure you provide the correct values" msgstr "Assurez-vous de fournir les bonnes valeurs" -#: src/components/storage/iscsi/DiscoverForm.jsx:118 +#: src/components/storage/iscsi/DiscoverForm.jsx:103 msgid "IP address" msgstr "Adresse IP" #. TRANSLATORS: network address -#: src/components/storage/iscsi/DiscoverForm.jsx:125 -#: src/components/storage/iscsi/DiscoverForm.jsx:127 +#: src/components/storage/iscsi/DiscoverForm.jsx:108 +#: src/components/storage/iscsi/DiscoverForm.jsx:110 msgid "Address" msgstr "Adresse" -#: src/components/storage/iscsi/DiscoverForm.jsx:132 +#: src/components/storage/iscsi/DiscoverForm.jsx:115 msgid "Incorrect IP address" msgstr "Adresse IP incorrecte" #. TRANSLATORS: network port number -#: src/components/storage/iscsi/DiscoverForm.jsx:136 -#: src/components/storage/iscsi/DiscoverForm.jsx:143 -#: src/components/storage/iscsi/DiscoverForm.jsx:145 +#: src/components/storage/iscsi/DiscoverForm.jsx:117 +#: src/components/storage/iscsi/DiscoverForm.jsx:122 +#: src/components/storage/iscsi/DiscoverForm.jsx:124 msgid "Port" msgstr "Port" -#: src/components/storage/iscsi/DiscoverForm.jsx:150 +#: src/components/storage/iscsi/DiscoverForm.jsx:129 msgid "Incorrect port" msgstr "Port incorrect" #. TRANSLATORS: %s is replaced by the iSCSI target node name -#: src/components/storage/iscsi/EditNodeForm.jsx:50 +#: src/components/storage/iscsi/EditNodeForm.jsx:48 #, c-format msgid "Edit %s" msgstr "Modifier %s" @@ -2389,8 +2317,8 @@ msgstr "Nom de l'initiateur" #. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 #: src/components/storage/iscsi/InitiatorPresenter.jsx:86 -#: src/components/storage/iscsi/NodesPresenter.jsx:103 -#: src/components/storage/iscsi/NodesPresenter.jsx:124 +#: src/components/storage/iscsi/NodesPresenter.jsx:101 +#: src/components/storage/iscsi/NodesPresenter.jsx:122 msgid "iBFT" msgstr "iBFT" @@ -2405,14 +2333,14 @@ msgid "Initiator" msgstr "Initiateur" #. TRANSLATORS: %s is replaced by the iSCSI target name -#: src/components/storage/iscsi/LoginForm.jsx:69 +#: src/components/storage/iscsi/LoginForm.jsx:66 #, c-format msgid "Login %s" msgstr "Connexion %s" #. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) -#: src/components/storage/iscsi/LoginForm.jsx:76 -#: src/components/storage/iscsi/LoginForm.jsx:79 +#: src/components/storage/iscsi/LoginForm.jsx:74 +#: src/components/storage/iscsi/LoginForm.jsx:77 msgid "Startup" msgstr "Démarrage" @@ -2434,29 +2362,28 @@ msgstr "Se connecter" msgid "Logout" msgstr "Se déconnecter" -#: src/components/storage/iscsi/NodesPresenter.jsx:101 -#: src/components/storage/iscsi/NodesPresenter.jsx:122 +#: src/components/storage/iscsi/NodesPresenter.jsx:99 +#: src/components/storage/iscsi/NodesPresenter.jsx:120 msgid "Portal" msgstr "Portail" -#: src/components/storage/iscsi/NodesPresenter.jsx:102 -#: src/components/storage/iscsi/NodesPresenter.jsx:123 +#: src/components/storage/iscsi/NodesPresenter.jsx:100 +#: src/components/storage/iscsi/NodesPresenter.jsx:121 msgid "Interface" msgstr "Interface" -#: src/components/storage/iscsi/TargetsSection.jsx:142 +#: src/components/storage/iscsi/TargetsSection.jsx:138 msgid "No iSCSI targets found." msgstr "Aucune cible iSCSI n'a été trouvée." -#: src/components/storage/iscsi/TargetsSection.jsx:143 +#: src/components/storage/iscsi/TargetsSection.jsx:140 msgid "" "Please, perform an iSCSI discovery in order to find available iSCSI targets." msgstr "" "Veuillez effectuer une découverte iSCSI afin de trouver les cibles iSCSI " "disponibles." -#. TRANSLATORS: button label, starts iSCSI discovery -#: src/components/storage/iscsi/TargetsSection.jsx:145 +#: src/components/storage/iscsi/TargetsSection.jsx:144 msgid "Discover iSCSI targets" msgstr "Découvrir les cibles iSCSI" @@ -2466,7 +2393,7 @@ msgid "Discover" msgstr "Découvrir" #. TRANSLATORS: iSCSI targets section title -#: src/components/storage/iscsi/TargetsSection.jsx:170 +#: src/components/storage/iscsi/TargetsSection.jsx:167 msgid "Targets" msgstr "Cibles" @@ -2564,7 +2491,7 @@ msgstr "réalisation d'une suite d'actions personnalisées" msgid "No user defined yet." msgstr "Aucun utilisateur n'a été défini." -#: src/components/users/FirstUser.jsx:38 +#: src/components/users/FirstUser.jsx:39 msgid "" "Please, be aware that a user must be defined before installing the system to " "be able to log into it." @@ -2572,71 +2499,71 @@ msgstr "" "Veuillez noter qu'un utilisateur doit être défini avant l'installation du " "système pour pouvoir s'y connecter." -#: src/components/users/FirstUser.jsx:42 +#: src/components/users/FirstUser.jsx:45 msgid "Define a user now" msgstr "Définir un utilisateur maintenant" -#: src/components/users/FirstUser.jsx:54 -#: src/components/users/FirstUserForm.jsx:210 +#: src/components/users/FirstUser.jsx:58 +#: src/components/users/FirstUserForm.jsx:227 msgid "Full name" msgstr "Nom complet" -#: src/components/users/FirstUser.jsx:55 -#: src/components/users/FirstUserForm.jsx:224 -#: src/components/users/FirstUserForm.jsx:229 -#: src/components/users/FirstUserForm.jsx:232 +#: src/components/users/FirstUser.jsx:59 +#: src/components/users/FirstUserForm.jsx:241 +#: src/components/users/FirstUserForm.jsx:246 +#: src/components/users/FirstUserForm.jsx:249 msgid "Username" msgstr "Nom d'utilisateur" -#: src/components/users/FirstUser.jsx:120 -#: src/components/users/RootAuthMethods.jsx:99 -#: src/components/users/RootAuthMethods.jsx:111 +#: src/components/users/FirstUser.jsx:124 +#: src/components/users/RootAuthMethods.jsx:104 +#: src/components/users/RootAuthMethods.jsx:116 msgid "Discard" msgstr "Rejeter" -#: src/components/users/FirstUserForm.jsx:46 +#: src/components/users/FirstUserForm.jsx:57 msgid "Username suggestion dropdown" msgstr "menu déroulant de noms d'utilisateur suggérés" #. TRANSLATORS: dropdown username suggestions -#: src/components/users/FirstUserForm.jsx:61 +#: src/components/users/FirstUserForm.jsx:72 msgid "Use suggested username" msgstr "Utiliser le nom d'utilisateur suggéré" -#: src/components/users/FirstUserForm.jsx:140 +#: src/components/users/FirstUserForm.jsx:151 #, fuzzy msgid "All fields are required" msgstr "Une valeur de taille est requise" -#: src/components/users/FirstUserForm.jsx:147 +#: src/components/users/FirstUserForm.jsx:158 msgid "Please, try again." msgstr "" -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 #, fuzzy msgid "Create user" msgstr "Créer un compte utilisateur" -#: src/components/users/FirstUserForm.jsx:197 +#: src/components/users/FirstUserForm.jsx:211 #, fuzzy msgid "Edit user" msgstr "Modifier %s" -#: src/components/users/FirstUserForm.jsx:214 -#: src/components/users/FirstUserForm.jsx:216 +#: src/components/users/FirstUserForm.jsx:231 +#: src/components/users/FirstUserForm.jsx:233 msgid "User full name" msgstr "Nom complet de l'utilisateur" -#: src/components/users/FirstUserForm.jsx:254 +#: src/components/users/FirstUserForm.jsx:271 msgid "Edit password too" msgstr "Modifier également le mot de passe" -#: src/components/users/FirstUserForm.jsx:269 +#: src/components/users/FirstUserForm.jsx:287 msgid "user autologin" msgstr "connexion automatique de l'utilisateur" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:273 +#: src/components/users/FirstUserForm.jsx:291 msgid "Auto-login" msgstr "connexion automatique" @@ -2644,7 +2571,7 @@ msgstr "connexion automatique" msgid "No root authentication method defined yet." msgstr "Aucune méthode d'authentification Root n'a été définie." -#: src/components/users/RootAuthMethods.jsx:38 +#: src/components/users/RootAuthMethods.jsx:39 msgid "" "Please, define at least one authentication method for logging into the " "system as root." @@ -2652,56 +2579,54 @@ msgstr "" "Veuillez définir au moins une méthode d'authentification pour vous connecter " "au système en tant que root." -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:43 +#: src/components/users/RootAuthMethods.jsx:46 msgid "Set a password" msgstr "Définir un mot de passe" -#. TRANSLATORS: push button label -#: src/components/users/RootAuthMethods.jsx:45 +#: src/components/users/RootAuthMethods.jsx:50 msgid "Upload a SSH Public Key" msgstr "Charger une clé publique SSH" -#: src/components/users/RootAuthMethods.jsx:94 -#: src/components/users/RootAuthMethods.jsx:107 +#: src/components/users/RootAuthMethods.jsx:100 +#: src/components/users/RootAuthMethods.jsx:112 msgid "Set" msgstr "Réglé" -#: src/components/users/RootAuthMethods.jsx:129 +#: src/components/users/RootAuthMethods.jsx:132 msgid "Already set" msgstr "Déjà réglé" -#: src/components/users/RootAuthMethods.jsx:130 -#: src/components/users/RootAuthMethods.jsx:134 +#: src/components/users/RootAuthMethods.jsx:132 +#: src/components/users/RootAuthMethods.jsx:136 msgid "Not set" msgstr "Non réglé" #. TRANSLATORS: table header, user authentication method -#: src/components/users/RootAuthMethods.jsx:155 +#: src/components/users/RootAuthMethods.jsx:157 msgid "Method" msgstr "Méthode" -#: src/components/users/RootAuthMethods.jsx:170 +#: src/components/users/RootAuthMethods.jsx:174 msgid "SSH Key" msgstr "Clé SSH" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Change the root password" msgstr "Modifier le mot de passe root" -#: src/components/users/RootAuthMethods.jsx:187 +#: src/components/users/RootAuthMethods.jsx:193 msgid "Set a root password" msgstr "Définir un mot de passe root" -#: src/components/users/RootAuthMethods.jsx:194 -msgid "Add a SSH Public Key for root" -msgstr "Ajouter une clé publique SSH pour root" - -#: src/components/users/RootAuthMethods.jsx:194 +#: src/components/users/RootAuthMethods.jsx:203 msgid "Edit the SSH Public Key for root" msgstr "Modifier la clé publique SSH pour root" -#: src/components/users/RootPasswordPopup.jsx:42 +#: src/components/users/RootAuthMethods.jsx:204 +msgid "Add a SSH Public Key for root" +msgstr "Ajouter une clé publique SSH pour root" + +#: src/components/users/RootPasswordPopup.jsx:43 msgid "Root password" msgstr "Mot de passe root" @@ -2735,6 +2660,38 @@ msgstr "" msgid "Root authentication" msgstr "Authentification root" +#~ msgid "Reading file..." +#~ msgstr "Lecture du fichier..." + +#~ msgid "Cannot read the file" +#~ msgstr "Lecture du fichier impossible" + +#~ msgid "Agama Error" +#~ msgstr "Erreur d'Agama" + +#~ msgid "Loading available products, please wait..." +#~ msgstr "Chargement des produits disponibles, veuillez patienter..." + +#, c-format +#~ msgid "There is %d destructive action planned" +#~ msgid_plural "There are %d destructive actions planned" +#~ msgstr[0] "Il y a %d action destructrice prévue" +#~ msgstr[1] "Il y a %d actions destructrices prévues" + +#, c-format +#~ msgid "Space action selector for %s" +#~ msgstr "Sélecteur d'allocation d'espace pour %s" + +#~ msgid "Allow resize" +#~ msgstr "Permettre le redimensionnement" + +#~ msgid "Do not modify" +#~ msgstr "Ne pas modifier" + +#, fuzzy +#~ msgid "Shrinkable" +#~ msgstr "Rétrécissable de %s" + #, fuzzy #~ msgid "Choose a language" #~ msgstr "Changer de langue" @@ -3108,10 +3065,6 @@ msgstr "Authentification root" #~ msgid "Waiting for information about selected device" #~ msgstr "En attente d'informations sur le périphérique sélectionné" -#, fuzzy -#~ msgid "Waiting for information about space policy" -#~ msgstr "En attente d'informations sur le périphérique sélectionné" - #, fuzzy #~ msgid "Choose a disk for placing the file system" #~ msgstr "Taille exacte du système de fichiers." diff --git a/web/po/id.po b/web/po/id.po index f2164b200d..a51fe577ed 100644 --- a/web/po/id.po +++ b/web/po/id.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-30 02:27+0000\n" +"POT-Creation-Date: 2024-07-14 02:32+0000\n" "PO-Revision-Date: 2024-04-09 17:43+0000\n" "Last-Translator: Arif Budiman \n" "Language-Team: Indonesian

      Localization Section
      ); jest.mock("~/components/overview/StorageSection", () => () =>
      Storage Section
      ); jest.mock("~/components/overview/SoftwareSection", () => () =>
      Software Section
      ); @@ -46,7 +53,6 @@ beforeEach(() => { manager: { startInstallation: startInstallationFn, }, - issues: jest.fn().mockResolvedValue({ isEmpty: true }), }; }); }); @@ -58,9 +64,9 @@ describe("when a product is selected", () => { it("renders the overview page content and the Install button", async () => { installerRender(); - screen.getByText("Localization Section"); - screen.getByText("Storage Section"); - screen.getByText("Software Section"); + screen.findByText("Localization Section"); + screen.findByText("Storage Section"); + screen.findByText("Software Section"); screen.findByText("Install Button"); }); }); From 0138c367afbdb4a2e014b436337374d74c161a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 10:49:18 +0100 Subject: [PATCH 215/430] fix(web): handle the 'no patterns' scenario --- web/src/components/software/SoftwarePage.jsx | 44 +++++++++++++------- web/src/components/software/UsedSize.jsx | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/web/src/components/software/SoftwarePage.jsx b/web/src/components/software/SoftwarePage.jsx index 3ae0c40f6c..001daf5927 100644 --- a/web/src/components/software/SoftwarePage.jsx +++ b/web/src/components/software/SoftwarePage.jsx @@ -100,6 +100,32 @@ const SelectedPatternsList = ({ patterns }) => { ); }; +const SelectedPatterns = ({ patterns }) => ( + + {_("Change selection")} + + } + > + + + + +); + +const NoPatterns = () => ( + + +

      + {_( + "This product does not allow to select software patterns during installation. However, you can add additional software once the installation is finished.", + )} +

      +
      +
      +); // FIXME: move build patterns to utils /** @@ -132,7 +158,7 @@ function SoftwarePage() { }, [client.software, patterns]); useEffect(() => { - if (patterns.length !== 0) return; + if (!isLoading) return; const loadPatterns = async () => { const patterns = await cancellablePromise(client.software.getPatterns()); @@ -143,7 +169,7 @@ function SoftwarePage() { }; loadPatterns(); - }, [client.software, patterns, cancellablePromise]); + }, [client.software, patterns, cancellablePromise, isLoading]); if (status === BUSY || isLoading) { ; @@ -161,20 +187,8 @@ function SoftwarePage() {
      - - {_("Change selection")} - - } - > - - - - + {patterns.length === 0 ? : } - diff --git a/web/src/components/software/UsedSize.jsx b/web/src/components/software/UsedSize.jsx index 499ec1369c..490d8a9684 100644 --- a/web/src/components/software/UsedSize.jsx +++ b/web/src/components/software/UsedSize.jsx @@ -34,7 +34,7 @@ export default function UsedSize({ size }) { return ( -

      {_("This space includes the base system and the selected software patterns.")}

      +

      {_("This space includes the base system and the selected software patterns, if any.")}

      ); } From 9da5e6246975f9376cbf96344df060ceea885807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 10:54:27 +0100 Subject: [PATCH 216/430] doc(web): update the changes file --- web/package/agama-web-ui.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 5cabeb09fe..9bbd0fac95 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jul 17 09:52:36 UTC 2024 - Imobach Gonzalez Sosa + +- Handle the case where there are not user selectable patterns + (gh#openSUSE/agama#1472). + ------------------------------------------------------------------- Fri Jul 12 10:41:28 UTC 2024 - David Diaz From 40d3ac782ebb4d1600de7a650b3224184690cf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 08:14:33 +0100 Subject: [PATCH 217/430] feat(products): rename the package to agama-products --- products.d/_service | 4 ++-- ...agama-products-opensuse.changes => agama-products.changes} | 0 .../{agama-products-opensuse.spec => agama-products.spec} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename products.d/{agama-products-opensuse.changes => agama-products.changes} (100%) rename products.d/{agama-products-opensuse.spec => agama-products.spec} (97%) diff --git a/products.d/_service b/products.d/_service index fa490c0896..cd3b780e4f 100644 --- a/products.d/_service +++ b/products.d/_service @@ -7,8 +7,8 @@ master products.d enable - agama-products-opensuse.changes - agama-products-opensuse.spec + agama-products.changes + agama-products.spec agama.obsinfo diff --git a/products.d/agama-products-opensuse.changes b/products.d/agama-products.changes similarity index 100% rename from products.d/agama-products-opensuse.changes rename to products.d/agama-products.changes diff --git a/products.d/agama-products-opensuse.spec b/products.d/agama-products.spec similarity index 97% rename from products.d/agama-products-opensuse.spec rename to products.d/agama-products.spec index 31b90f3ed2..30153c9909 100644 --- a/products.d/agama-products-opensuse.spec +++ b/products.d/agama-products.spec @@ -15,7 +15,7 @@ # Please submit bugfixes or comments via http://bugs.opensuse.org/ # -Name: agama-products-opensuse +Name: agama-products # This will be set by osc services, that will run after this. Version: 0 Release: 0 From cf82c495397baf86b79815d4aec39a39b7456cdc Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 4 Jul 2024 14:13:25 +0200 Subject: [PATCH 218/430] JSON schema: improve non-storage parts - disallow additional properties - more specific type than string --- rust/agama-lib/share/profile.schema.json | 26 +++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index f260bebe39..e97b8f0c40 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -13,16 +13,23 @@ "properties": { "patterns": { "title": "List of patterns to install", - "type": "array" + "type": "array", + "items": { + "type": "string", + "examples": ["minimal_base"] + } } } }, "product": { "title": "Product to install", "type": "object", + "additionalProperties": false, + "required": ["id"], "properties": { "id": { "title": "Product identifier", + "description": "The id field from a products.d/foo.yaml file", "type": "string" }, "registrationCode": { @@ -46,6 +53,9 @@ "items": { "type": "object", "additionalProperties": false, + "required": [ + "id" + ], "properties": { "id": { "title": "Connection ID", @@ -117,7 +127,8 @@ "type": "string" }, "security": { - "type": "string" + "type": "string", + "examples": ["? maybe this is actually an enum"] }, "ssid": { "type": "string" @@ -139,7 +150,8 @@ "additionalProperties": false, "properties": { "mode": { - "type": "string" + "type": "string", + "examples": ["? maybe this is actually an enum"] }, "options": { "type": "string" @@ -156,6 +168,7 @@ "match": { "type": "object", "title": "Match settings", + "description": "Identifies the network interface to apply the connection settings to", "additionalProperties": false, "properties": { "kernel": { @@ -188,10 +201,7 @@ } } } - }, - "required": [ - "id" - ] + } } } } @@ -199,6 +209,7 @@ "user": { "title": "First user settings", "type": "object", + "additionalProperties": false, "properties": { "fullName": { "title": "Full name (e.g., 'Jane Doe')", @@ -222,6 +233,7 @@ "root": { "title": "Root authentication settings", "type": "object", + "additionalProperties": false, "properties": { "password": { "title": "Root password", From 6caa8c19384f60679fe7f7624fb339f00a12954e Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 17 Jul 2024 11:47:16 +0200 Subject: [PATCH 219/430] JSON schema: put examples to their dedicated schema property --- rust/agama-lib/share/profile.schema.json | 90 ++++++++++++++---------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index e97b8f0c40..659fd27294 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -76,7 +76,7 @@ "minimum": 0 }, "method4": { - "title": "IPv4 configuration method (e.g., 'auto')", + "title": "IPv4 configuration method", "type": "string", "enum": [ "auto", @@ -86,7 +86,7 @@ ] }, "method6": { - "title": "IPv6 configuration method (e.g., 'auto')", + "title": "IPv6 configuration method", "type": "string", "enum": [ "auto", @@ -96,12 +96,14 @@ ] }, "gateway4": { - "title": "Connection gateway address (e.g., '192.168.122.1')", - "type": "string" + "title": "Connection gateway address", + "type": "string", + "examples": ["192.168.122.1"] }, "gateway6": { - "title": "Connection gateway address (e.g., '::ffff:c0a8:7a01')", - "type": "string" + "title": "Connection gateway address", + "type": "string", + "examples": ["::ffff:c0a8:7a01"] }, "addresses": { "type": "array", @@ -212,16 +214,19 @@ "additionalProperties": false, "properties": { "fullName": { - "title": "Full name (e.g., 'Jane Doe')", - "type": "string" + "title": "Full name", + "type": "string", + "examples": ["Jane Doe"] }, "userName": { - "title": "User login name (e.g., 'jane.doe')", - "type": "string" + "title": "User login name", + "type": "string", + "examples": ["jane.doe"] }, "password": { - "title": "User password (e.g., 'nots3cr3t')", - "type": "string" + "title": "User password", + "type": "string", + "examples": ["nots3cr3t"] } }, "required": [ @@ -250,8 +255,9 @@ "type": "object", "properties": { "language": { - "title": "System language ID (e.g., 'en_US')", - "type": "string" + "title": "System language ID", + "type": "string", + "examples": ["en_US"] }, "keyboard": { "title": "Keyboard layout ID", @@ -259,7 +265,8 @@ }, "timezone": { "title": "Time zone identifier such as 'Europe/Berlin'", - "type": "string" + "type": "string", + "examples": ["Europe/Berlin"] } } }, @@ -286,8 +293,9 @@ "required": ["disk"], "properties": { "disk": { - "title": "Disk device name (e.g., '/dev/vda')", - "type": "string" + "title": "Disk device name", + "type": "string", + "examples": ["/dev/vda"] } } }, @@ -301,8 +309,9 @@ "title": "Devices in which to create the physical volumes", "type": "array", "items": { - "title": "Disk device name (e.g., '/dev/vda')", - "type": "string" + "title": "Disk device name", + "type": "string", + "examples": ["/dev/vda"] } } } @@ -320,9 +329,10 @@ "type": "boolean" }, "device": { - "title": "Device to use for booting (e.g., '/dev/vda')", + "title": "Device to use for booting", "description": "The installation device is used by default for booting", - "type": "string" + "type": "string", + "examples": ["/dev/vda"] } } }, @@ -379,8 +389,9 @@ "additionalProperties": false, "properties": { "forceDelete": { - "title": "Device to delete (e.g., '/dev/vda')", - "type": "string" + "title": "Device to delete", + "type": "string", + "examples": ["/dev/vda"] } } }, @@ -392,8 +403,9 @@ "additionalProperties": false, "properties": { "resize": { - "title": "Device to allow resizing (e.g., '/dev/vda')", - "type": "string" + "title": "Device to allow resizing", + "type": "string", + "examples": ["/dev/vda"] } } } @@ -490,7 +502,8 @@ "$ref": "#/$defs/sizeValue" }, "minItems": 1, - "maxItems": 2 + "maxItems": 2, + "examples": [[1024, "2 GiB"]] }, { "title": "Size range", @@ -525,9 +538,10 @@ "additionalProperties": false, "properties": { "newPartition": { - "title": "Name of a disk device (e.g., '/dev/vda')", - "type": "string" - } + "title": "Name of a disk device", + "type": "string", + "examples": ["/dev/vda"] + } } }, { @@ -538,8 +552,9 @@ "required": ["newVg"], "properties": { "newVg": { - "title": "Name of a disk device (e.g., '/dev/vda')", - "type": "string" + "title": "Name of a disk device", + "type": "string", + "examples": ["/dev/vda"] } } }, @@ -551,8 +566,9 @@ "required": ["device"], "properties": { "device": { - "title": "Name of a device (e.g., '/dev/vda1')", - "type": "string" + "title": "Name of a device", + "type": "string", + "examples": ["/dev/vda1"] } } }, @@ -564,8 +580,9 @@ "required": ["filesystem"], "properties": { "filesystem": { - "title": "Name of a device containing the file system (e.g., '/dev/vda1')", - "type": "string" + "title": "Name of a device containing the file system", + "type": "string", + "examples": ["/dev/vda1"] } } } @@ -588,7 +605,8 @@ "sizeString": { "title": "Human readable size (e.g., '2 GiB')", "type": "string", - "pattern": "^[0-9]+(\\.[0-9]+)?(\\s*([KkMmGgTtPpEeZzYy][iI]?)?[Bb])?$" + "pattern": "^[0-9]+(\\.[0-9]+)?(\\s*([KkMmGgTtPpEeZzYy][iI]?)?[Bb])?$", + "examples": ["2 GiB", "1.5 TB", "1TIB", "1073741824 b", "1073741824"] }, "sizeInteger": { "title": "Size in bytes", From f367cf54908b845901c538e290a0f89c4ba95995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 08:36:17 +0100 Subject: [PATCH 220/430] feat(products): add a SLES 16 definition --- products.d/sles_160.yaml | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 products.d/sles_160.yaml diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml new file mode 100644 index 0000000000..ed113f0363 --- /dev/null +++ b/products.d/sles_160.yaml @@ -0,0 +1,135 @@ +id: SLES_16.0 +name: SUSE Linux Enteprise Server 16.0 Alpha +# ------------------------------------------------------------------------------ +# WARNING: When changing the product description delete the translations located +# at the at translations/description key below to avoid using obsolete +# translations!! +# ------------------------------------------------------------------------------ +description: "SUSE Linux Enterprise Server is the open, reliable, compliant, and future-proof + Linux Server choice that ensures the enterprise's business continuity. It is the secure and + adaptable OS for long-term supported, innovation-ready infrastructure running business-critical + workloads on-premises, in the cloud, and at the edge." +# Do not manually change any translations! See README.md for more details. +translations: + description: +software: + installation_repositories: + - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0/product/repo/SLES-Packages-16.0-x86_64/ + archs: x86_64 + - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0/product/repo/SLES-Packages-16.0-aarch64/ + archs: aarch64 + + mandatory_patterns: + - sles_enhanced_base + optional_patterns: null # no optional pattern shared + user_patterns: [] + mandatory_packages: + - NetworkManager + optional_packages: null + base_product: SLES + +security: + lsm: null + available_lsms: + apparmor: + patterns: + - apparmor + selinux: + patterns: + - selinux + policy: enforcing + none: + patterns: null + +storage: + space_policy: delete + volumes: + - "/" + - "swap" + volume_templates: + - mount_path: "/" + filesystem: btrfs + btrfs: + snapshots: true + read_only: false + default_subvolume: "@" + subvolumes: + - path: home + - path: opt + - path: root + - path: srv + - path: usr/local + # Unified var subvolume - https://lists.opensuse.org/opensuse-packaging/2017-11/msg00017.html + - path: var + copy_on_write: false + # Architecture specific subvolumes + - path: boot/grub2/arm64-efi + archs: aarch64 + - path: boot/grub2/arm-efi + archs: arm + - path: boot/grub2/i386-pc + archs: x86_64 + - path: boot/grub2/powerpc-ieee1275 + archs: ppc,!board_powernv + - path: boot/grub2/s390x-emu + archs: s390 + - path: boot/grub2/x86_64-efi + archs: x86_64 + - path: boot/grub2/riscv64-efi + archs: riscv64 + size: + auto: true + outline: + required: true + filesystems: + - btrfs + - ext2 + - ext3 + - ext4 + - xfs + auto_size: + base_min: 5 GiB + base_max: 15 GiB + snapshots_increment: 250% + max_fallback_for: + - "/home" + snapshots_configurable: true + - mount_path: "swap" + filesystem: swap + size: + auto: true + outline: + auto_size: + base_min: 1 GiB + base_max: 2 GiB + adjust_by_ram: true + required: false + filesystems: + - swap + - mount_path: "/home" + filesystem: xfs + size: + auto: false + min: 10 GiB + max: unlimited + outline: + required: false + filesystems: + - btrfs + - ext2 + - ext3 + - ext4 + - xfs + - filesystem: xfs + size: + auto: false + min: 1 GiB + outline: + required: false + filesystems: + - btrfs + - ext2 + - ext3 + - ext4 + - xfs + - vfat From da621b92c6f1371ad968771cc749fe04e3ffc3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 08:36:53 +0100 Subject: [PATCH 221/430] feat(products): split in two packages * Use agama-products-opensuse for openSUSE-based products. * Use agama-products-sle for SLE-based products. --- products.d/agama-products.spec | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/products.d/agama-products.spec b/products.d/agama-products.spec index 30153c9909..ef40297dad 100644 --- a/products.d/agama-products.spec +++ b/products.d/agama-products.spec @@ -19,14 +19,14 @@ Name: agama-products # This will be set by osc services, that will run after this. Version: 0 Release: 0 -Summary: Definition of openSUSE products for the Agama installer +Summary: Definition of products for the Agama installer License: GPL-2.0-only Url: https://github.com/opensuse/agama BuildArch: noarch Source0: agama.tar %description -Products definition for Agama installer. This one is for opensuse products. +Products definition for Agama installer. %prep %autosetup -a0 -n agama @@ -37,7 +37,13 @@ Products definition for Agama installer. This one is for opensuse products. install -D -d -m 0755 %{buildroot}%{_datadir}/agama/products.d install -m 0644 *.yaml %{buildroot}%{_datadir}/agama/products.d -%files +%package opensuse +Summary: Definition of openSUSE products for the Agama installer. + +%description opensuse +Definition of openSUSE products (Tumbleweed, Leap and MicroOS) for the Agama installer. + +%files opensuse %doc README.md %license LICENSE %dir %{_datadir}/agama @@ -46,4 +52,18 @@ install -m 0644 *.yaml %{buildroot}%{_datadir}/agama/products.d %{_datadir}/agama/products.d/tumbleweed.yaml %{_datadir}/agama/products.d/leap_160.yaml +%package sle +Summary: Definition of SLE products for the Agama installer. + +%description sle +SLE-based products definition for Agama installer. +Definition of SLE-based products (e.g., SUSE Linux Enterprise Server) for the Agama installer. + +%files sle +%doc README.md +%license LICENSE +%dir %{_datadir}/agama +%dir %{_datadir}/agama/products.d +%{_datadir}/agama/products.d/sles_160.yaml + %changelog From 8955b2dd2add41fca44763309e7ef946fee8e856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 08:38:01 +0100 Subject: [PATCH 222/430] doc(products): update changes file --- products.d/agama-products.changes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes index 6bd4e35180..638a8a7e7d 100644 --- a/products.d/agama-products.changes +++ b/products.d/agama-products.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Wed Jul 17 07:34:01 UTC 2024 - Imobach Gonzalez Sosa + +- Add a definition for SUSE Linux Enteprise Server + (gh#openSUSE/agama#1473). +- Generate separate packages for openSUSE and SLE-based products. + ------------------------------------------------------------------- Tue Jul 12 17:29:00 UTC 2024 - Natasha Ament From 1fcc44cac6fde37592ffb7bc1ca20e3485908aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 11:50:22 +0100 Subject: [PATCH 223/430] fix(ci): submit products to agama-products --- .github/workflows/obs-staging-products.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/obs-staging-products.yml b/.github/workflows/obs-staging-products.yml index f7bed76802..57cca558b6 100644 --- a/.github/workflows/obs-staging-products.yml +++ b/.github/workflows/obs-staging-products.yml @@ -18,5 +18,5 @@ jobs: # pass all secrets secrets: inherit with: - package_name: agama-products-opensuse + package_name: agama-products service_file: products.d/_service From 65b03b8e9d1ff396392b162853fc078ad5f1bed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 12:05:19 +0100 Subject: [PATCH 224/430] fix(products): update from code review --- products.d/sles_160.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index ed113f0363..e87e36b030 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -29,7 +29,7 @@ software: base_product: SLES security: - lsm: null + lsm: none available_lsms: apparmor: patterns: From 3ab9103decd7db54e98e2802041daf4edf77cc79 Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg Date: Tue, 11 Jun 2024 15:44:37 +0200 Subject: [PATCH 225/430] Add DNS search domains to network model --- rust/agama-server/src/network/model.rs | 2 + rust/agama-server/src/network/nm/dbus.rs | 75 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 739a7b6c47..2e20446432 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -759,6 +759,8 @@ pub struct IpConfig { pub addresses: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub nameservers: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub dns_searchlist: Vec, pub gateway4: Option, pub gateway6: Option, pub routes4: Option>, diff --git a/rust/agama-server/src/network/nm/dbus.rs b/rust/agama-server/src/network/nm/dbus.rs index b0e66c0b77..1ae32b0c70 100644 --- a/rust/agama-server/src/network/nm/dbus.rs +++ b/rust/agama-server/src/network/nm/dbus.rs @@ -297,6 +297,7 @@ fn ip_config_to_ipv4_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value let mut ipv4_dbus = HashMap::from([ ("address-data", address_data), ("dns-data", dns_data), + ("dns-search", ip_config.dns_searchlist.clone().into()), ("method", ip_config.method4.to_string().into()), ]); @@ -342,6 +343,7 @@ fn ip_config_to_ipv6_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value let mut ipv6_dbus = HashMap::from([ ("address-data", address_data), ("dns-data", dns_data), + ("dns-search", ip_config.dns_searchlist.clone().into()), ("method", ip_config.method6.to_string().into()), ]); @@ -718,6 +720,20 @@ fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option { ip_config.nameservers.append(&mut servers); } + if let Some(dns_search) = ipv4.get("dns-search") { + let searchlist: Vec = dns_search + .downcast_ref::()? + .iter() + .flat_map(|x| x.downcast_ref::()) + .map(|x| x.to_string()) + .collect(); + for searchdomain in searchlist { + if !ip_config.dns_searchlist.contains(&searchdomain) { + ip_config.dns_searchlist.push(searchdomain); + } + } + } + if let Some(route_data) = ipv4.get("route-data") { ip_config.routes4 = routes_from_dbus(route_data); } @@ -742,6 +758,20 @@ fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option { ip_config.nameservers.append(&mut servers); } + if let Some(dns_search) = ipv6.get("dns-search") { + let searchlist: Vec = dns_search + .downcast_ref::()? + .iter() + .flat_map(|x| x.downcast_ref::()) + .map(|x| x.to_string()) + .collect(); + for searchdomain in searchlist { + if !ip_config.dns_searchlist.contains(&searchdomain) { + ip_config.dns_searchlist.push(searchdomain); + } + } + } + if let Some(route_data) = ipv6.get("route-data") { ip_config.routes6 = routes_from_dbus(route_data); } @@ -1005,6 +1035,10 @@ mod test { "dns-data".to_string(), Value::new(vec!["192.168.0.2"]).to_owned(), ), + ( + "dns-search".to_string(), + Value::new(vec!["suse.com", "example.com"]).to_owned(), + ), ( "route-data".to_string(), Value::new(route_v4_data).to_owned(), @@ -1037,6 +1071,10 @@ mod test { "dns-data".to_string(), Value::new(vec!["::ffff:c0a8:102"]).to_owned(), ), + ( + "dns-search".to_string(), + Value::new(vec!["suse.com", "suse.de"]).to_owned(), + ), ( "route-data".to_string(), Value::new(route_v6_data).to_owned(), @@ -1081,6 +1119,12 @@ mod test { "::ffff:c0a8:102".parse::().unwrap() ] ); + assert_eq!(ip_config.dns_searchlist.len(), 3); + assert!(ip_config.dns_searchlist.contains(&"suse.com".to_string())); + assert!(ip_config.dns_searchlist.contains(&"suse.de".to_string())); + assert!(ip_config + .dns_searchlist + .contains(&"example.com".to_string())); assert_eq!(ip_config.method4, Ipv4Method::Auto); assert_eq!(ip_config.method6, Ipv6Method::Auto); assert_eq!( @@ -1506,6 +1550,7 @@ mod test { next_hop: Some(IpAddr::from_str("2001:db8::1").unwrap()), metric: Some(100), }]), + dns_searchlist: vec!["suse.com".to_string(), "suse.de".to_string()], ..Default::default() }; let mac_address = MacAddress::from_str("FD:CB:A9:87:65:43").unwrap(); @@ -1562,6 +1607,21 @@ mod test { assert!(route4_hashmap.contains_key("metric")); assert_eq!(route4_hashmap["metric"], Value::from(100_u32)); } + let dns_searchlist_array: Array = ipv4_dbus + .get("dns-search") + .unwrap() + .downcast_ref::() + .unwrap() + .try_into() + .unwrap(); + let dns_searchlist: Vec = dns_searchlist_array + .iter() + .flat_map(|x| x.downcast_ref::()) + .map(|x| x.to_string()) + .collect(); + assert_eq!(dns_searchlist.len(), 2); + assert!(dns_searchlist.contains(&"suse.com".to_string())); + assert!(dns_searchlist.contains(&"suse.de".to_string())); let ipv6_dbus = conn_dbus.get("ipv6").unwrap(); let gateway6: &str = ipv6_dbus.get("gateway").unwrap().downcast_ref().unwrap(); @@ -1585,5 +1645,20 @@ mod test { assert!(route6_hashmap.contains_key("metric")); assert_eq!(route6_hashmap["metric"], Value::from(100_u32)); } + let dns_searchlist_array: Array = ipv6_dbus + .get("dns-search") + .unwrap() + .downcast_ref::() + .unwrap() + .try_into() + .unwrap(); + let dns_searchlist: Vec = dns_searchlist_array + .iter() + .flat_map(|x| x.downcast_ref::()) + .map(|x| x.to_string()) + .collect(); + assert_eq!(dns_searchlist.len(), 2); + assert!(dns_searchlist.contains(&"suse.com".to_string())); + assert!(dns_searchlist.contains(&"suse.de".to_string())); } } From e433db832067deea6efa8ed5a44b3670e2b3624c Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg Date: Tue, 11 Jun 2024 15:51:03 +0200 Subject: [PATCH 226/430] Add ignore-auto-dns to network model --- rust/agama-server/src/network/model.rs | 1 + rust/agama-server/src/network/nm/dbus.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 2e20446432..737afe6a16 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -761,6 +761,7 @@ pub struct IpConfig { pub nameservers: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub dns_searchlist: Vec, + pub ignore_auto_dns: bool, pub gateway4: Option, pub gateway6: Option, pub routes4: Option>, diff --git a/rust/agama-server/src/network/nm/dbus.rs b/rust/agama-server/src/network/nm/dbus.rs index 1ae32b0c70..b86853a8ad 100644 --- a/rust/agama-server/src/network/nm/dbus.rs +++ b/rust/agama-server/src/network/nm/dbus.rs @@ -298,6 +298,7 @@ fn ip_config_to_ipv4_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value ("address-data", address_data), ("dns-data", dns_data), ("dns-search", ip_config.dns_searchlist.clone().into()), + ("ignore-auto-dns", ip_config.ignore_auto_dns.into()), ("method", ip_config.method4.to_string().into()), ]); @@ -344,6 +345,7 @@ fn ip_config_to_ipv6_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value ("address-data", address_data), ("dns-data", dns_data), ("dns-search", ip_config.dns_searchlist.clone().into()), + ("ignore-auto-dns", ip_config.ignore_auto_dns.into()), ("method", ip_config.method6.to_string().into()), ]); @@ -734,6 +736,10 @@ fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option { } } + if let Some(ignore_auto_dns) = ipv4.get("ignore-auto-dns") { + ip_config.ignore_auto_dns = ignore_auto_dns.try_into().ok()?; + } + if let Some(route_data) = ipv4.get("route-data") { ip_config.routes4 = routes_from_dbus(route_data); } @@ -772,6 +778,10 @@ fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option { } } + if let Some(ignore_auto_dns) = ipv6.get("ignore-auto-dns") { + ip_config.ignore_auto_dns = ignore_auto_dns.try_into().ok()?; + } + if let Some(route_data) = ipv6.get("route-data") { ip_config.routes6 = routes_from_dbus(route_data); } @@ -1039,6 +1049,7 @@ mod test { "dns-search".to_string(), Value::new(vec!["suse.com", "example.com"]).to_owned(), ), + ("ignore-auto-dns".to_string(), Value::new(true).to_owned()), ( "route-data".to_string(), Value::new(route_v4_data).to_owned(), @@ -1125,6 +1136,7 @@ mod test { assert!(ip_config .dns_searchlist .contains(&"example.com".to_string())); + assert!(ip_config.ignore_auto_dns); assert_eq!(ip_config.method4, Ipv4Method::Auto); assert_eq!(ip_config.method6, Ipv6Method::Auto); assert_eq!( @@ -1622,6 +1634,11 @@ mod test { assert_eq!(dns_searchlist.len(), 2); assert!(dns_searchlist.contains(&"suse.com".to_string())); assert!(dns_searchlist.contains(&"suse.de".to_string())); + assert!(!ipv4_dbus + .get("ignore-auto-dns") + .unwrap() + .downcast_ref::() + .unwrap()); let ipv6_dbus = conn_dbus.get("ipv6").unwrap(); let gateway6: &str = ipv6_dbus.get("gateway").unwrap().downcast_ref().unwrap(); @@ -1660,5 +1677,10 @@ mod test { assert_eq!(dns_searchlist.len(), 2); assert!(dns_searchlist.contains(&"suse.com".to_string())); assert!(dns_searchlist.contains(&"suse.de".to_string())); + assert!(!ipv6_dbus + .get("ignore-auto-dns") + .unwrap() + .downcast_ref::() + .unwrap()); } } From 826b08e0d7eff7ab9ff23e24c10e514f93df500a Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg Date: Wed, 17 Jul 2024 12:57:45 +0200 Subject: [PATCH 227/430] Add additional DNS properties to network settings dns_searchlist and ignore_auto_dns --- rust/agama-lib/share/profile.schema.json | 12 ++++++++++++ rust/agama-lib/src/network/proxies.rs | 12 ++++++++++++ rust/agama-lib/src/network/settings.rs | 4 ++++ rust/agama-server/src/network/model.rs | 9 +++++++++ 4 files changed, 37 insertions(+) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index f260bebe39..7a2cde7fa8 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -108,6 +108,18 @@ "type": "string" } }, + "dns_searchlist": { + "type": "array", + "items": { + "description": "DNS search domains", + "type": "string", + "additionalProperties": false + } + }, + "ignore_auto_dns": { + "description": "Whether DNS options provided via DHCP are used or not", + "type": "boolean" + }, "wireless": { "type": "object", "title": "Wireless configuration", diff --git a/rust/agama-lib/src/network/proxies.rs b/rust/agama-lib/src/network/proxies.rs index 9e1de983d3..45321da3bf 100644 --- a/rust/agama-lib/src/network/proxies.rs +++ b/rust/agama-lib/src/network/proxies.rs @@ -159,6 +159,18 @@ trait IP { fn nameservers(&self) -> zbus::Result>; #[dbus_proxy(property)] fn set_nameservers(&self, value: &[&str]) -> zbus::Result<()>; + + /// DNS searchlist property + #[dbus_proxy(property)] + fn dns_searchlist(&self) -> zbus::Result>; + #[dbus_proxy(property)] + fn set_dns_searchlist(&self, value: &[&str]) -> zbus::Result<()>; + + /// Ignore auto DNS property + #[dbus_proxy(property)] + fn ignore_auto_dns(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_ignore_auto_dns(&self, value: bool) -> zbus::Result<()>; } #[dbus_proxy( diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 256ed0d055..7ec3be4068 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -85,6 +85,10 @@ pub struct NetworkConnection { pub addresses: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub nameservers: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub dns_searchlist: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub ignore_auto_dns: Option, #[serde(skip_serializing_if = "Option::is_none")] pub wireless: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 737afe6a16..bbe2f8668d 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -585,6 +585,10 @@ impl TryFrom for Connection { connection.status = status; } + if let Some(ignore_auto_dns) = conn.ignore_auto_dns { + connection.ip_config.ignore_auto_dns = ignore_auto_dns; + } + if let Some(wireless_config) = conn.wireless { let config = WirelessConfig::try_from(wireless_config)?; connection.config = config.into(); @@ -597,6 +601,7 @@ impl TryFrom for Connection { connection.ip_config.addresses = conn.addresses; connection.ip_config.nameservers = conn.nameservers; + connection.ip_config.dns_searchlist = conn.dns_searchlist; connection.ip_config.gateway4 = conn.gateway4; connection.ip_config.gateway6 = conn.gateway6; connection.interface = conn.interface; @@ -616,6 +621,8 @@ impl TryFrom for NetworkConnection { let method6 = Some(conn.ip_config.method6.to_string()); let mac_address = (!mac.is_empty()).then_some(mac); let nameservers = conn.ip_config.nameservers; + let dns_searchlist = conn.ip_config.dns_searchlist; + let ignore_auto_dns = Some(conn.ip_config.ignore_auto_dns); let addresses = conn.ip_config.addresses; let gateway4 = conn.ip_config.gateway4; let gateway6 = conn.ip_config.gateway6; @@ -631,6 +638,8 @@ impl TryFrom for NetworkConnection { gateway4, gateway6, nameservers, + dns_searchlist, + ignore_auto_dns, mac_address, interface, addresses, From 16017c282abbe0690fd3a75bde8e74081b50a62f Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg Date: Thu, 13 Jun 2024 15:17:13 +0200 Subject: [PATCH 228/430] Update changes --- rust/package/agama.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 0df4850cb2..970368fce3 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jul 17 11:15:33 UTC 2024 - Jorik Cronenberg + +- Add dns search domains and ignore-auto-dns to network settings + (gh#openSUSE/agama#1330). + ------------------------------------------------------------------- Tue Jul 16 11:56:29 UTC 2024 - Josef Reidinger From bdc3bbcf78df0055b2ee9d6d95fe67977d31b15c Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 09:44:49 +0200 Subject: [PATCH 229/430] initial version of report patching --- service/lib/agama/autoyast/converter.rb | 2 + service/lib/agama/autoyast/report_patching.rb | 106 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 service/lib/agama/autoyast/report_patching.rb diff --git a/service/lib/agama/autoyast/converter.rb b/service/lib/agama/autoyast/converter.rb index e49fd97ce1..0dfcadaa96 100755 --- a/service/lib/agama/autoyast/converter.rb +++ b/service/lib/agama/autoyast/converter.rb @@ -33,6 +33,8 @@ require "fileutils" require "pathname" +require "agama/autoyast/report_patching" + # :nodoc: module Agama module AutoYaST diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb new file mode 100644 index 0000000000..609554b202 --- /dev/null +++ b/service/lib/agama/autoyast/report_patching.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +# Goal of this file is monkey patch Popup and Report functionality to not try to use UI +# and instead adapt it for agama needs. +# Do not directly require agama ruby files to be able to keep autoyast converter and agama +# independent. +# TODO: what to do if it runs without agama? Just print question to stderr? + +require "yaml" +require "yast/execute" + +module Yast2 + class Popup + class << self + # Keep in sync with real Yast2::Popup + def show(message, details: "", headline: "", timeout: 0, focus: nil, buttons: :ok, richtext: false, style: :notice) + # at first construct agama question to display. + # NOTE: timeout is not supported. + # FIXME: what to do with richtext? + text = message + text += "\n\n" + details unless details.empty? + options = generate_options(buttons) + question = { + # TODO: id for newly created question is ignored, but maybe it will be better to not have to specify it at all? + id: 0, + class: "autoyast.popup", + text: text, + options: generate_options(buttons), + default_option: focus || options.first, + data: {} + } + data = { generic: question }.to_yaml + answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) + answer = YAML.load(answer_yaml) + answer["generic"]["answer"].to_sym + end + + private + + def generate_options(buttons) + case buttons + when :ok + [:ok] + when :continue_cancel + [:continue, :cancel] + when :yes_no + [:yes, :no] + when Hash + buttons.keys + else + raise ArgumentError, "Invalid value #{buttons.inspect} for buttons" + end + end + end + end +end + +# needed to ask for GPG encrypted autoyast profiles +# TODO: encrypt agama profile? is it needed? +module UI + class PasswordDialog + def new(label, confirm: false) + @label = label + # NOTE: implement confirm if needed + end + + def run + # at first construct agama question to display. + text = @label + question = { + # TODO: id for newly created question is ignored, but maybe it will be better to not have to specify it at all? + id: 0, + class: "autoyast.password", + text: text, + options: ["ok", "cancel"], + default_option: "cancel", + data: {} + } + data = { generic: question, with_password: {} }.to_yaml + answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) + answer = YAML.load(answer_yaml) + result = answer["generic"]["answer"].to_sym + return nil if result == "cancel" + answer["with_password"]["password"] + end + end +end From fb7603689d3b65a2f91cbdcbae75de45d87042b8 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 14:11:31 +0200 Subject: [PATCH 230/430] add readme for agama-yast with hint how to deploy modified gem --- service/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 service/README.md diff --git a/service/README.md b/service/README.md new file mode 100644 index 0000000000..63261976aa --- /dev/null +++ b/service/README.md @@ -0,0 +1,21 @@ +# Agama YaST + +According to [Agama's architecture](../doc/architecture.md) this project implements the following components: + +* The *Agama YaST*, the layer build on top of YaST functionality. + +## Testing Changes + +The easiest way to test changes done to ruby code on agama liveCD is to build +gem with modified sources with `gem build agama-yast`. Then copy resulting file +to agama live ISO. There do this sequence of commands: + +```sh +# ensure that only modified sources are installed +gem uninstall agama-yast +# install modified sources including proper binary names +gem install --no-doc --no-format-executable +``` + +If change modifies also dbus parts, then restart related dbus services. + From ddd18bfd756c4941a4af07429ef9ff866086c9f5 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 14:20:54 +0200 Subject: [PATCH 231/430] fix file loading --- service/lib/agama/autoyast/report_patching.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb index 609554b202..62a845415b 100644 --- a/service/lib/agama/autoyast/report_patching.rb +++ b/service/lib/agama/autoyast/report_patching.rb @@ -26,7 +26,7 @@ # TODO: what to do if it runs without agama? Just print question to stderr? require "yaml" -require "yast/execute" +require "yast2/execute" module Yast2 class Popup From d2a7391aca6136920a62f6f8e0765eea01be8910 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 14:27:17 +0200 Subject: [PATCH 232/430] make rubocop happy --- service/lib/agama/autoyast/bond_reader.rb | 0 .../lib/agama/autoyast/connections_reader.rb | 0 service/lib/agama/autoyast/l10n_reader.rb | 0 service/lib/agama/autoyast/network_reader.rb | 0 service/lib/agama/autoyast/product_reader.rb | 0 service/lib/agama/autoyast/report_patching.rb | 28 ++++++++++++++----- service/lib/agama/autoyast/root_reader.rb | 1 - service/lib/agama/autoyast/software_reader.rb | 1 - service/lib/agama/autoyast/storage_reader.rb | 1 - service/lib/agama/autoyast/user_reader.rb | 1 - service/lib/agama/autoyast/wireless_reader.rb | 1 - 11 files changed, 21 insertions(+), 12 deletions(-) mode change 100644 => 100755 service/lib/agama/autoyast/bond_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/connections_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/l10n_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/network_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/product_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/root_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/software_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/storage_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/user_reader.rb mode change 100644 => 100755 service/lib/agama/autoyast/wireless_reader.rb diff --git a/service/lib/agama/autoyast/bond_reader.rb b/service/lib/agama/autoyast/bond_reader.rb old mode 100644 new mode 100755 diff --git a/service/lib/agama/autoyast/connections_reader.rb b/service/lib/agama/autoyast/connections_reader.rb old mode 100644 new mode 100755 diff --git a/service/lib/agama/autoyast/l10n_reader.rb b/service/lib/agama/autoyast/l10n_reader.rb old mode 100644 new mode 100755 diff --git a/service/lib/agama/autoyast/network_reader.rb b/service/lib/agama/autoyast/network_reader.rb old mode 100644 new mode 100755 diff --git a/service/lib/agama/autoyast/product_reader.rb b/service/lib/agama/autoyast/product_reader.rb old mode 100644 new mode 100755 diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb index 62a845415b..dbfd7fef28 100644 --- a/service/lib/agama/autoyast/report_patching.rb +++ b/service/lib/agama/autoyast/report_patching.rb @@ -28,11 +28,16 @@ require "yaml" require "yast2/execute" +# :nodoc: +# rubocop:disable Metrics/ParameterLists +# rubocop:disable Lint/UnusedMethodArgument module Yast2 + # :nodoc: class Popup class << self # Keep in sync with real Yast2::Popup - def show(message, details: "", headline: "", timeout: 0, focus: nil, buttons: :ok, richtext: false, style: :notice) + def show(message, details: "", headline: "", timeout: 0, focus: nil, + buttons: :ok, richtext: false, style: :notice) # at first construct agama question to display. # NOTE: timeout is not supported. # FIXME: what to do with richtext? @@ -40,7 +45,8 @@ def show(message, details: "", headline: "", timeout: 0, focus: nil, buttons: :o text += "\n\n" + details unless details.empty? options = generate_options(buttons) question = { - # TODO: id for newly created question is ignored, but maybe it will be better to not have to specify it at all? + # TODO: id for newly created question is ignored, but maybe it will + # be better to not have to specify it at all? id: 0, class: "autoyast.popup", text: text, @@ -49,8 +55,9 @@ def show(message, details: "", headline: "", timeout: 0, focus: nil, buttons: :o data: {} } data = { generic: question }.to_yaml - answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) - answer = YAML.load(answer_yaml) + answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", + stdin: data, stdout: :capture) + answer = YAML.safe_load(answer_yaml) answer["generic"]["answer"].to_sym end @@ -77,6 +84,7 @@ def generate_options(buttons) # needed to ask for GPG encrypted autoyast profiles # TODO: encrypt agama profile? is it needed? module UI + # :nodoc: class PasswordDialog def new(label, confirm: false) @label = label @@ -87,7 +95,8 @@ def run # at first construct agama question to display. text = @label question = { - # TODO: id for newly created question is ignored, but maybe it will be better to not have to specify it at all? + # TODO: id for newly created question is ignored, but maybe it will + # be better to not have to specify it at all? id: 0, class: "autoyast.password", text: text, @@ -96,11 +105,16 @@ def run data: {} } data = { generic: question, with_password: {} }.to_yaml - answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) - answer = YAML.load(answer_yaml) + answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, +stdout: :capture) + answer = YAML.safe_load(answer_yaml) result = answer["generic"]["answer"].to_sym return nil if result == "cancel" + answer["with_password"]["password"] end end end + +# rubocop:enable Metrics/ParameterLists +# rubocop:enable Lint/UnusedMethodArgument diff --git a/service/lib/agama/autoyast/root_reader.rb b/service/lib/agama/autoyast/root_reader.rb old mode 100644 new mode 100755 index 2b009069f5..1598cc5f92 --- a/service/lib/agama/autoyast/root_reader.rb +++ b/service/lib/agama/autoyast/root_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/software_reader.rb b/service/lib/agama/autoyast/software_reader.rb old mode 100644 new mode 100755 index b4edc774e1..213c7b4c48 --- a/service/lib/agama/autoyast/software_reader.rb +++ b/service/lib/agama/autoyast/software_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/storage_reader.rb b/service/lib/agama/autoyast/storage_reader.rb old mode 100644 new mode 100755 index 96a00e975c..7e0b543a9f --- a/service/lib/agama/autoyast/storage_reader.rb +++ b/service/lib/agama/autoyast/storage_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/user_reader.rb b/service/lib/agama/autoyast/user_reader.rb old mode 100644 new mode 100755 index ad850ff550..1df0ec05b3 --- a/service/lib/agama/autoyast/user_reader.rb +++ b/service/lib/agama/autoyast/user_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC diff --git a/service/lib/agama/autoyast/wireless_reader.rb b/service/lib/agama/autoyast/wireless_reader.rb old mode 100644 new mode 100755 index 3a273eec68..9c4bb5f3e5 --- a/service/lib/agama/autoyast/wireless_reader.rb +++ b/service/lib/agama/autoyast/wireless_reader.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true # Copyright (c) [2024] SUSE LLC From 9c2c97ab5d5b564a47d7cc5750502c14218e90bc Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 18 Jul 2024 14:41:24 +0200 Subject: [PATCH 233/430] JSON schema: specify enums for wireless security and bond mode --- rust/agama-lib/share/profile.schema.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 659fd27294..b1607caad8 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -130,7 +130,15 @@ }, "security": { "type": "string", - "examples": ["? maybe this is actually an enum"] + "enum": [ + "none", + "owe", + "ieee8021x", + "wpa-psk", + "sae", + "wpa-eap", + "wpa-eap-suite-b192" + ] }, "ssid": { "type": "string" @@ -153,7 +161,15 @@ "properties": { "mode": { "type": "string", - "examples": ["? maybe this is actually an enum"] + "enum": [ + "balance-rr", + "active-backup", + "balance-xor", + "broadcast", + "802.3ad", + "balance-tlb", + "balance-alb" + ] }, "options": { "type": "string" From afabdc035a28476fadce31497207d6db37b77883 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 17 Jul 2024 12:14:49 +0200 Subject: [PATCH 234/430] chore: testing-using-container: fix container project name --- testing_using_container.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing_using_container.sh b/testing_using_container.sh index 60362af709..988d3d8fd4 100755 --- a/testing_using_container.sh +++ b/testing_using_container.sh @@ -13,7 +13,7 @@ set -x set -eu # https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/agama-testing -CIMAGE=registry.opensuse.org/systemsmanagement/agama/staging/containers/opensuse/agama-testing:latest +CIMAGE=registry.opensuse.org/systemsmanagement/agama/devel/containers/opensuse/agama-testing:latest # rename this if you test multiple things CNAME=agama From cee1b6f5f4e8c2944cfef6e2b4023dbc8ab5c628 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 17 Jul 2024 12:15:41 +0200 Subject: [PATCH 235/430] setup-web.sh: run npm-install unconditionally otherwise npm run fails after the requirements change and we keep working with an outdated environment: "Cannot find module react-refresh-typescript" --- setup-web.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup-web.sh b/setup-web.sh index 5eea2ca5f0..440f1af438 100755 --- a/setup-web.sh +++ b/setup-web.sh @@ -23,10 +23,7 @@ $SUDO zypper --non-interactive install \ cd web -if [ ! -e node_modules ]; then - npm install -fi - +npm install npm run build cd - From c6d025c02eb6f66a27bd5a237a1895242f065375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 16 Jul 2024 15:36:41 +0200 Subject: [PATCH 236/430] Use Puppeteer for integration tests --- puppeteer/.gitignore | 2 + puppeteer/README.md | 93 + puppeteer/agama-integration-tests | 16 + puppeteer/package-lock.json | 3138 +++++++++++++++++ puppeteer/package.json | 8 + puppeteer/package/_service | 26 + .../package/agama-integration-tests.changes | 5 + .../package/agama-integration-tests.spec | 68 + puppeteer/tests/test_root_password.js | 169 + 9 files changed, 3525 insertions(+) create mode 100644 puppeteer/.gitignore create mode 100644 puppeteer/README.md create mode 100755 puppeteer/agama-integration-tests create mode 100644 puppeteer/package-lock.json create mode 100644 puppeteer/package.json create mode 100644 puppeteer/package/_service create mode 100644 puppeteer/package/agama-integration-tests.changes create mode 100644 puppeteer/package/agama-integration-tests.spec create mode 100644 puppeteer/tests/test_root_password.js diff --git a/puppeteer/.gitignore b/puppeteer/.gitignore new file mode 100644 index 0000000000..f2b93381e9 --- /dev/null +++ b/puppeteer/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +log/ diff --git a/puppeteer/README.md b/puppeteer/README.md new file mode 100644 index 0000000000..6b0ce8b431 --- /dev/null +++ b/puppeteer/README.md @@ -0,0 +1,93 @@ +# Agama Integration Tests + +This is directory contains support writing integration tests for Agama. It is +based on the [Puppeteer](https://pptr.dev/) library. + +Currently there is only one simple test which only selects the product to +install and sets the root password. More tests will be implemented separately in +the openQA, this package basically just ensures that the needed libraries and +tools are present on the Live ISO. + +## Running Tests + +The integration tests can be started from a Git checkout or from a Live ISO. + +### Live ISO + +To run the test from Live ISO: + +```sh +agama-integration-tests /usr/share/agama/integration-tests/tests/test_root_password.js +``` + +This runs a headless test which expects the Agama is running on the local +machine. See the [Options](#options) section below how to customize the test +run. + +### Git + +To run the test directly from Git checkout: + +```sh +./agama-integration-tests tests/test_root_password.js +``` + +At the first run it installs Puppeteer and the dependant NPM packages. You can +install them manually with this command: + +```sh +PUPPETEER_SKIP_DOWNLOAD=true npm install --omit=optional +``` + +## Options + +The recommended command to run the test during development is + +```sh +AGAMA_BROWSER=chromium AGAMA_SERVER=https://agama.local AGAMA_SLOWMO=50 \ +AGAMA_HEADLESS=false ./agama-integration-tests tests/test_root_password.js +``` + +The options are described below. + +### Test Browser + +By default the test uses the Firefox browser but it is possible to use Chromium +or Google Chrome as well. See the [supported browsers](#supported-browsers) +section below. + +Set `AGAMA_BROWSER=chromium` or `AGAMA_BROWSER=chrome` to use different +browsers. + +### Headless Mode + +The test runs in headless mode (no UI displayed). For development or debugging +it might be better to see the real browser running the test. + +Set `AGAMA_HEADLESS=false` to display the browser during the test. + +When running the test from the Live ISO you need to enable the X forwarding +(`ssh -X` option) or set `DISPLAY=:0` to use the locally running X server. + +### Target Agama Server + +The test connects to a locally running Agama, for using a remote server set +the `AGAMA_SERVER` to the server URL. + +### Slow Motion + +Because the browser is controlled by a script the actions might be too fast to +watch. Use the `AGAMA_SLOWMO` variable with a delay in miliseconds between the +actions. A reasonable value is round 50. + +## Supported Browsers + +The Puppeteer library was originally written for the Chromium browser, but later +they added support also for the Firefox browser. However, not all features might +be supported in Firefox, e.g. it cannot record a video of the test run. See +more details in the [Puppeteer documentation](https://pptr.dev/webdriver-bidi). + +> [!NOTE] +Unfortunately the Firefox version installed in SLE15 and openSUSE Leap 15.x is +too old and does not work with Puppeteer. The version in openSUSE Tumbleweed +works fine. diff --git a/puppeteer/agama-integration-tests b/puppeteer/agama-integration-tests new file mode 100755 index 0000000000..2617846ef6 --- /dev/null +++ b/puppeteer/agama-integration-tests @@ -0,0 +1,16 @@ +#! /usr/bin/sh + +# exit on error, unset variables are an error +set -eu + +MYDIR=$(realpath "$(dirname "$0")") + +if [ -e "$MYDIR/../.git/" ]; then + PUPPETEER_SKIP_DOWNLOAD=true npm install --omit=optional + npx mocha --bail "$@" +else + # set the default load path + export NODE_PATH=/usr/share/agama/integration-tests/node_modules + # run the CLI script directly, npm/npx might not be installed + /usr/bin/env node /usr/share/agama/integration-tests/node_modules/mocha/bin/mocha.js --bail "$@" +fi diff --git a/puppeteer/package-lock.json b/puppeteer/package-lock.json new file mode 100644 index 0000000000..32814be03f --- /dev/null +++ b/puppeteer/package-lock.json @@ -0,0 +1,3138 @@ +{ + "name": "puppeteer", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "chai": "^5.1.1", + "mocha": "^10.6.0", + "puppeteer": "^22.13.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.4.tgz", + "integrity": "sha512-BdG2qiI1dn89OTUUsx2GZSpUzW+DRffR1wlMJyKxVHYrhnKoELSDxDd+2XImUkuWPEKk76H5FcM/gPFrEK1Tfw==", + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.2", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chromium-bidi": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.1.tgz", + "integrity": "sha512-kSxJRj0VgtUKz6nmzc2JPfyfJGzwzt65u7PqhPHtgGQUZLF5oG+ST6l6e5ONfStUMAlhSutFCjaGKllXZa16jA==", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1299070", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz", + "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==" + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "node_modules/mocha": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.6.0.tgz", + "integrity": "sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw==", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.1.tgz", + "integrity": "sha512-PwXLDQK5u83Fm5A7TGMq+9BR7iHDJ8a3h21PSsh/E6VfhxiKYkU7+tvGZNSCap6k3pCNDd9oNteVBEctcBalmQ==", + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "2.2.4", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1299070", + "puppeteer-core": "22.13.1" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.1.tgz", + "integrity": "sha512-NmhnASYp51QPRCAf9n0OPxuPMmzkKd8+2sB9Q+BjwwCG25gz6iuNc3LQDWa+cH2tyivmJppLhNNFt6Q3HmoOpw==", + "dependencies": { + "@puppeteer/browsers": "2.2.4", + "chromium-bidi": "0.6.1", + "debug": "^4.3.5", + "devtools-protocol": "0.0.1299070", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@puppeteer/browsers": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.4.tgz", + "integrity": "sha512-BdG2qiI1dn89OTUUsx2GZSpUzW+DRffR1wlMJyKxVHYrhnKoELSDxDd+2XImUkuWPEKk76H5FcM/gPFrEK1Tfw==", + "requires": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.2", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } + }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "optional": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "requires": { + "debug": "^4.3.4" + } + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==" + }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "requires": { + "tslib": "^2.0.1" + } + }, + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "optional": true + }, + "bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "optional": true, + "requires": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "bare-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "optional": true + }, + "bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "optional": true, + "requires": { + "bare-os": "^2.1.0" + } + }, + "bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "optional": true, + "requires": { + "streamx": "^2.18.0" + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==" + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "requires": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==" + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chromium-bidi": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.1.tgz", + "integrity": "sha512-kSxJRj0VgtUKz6nmzc2JPfyfJGzwzt65u7PqhPHtgGQUZLF5oG+ST6l6e5ONfStUMAlhSutFCjaGKllXZa16jA==", + "requires": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "requires": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + } + }, + "data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==" + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" + }, + "deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==" + }, + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + } + }, + "devtools-protocol": { + "version": "0.0.1299070", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz", + "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==" + }, + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "requires": { + "pend": "~1.2.0" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "requires": { + "get-func-name": "^2.0.1" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "mocha": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.6.0.tgz", + "integrity": "sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw==", + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + } + }, + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==" + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "requires": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "puppeteer": { + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.1.tgz", + "integrity": "sha512-PwXLDQK5u83Fm5A7TGMq+9BR7iHDJ8a3h21PSsh/E6VfhxiKYkU7+tvGZNSCap6k3pCNDd9oNteVBEctcBalmQ==", + "requires": { + "@puppeteer/browsers": "2.2.4", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1299070", + "puppeteer-core": "22.13.1" + } + }, + "puppeteer-core": { + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.1.tgz", + "integrity": "sha512-NmhnASYp51QPRCAf9n0OPxuPMmzkKd8+2sB9Q+BjwwCG25gz6iuNc3LQDWa+cH2tyivmJppLhNNFt6Q3HmoOpw==", + "requires": { + "@puppeteer/browsers": "2.2.4", + "chromium-bidi": "0.6.1", + "debug": "^4.3.5", + "devtools-protocol": "0.0.1299070", + "ws": "^8.18.0" + } + }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + }, + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "requires": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "requires": { + "b4a": "^1.6.4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "optional": true + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, + "urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, + "workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==" + } + } +} diff --git a/puppeteer/package.json b/puppeteer/package.json new file mode 100644 index 0000000000..26ef5b268d --- /dev/null +++ b/puppeteer/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "dependencies": { + "chai": "^5.1.1", + "mocha": "^10.6.0", + "puppeteer": "^22.13.0" + } +} diff --git a/puppeteer/package/_service b/puppeteer/package/_service new file mode 100644 index 0000000000..440c0e63d5 --- /dev/null +++ b/puppeteer/package/_service @@ -0,0 +1,26 @@ + + + @PARENT_TAG@+@TAG_OFFSET@ + v(.*) + https://github.com/openSUSE/agama.git + git + master + puppeteer + enable + package-lock.json + package/agama-integration-tests.changes + package/agama-integration-tests.spec + + + node_modules.obscpio + node_modules.spec.inc + 1000 + + + agama.obsinfo + agama + + + agama + + diff --git a/puppeteer/package/agama-integration-tests.changes b/puppeteer/package/agama-integration-tests.changes new file mode 100644 index 0000000000..def90b35e9 --- /dev/null +++ b/puppeteer/package/agama-integration-tests.changes @@ -0,0 +1,5 @@ +------------------------------------------------------------------- +Tue Jul 16 13:25:52 UTC 2024 - Ladislav Slezák + +- Initial version + diff --git a/puppeteer/package/agama-integration-tests.spec b/puppeteer/package/agama-integration-tests.spec new file mode 100644 index 0000000000..7eb55e26ad --- /dev/null +++ b/puppeteer/package/agama-integration-tests.spec @@ -0,0 +1,68 @@ +# +# spec file for package agama-integration-tests +# +# Copyright (c) 2024 SUSE LLC +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via https://bugs.opensuse.org/ +# + +Name: agama-integration-tests +Version: 0 +Release: 0 +Summary: Support for running Agama integration tests +License: GPL-2.0-only +URL: https://github.com/openSUSE/agama +# source_validator insists that if obscpio has no version then +# tarball must neither +Source0: agama.tar +Source10: package-lock.json +Source11: node_modules.spec.inc +Source12: node_modules.sums +%include %_sourcedir/node_modules.spec.inc +BuildArch: noarch +BuildRequires: fdupes +BuildRequires: local-npm-registry +Requires: nodejs(engine) >= 18 + +%description +This package provides infrastructure and tooling needed to run the Agama +integration tests. It includes the Puppeteer framework with all dependencies. + +The package includes only one example test, the real tests should be added from +outside. + +%prep +%autosetup -p1 -n agama + +%build +rm -f package-lock.json +PUPPETEER_SKIP_DOWNLOAD=true local-npm-registry %{_sourcedir} install --omit=optional --with=dev --legacy-peer-deps || ( find ~/.npm/_logs -name '*-debug.log' -print0 | xargs -0 cat; false) + +%install +install -D -d -m 0755 %{buildroot}%{_datadir}/agama/integration-tests +cp -aR node_modules %{buildroot}%{_datadir}/agama/integration-tests +cp -aR %{_builddir}/agama/tests %{buildroot}%{_datadir}/agama/integration-tests +cp -a %{_builddir}/agama/package.json %{buildroot}%{_datadir}/agama/integration-tests +install -D -d -m 0755 %{buildroot}%{_bindir} +cp -a %{_builddir}/agama/agama-integration-tests %{buildroot}%{_bindir} + +# symlink duplicate files +%fdupes -s %{buildroot}/%{_datadir}/agama/integration-tests + +%files +%defattr(-,root,root,-) +%doc README.md +%dir %{_datadir}/agama +%{_datadir}/agama/integration-tests +%attr(0755,root,root) %{_bindir}/agama-integration-tests + +%changelog diff --git a/puppeteer/tests/test_root_password.js b/puppeteer/tests/test_root_password.js new file mode 100644 index 0000000000..7bf938984b --- /dev/null +++ b/puppeteer/tests/test_root_password.js @@ -0,0 +1,169 @@ +import fs from "fs"; +import path from "path"; + +import puppeteer from "puppeteer"; +import { expect } from "chai"; + +// helper function for converting String to Boolean +function booleanEnv(name, default_value) { + const env = process.env[name]; + if (env === undefined) { + return default_value; + } + switch (env.toLowerCase()) { + case "0": + case "false": + case "off": + case "disabled": + case "no": + return false; + case "1": + case "true": + case "on": + case "enabled": + case "yes": + return true; + default: + return default_value; + } +} + +// helper function for configuring the browser +function browserSettings(name) { + switch (name.toLowerCase()) { + case "firefox": + return { + product: "firefox", + executablePath: "/usr/bin/firefox", + }; + case "chrome": + return { + product: "chrome", + executablePath: "/usr/bin/google-chrome-stable", + }; + case "chromium": + return { + product: "chrome", + executablePath: "/usr/bin/chromium", + }; + default: + throw new Error(`Unsupported browser type: ${name}`); + } +} + +const agamaServer = process.env.AGAMA_SERVER || "http://localhost"; +const agamaPassword = process.env.AGAMA_PASSWORD || "linux"; +const agamaBrowser = process.env.AGAMA_BROWSER || "firefox"; +const slowMo = parseInt(process.env.AGAMA_SLOWMO || "0"); +const headless = booleanEnv("AGAMA_HEADLESS", true); + +describe("Agama test", function () { + // mocha timeout + this.timeout(20000); + + let page; + let browser; + + before(async function () { + browser = await puppeteer.launch({ + // "webDriverBiDi" does not work with old FireFox, comment it out if needed + protocol: "webDriverBiDi", + headless, + ignoreHTTPSErrors: true, + timeout: 30000, + slowMo, + defaultViewport: { + width: 1280, + height: 768 + }, + ...browserSettings(agamaBrowser) + }); + page = await browser.newPage(); + page.setDefaultTimeout(20000); + await page.goto(agamaServer, { timeout: 60000, waitUntil: "domcontentloaded" }); + }); + + after(async function () { + await page.close(); + await browser.close(); + }) + + // automatically take a screenshot and dump the page content for failed tests + afterEach(async function () { + if (this.currentTest.state === "failed") { + // directory for storing the data + const dir = "log"; + if (!fs.existsSync(dir)) fs.mkdirSync(dir); + + // base file name for the dumps + const name = path.join(dir, this.currentTest.title.replace(/[^a-zA-Z0-9]/g, "_")); + await page.screenshot({ path: name + ".png" }); + const html = await page.content(); + fs.writeFileSync(name + ".html", html); + } + }); + + it("should have Agama page title", async function () { + expect(await page.title()).to.eql("Agama"); + }); + + it("allows logging in", async function () { + // await page.waitForSelector("input#password"); + await page.type("input#password", agamaPassword); + await page.click("button[type='submit']"); + }); + + it("should optionally display the product selection dialog", async function () { + this.timeout(60000); + // Either the main page is displayed (with the storage link) or there is + // the product selection page. + let productSelectionDisplayed = await Promise.any([ + page.waitForSelector("a[href='#/storage']") + .then(s => {s.dispose(); return false}), + page.waitForSelector("button[form='productSelectionForm']") + .then(s => {s.dispose(); return true}) + ]); + + if (productSelectionDisplayed) { + await page.locator("::-p-text('openSUSE Tumbleweed')").click(); + await page.locator("button[form='productSelectionForm']") + // wait until the button is enabled + .setWaitForEnabled(true) + .click(); + // refreshing the repositories might take long time + await page.locator("h3::-p-text('Overview')").setTimeout(60000).wait(); + } else { + // no product selection displayed, mark the test as skipped + this.skip(); + } + }); + + it("should display overview card", async function () { + await page.waitForSelector("h3::-p-text('Overview')"); + }); + + it("should allow setting the root password", async function () { + await page.locator("a[href='#/users']").click(); + + let button = await Promise.any([ + page.waitForSelector("button::-p-text(Set a password)"), + page.waitForSelector("button#actions-for-root-password") + ]); + + await button.click(); + const id = await button.evaluate(x => x.id); + // drop the handler to avoid memory leaks + button.dispose(); + + // if the menu button was clicked we need to additionally press the "Change" menu item + if (id === "actions-for-root-password") { + await page.locator("button[role='menuitem']::-p-text('Change')").click(); + } + + const newPassword = "test"; + await page.type("input#password", newPassword); + await page.type("input#passwordConfirmation", newPassword); + + await page.locator("button::-p-text(Confirm)").click(); + }); +}); From e6ea3e178b59c7c3b609a13b4916dd3f8c431511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 18 Jul 2024 15:29:35 +0200 Subject: [PATCH 237/430] Autosubmit to OBS also the agama-integration-tests package --- .../obs-staging-integration-tests.yml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/obs-staging-integration-tests.yml diff --git a/.github/workflows/obs-staging-integration-tests.yml b/.github/workflows/obs-staging-integration-tests.yml new file mode 100644 index 0000000000..534f946710 --- /dev/null +++ b/.github/workflows/obs-staging-integration-tests.yml @@ -0,0 +1,22 @@ +name: Submit agama-integration-tests + +on: + # runs on pushes targeting the default branch + push: + branches: + - master + paths: + # run only when a source file is changed + - puppeteer/** + + # allow running manually + workflow_dispatch: + +jobs: + update_staging: + uses: ./.github/workflows/obs-staging-shared.yml + # pass all secrets + secrets: inherit + with: + package_name: agama-integration-tests + service_file: puppeteer/package/_service From 274fd5ab58403df9fb9fa08a69abeda21f8eef01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 18 Jul 2024 14:38:22 +0100 Subject: [PATCH 238/430] refactor(web): make useProduct more granular * Use a separate "software/product" query. --- web/src/queries/software.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/web/src/queries/software.js b/web/src/queries/software.js index a902d10ad7..8aa17911b1 100644 --- a/web/src/queries/software.js +++ b/web/src/queries/software.js @@ -33,6 +33,15 @@ const configQuery = () => ({ queryFn: () => fetch("/api/software/config").then((res) => res.json()), }); +const selectedProductQuery = () => ({ + queryKey: ["software/product"], + queryFn: async () => { + const response = await fetch("/api/software/config"); + const { product } = await response.json(); + return product; + }, +}); + const productsQuery = () => ({ queryKey: ["software/products"], queryFn: () => fetch("/api/software/products").then((res) => res.json()), @@ -60,6 +69,7 @@ const useConfigMutation = () => { }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["software/config"] }); + queryClient.invalidateQueries({ queryKey: ["software/product"] }); client.manager.startProbing(); }, }; @@ -88,15 +98,22 @@ const useProductChanges = () => { }; const useProduct = () => { - const [{ data: config }, { data: products }] = useSuspenseQueries({ - queries: [configQuery(), productsQuery()], + const [{ data: selected }, { data: products }] = useSuspenseQueries({ + queries: [selectedProductQuery(), productsQuery()], }); - const selectedProduct = products.find((p) => p.id === config.product); + const selectedProduct = products.find((p) => p.id === selected); return { products, selectedProduct, }; }; -export { configQuery, productsQuery, useConfigMutation, useProduct, useProductChanges }; +export { + configQuery, + selectedProductQuery, + productsQuery, + useConfigMutation, + useProduct, + useProductChanges, +}; From 45c7795c079e8d5491eccab78c5de660774c0ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 18 Jul 2024 15:42:02 +0200 Subject: [PATCH 239/430] Fix up for the package autosubmission --- .github/workflows/obs-staging-integration-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/obs-staging-integration-tests.yml b/.github/workflows/obs-staging-integration-tests.yml index 534f946710..85cdcf8806 100644 --- a/.github/workflows/obs-staging-integration-tests.yml +++ b/.github/workflows/obs-staging-integration-tests.yml @@ -18,5 +18,6 @@ jobs: # pass all secrets secrets: inherit with: + install_packages: obs-service-node_modules package_name: agama-integration-tests service_file: puppeteer/package/_service From 29a70d023959e0d7203aabcaecb53a61a40035aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 18 Jul 2024 15:54:55 +0200 Subject: [PATCH 240/430] Live ISO: add Puppeteer, drop the Playwright flavor --- live/src/_multibuild | 1 - live/src/agama-installer-openSUSE.kiwi | 15 +++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/live/src/_multibuild b/live/src/_multibuild index 3beec63585..bf665e3100 100644 --- a/live/src/_multibuild +++ b/live/src/_multibuild @@ -1,5 +1,4 @@ openSUSE - openSUSE-Playwright openSUSE-PXE diff --git a/live/src/agama-installer-openSUSE.kiwi b/live/src/agama-installer-openSUSE.kiwi index 84a2fb2993..d5908bfb60 100644 --- a/live/src/agama-installer-openSUSE.kiwi +++ b/live/src/agama-installer-openSUSE.kiwi @@ -10,7 +10,6 @@ - @@ -29,7 +28,7 @@ - + @@ -147,6 +146,7 @@ + @@ -160,19 +160,10 @@ - + - - - - - - - - - From 3a02b6b3aa9aec384c5be66333b089812a6fe4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 18 Jul 2024 16:09:18 +0200 Subject: [PATCH 241/430] Live ISO autosubmission needs obs-service-format_spec_file package --- .github/workflows/obs-staging-live.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/obs-staging-live.yml b/.github/workflows/obs-staging-live.yml index eae370de27..b9ae184f67 100644 --- a/.github/workflows/obs-staging-live.yml +++ b/.github/workflows/obs-staging-live.yml @@ -29,7 +29,7 @@ jobs: - name: Install tools run: zypper --non-interactive install --no-recommends - findutils git make osc + findutils git make osc obs-service-format_spec_file - name: Git Checkout uses: actions/checkout@v4 From 747ee38749cc2167ec2f4ebd9b8141c73c7bfc82 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 16:24:36 +0200 Subject: [PATCH 242/430] fix returning correct question and add tracing to see which kind of question is created --- rust/agama-server/src/questions/web.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 3bae35d0e6..d1c1077eef 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -61,6 +61,7 @@ impl<'a> QuestionsClient<'a> { .map(|(k, v)| (k.as_str(), v.as_str())) .collect(); let path = if question.with_password.is_some() { + tracing::info!("creating question with password"); self.questions_proxy .new_with_password( &generic.class, @@ -71,6 +72,7 @@ impl<'a> QuestionsClient<'a> { ) .await? } else { + tracing::info!("creating generic question"); self.questions_proxy .new_question( &generic.class, @@ -89,7 +91,8 @@ impl<'a> QuestionsClient<'a> { return Err(ServiceError::UnsuccessfulAction(msg)); }; // TODO: better error if path does not contain id res.generic.id = id_cap["id"].parse::().unwrap(); - Ok(question) + tracing::info!("new question gets id {}", res.generic.id); + Ok(res) } pub async fn questions(&self) -> Result, ServiceError> { From 4a7ece494b5c98ad023d14cc6f959c93125ce9c1 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 16:29:52 +0200 Subject: [PATCH 243/430] clarify format of questions and answers --- rust/agama-cli/src/questions.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index 51513b2a76..7dfe1ae709 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -3,6 +3,7 @@ use agama_lib::questions::http_client::HTTPClient; use agama_lib::{connection, error::ServiceError}; use clap::{Args, Subcommand, ValueEnum}; +// TODO: use for answers also JSON to be consistent #[derive(Subcommand, Debug)] pub enum QuestionsCommands { /// Set the mode for answering questions. @@ -19,9 +20,9 @@ pub enum QuestionsCommands { /// Path to a file containing the answers in YAML format. path: String, }, - /// prints list of questions that is waiting for answer in YAML format + /// prints list of questions that is waiting for answer in JSON format List, - /// Ask question from stdin in YAML format and print answer when it is answered. + /// Ask question from stdin in JSON format and print answer when it is answered. Ask, } From c5494e2a8940c5c165b2972cc2ac67f2f804d3ed Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 14:54:19 +0200 Subject: [PATCH 244/430] fix superclass --- service/lib/agama/autoyast/report_patching.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb index dbfd7fef28..965e8bb09f 100644 --- a/service/lib/agama/autoyast/report_patching.rb +++ b/service/lib/agama/autoyast/report_patching.rb @@ -26,7 +26,9 @@ # TODO: what to do if it runs without agama? Just print question to stderr? require "yaml" +require "yast" require "yast2/execute" +require "ui/dialog" # :nodoc: # rubocop:disable Metrics/ParameterLists @@ -85,7 +87,7 @@ def generate_options(buttons) # TODO: encrypt agama profile? is it needed? module UI # :nodoc: - class PasswordDialog + class PasswordDialog < Dialog def new(label, confirm: false) @label = label # NOTE: implement confirm if needed From f1dc311dba6c14e1e39cd2bb7cec66dcfba2ce92 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Thu, 18 Jul 2024 16:28:33 +0200 Subject: [PATCH 245/430] fixes from testing --- service/lib/agama/autoyast/report_patching.rb | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb index 965e8bb09f..a102ac1cba 100644 --- a/service/lib/agama/autoyast/report_patching.rb +++ b/service/lib/agama/autoyast/report_patching.rb @@ -25,7 +25,7 @@ # independent. # TODO: what to do if it runs without agama? Just print question to stderr? -require "yaml" +require "json" require "yast" require "yast2/execute" require "ui/dialog" @@ -56,10 +56,10 @@ def show(message, details: "", headline: "", timeout: 0, focus: nil, default_option: focus || options.first, data: {} } - data = { generic: question }.to_yaml - answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", + data = { generic: question }.to_json + answer_json = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) - answer = YAML.safe_load(answer_yaml) + answer = JSON.parse!(answer_json) answer["generic"]["answer"].to_sym end @@ -99,17 +99,17 @@ def run question = { # TODO: id for newly created question is ignored, but maybe it will # be better to not have to specify it at all? - id: 0, - class: "autoyast.password", - text: text, - options: ["ok", "cancel"], - default_option: "cancel", - data: {} + "id" => 0, + "class" => "autoyast.password", + "text" => text, + "options" => ["ok", "cancel"], + "defaultOption" => "cancel", + "data" => {} } - data = { generic: question, with_password: {} }.to_yaml - answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, + data = { "generic" => question, "withPassword" => {} }.to_json + answer_json = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) - answer = YAML.safe_load(answer_yaml) + answer = JSON.parse!(answer_json) result = answer["generic"]["answer"].to_sym return nil if result == "cancel" From 397be240e189ed759ca355a4cfbc5e5c3da7362a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 18 Jul 2024 17:31:14 +0200 Subject: [PATCH 246/430] Drop Playwright support --- .github/workflows/obs-release.yml | 2 +- .github/workflows/obs-staging-playwright.yml | 22 -- README.md | 3 - Rakefile | 9 - doc/yupdate.md | 4 - live/src/agama-installer-openSUSE.changes | 6 + playwright/.gitignore | 6 - playwright/LICENSE | 339 ------------------- playwright/README.md | 233 ------------- playwright/config/agama.yaml | 112 ------ playwright/global-setup.ts | 76 ----- playwright/lib/installer.ts | 15 - playwright/package/_service | 20 -- playwright/package/agama-playwright.changes | 5 - playwright/package/agama-playwright.spec | 52 --- playwright/playwright.config.ts | 77 ----- playwright/tests/main_page.spec.ts | 17 - playwright/tests/root_password.spec.ts | 35 -- playwright/tests/take_screenshots.spec.ts | 121 ------- 19 files changed, 7 insertions(+), 1147 deletions(-) delete mode 100644 .github/workflows/obs-staging-playwright.yml delete mode 100644 playwright/.gitignore delete mode 100644 playwright/LICENSE delete mode 100644 playwright/README.md delete mode 100644 playwright/config/agama.yaml delete mode 100644 playwright/global-setup.ts delete mode 100644 playwright/lib/installer.ts delete mode 100644 playwright/package/_service delete mode 100644 playwright/package/agama-playwright.changes delete mode 100644 playwright/package/agama-playwright.spec delete mode 100644 playwright/playwright.config.ts delete mode 100644 playwright/tests/main_page.spec.ts delete mode 100644 playwright/tests/root_password.spec.ts delete mode 100644 playwright/tests/take_screenshots.spec.ts diff --git a/.github/workflows/obs-release.yml b/.github/workflows/obs-release.yml index 77d1f0049a..2f24512a7d 100644 --- a/.github/workflows/obs-release.yml +++ b/.github/workflows/obs-release.yml @@ -11,7 +11,7 @@ on: - v[0-9]* jobs: - # Note: cockpit-agama-playwright is currently not submitted + # Note: agama-integration-tests and the Live ISO are currently not submitted update_rust: uses: ./.github/workflows/obs-staging-shared.yml diff --git a/.github/workflows/obs-staging-playwright.yml b/.github/workflows/obs-staging-playwright.yml deleted file mode 100644 index dc4a22990f..0000000000 --- a/.github/workflows/obs-staging-playwright.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Submit agama-playwright - -on: - # runs on pushes targeting the default branch - push: - branches: - - master - paths: - # run only when a Playwright source is changed - - playwright/** - - # allow running manually - workflow_dispatch: - -jobs: - update_staging: - uses: ./.github/workflows/obs-staging-shared.yml - # pass all secrets - secrets: inherit - with: - package_name: agama-playwright - service_file: playwright/package/_service diff --git a/README.md b/README.md index 897281f845..5220d6d9ff 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,6 @@ interfaces over it. | ------------------------------------------------------ | --------------------------------------------------------------- | | ![Installing](./doc/images/screenshots/installing.png) | ![Installation finished](./doc/images/screenshots/finished.png) | -_Note for developers: For updating the screenshots see the -[integration test documentation](playwright/README.md#updating-the-screenshots)._ - ## Why a New Installer diff --git a/Rakefile b/Rakefile index e11a7524e8..ec78d0e974 100644 --- a/Rakefile +++ b/Rakefile @@ -177,15 +177,6 @@ if ENV["YUPDATE_FORCE"] == "1" || File.exist?("/.packages.initrd") || live_iso? end end - # update also the tests if they are present in the system - if ENV["YUPDATE_SKIP_TESTS"] != "1" && File.exist?("/usr/share/agama-playwright") - puts "Installing the integration tests..." - - # we are installing into an empty chroot, make sure the target exists - FileUtils.mkdir_p(File.join(destdir, "/usr/share")) - FileUtils.cp_r("playwright/.", File.join(destdir, "/usr/share/agama-playwright")) - end - if ENV["YUPDATE_SKIP_PRODUCTS"] != "1" files = Dir.glob("products.d/*.y{a}ml") files.each do |f| diff --git a/doc/yupdate.md b/doc/yupdate.md index 0792f2a710..aec1863ac7 100644 --- a/doc/yupdate.md +++ b/doc/yupdate.md @@ -59,10 +59,6 @@ You can modify the update process with these environment variables: - `YUPDATE_SKIP_BACKEND=1` - Skip updating the D-Bus service backend. This is similar to the previous option, use it when you do want to keep the D-Bus service unchanged. -- `YUPDATE_SKIP_TESTS=1` - Skip updating the integration tests if they are - installed. If the tests are not installed they are not added automatically. - If you want to add them first create the `/usr/share/agama-playwright` - directory where the tests will be added and then run the `yupdate` command. ## Notes diff --git a/live/src/agama-installer-openSUSE.changes b/live/src/agama-installer-openSUSE.changes index bdf33516df..dc8f179dbc 100644 --- a/live/src/agama-installer-openSUSE.changes +++ b/live/src/agama-installer-openSUSE.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jul 18 15:25:39 UTC 2024 - Ladislav Slezák + +- Include Puppeteer in all ISO images (gh#openSUSE/agama#1477) +- Drop Playwright ISO flavor (gh#openSUSE/agama#1481) + ------------------------------------------------------------------- Tue Jul 9 13:26:38 UTC 2024 - Knut Anderssen diff --git a/playwright/.gitignore b/playwright/.gitignore deleted file mode 100644 index 27c6b32018..0000000000 --- a/playwright/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -/test-results/ -/playwright-report/ -/playwright/.cache/ -storageState.json -/screenshots/ diff --git a/playwright/LICENSE b/playwright/LICENSE deleted file mode 100644 index d159169d10..0000000000 --- a/playwright/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/playwright/README.md b/playwright/README.md deleted file mode 100644 index 9b5d815f80..0000000000 --- a/playwright/README.md +++ /dev/null @@ -1,233 +0,0 @@ -## Integration Tests - -This directory contains integration tests which use the [Playwright]( -https://playwright.dev/) testing framework. - -## Installation - -To install the Playwright tool run this command in the `playwright` subdirectory: - -```shell -npm install --no-save @playwright/test -``` - -This will install the NPM packages into the `node_modules` subdirectory. - -Alternatively you can install it as an RPM package from the -[systemsmanagement:Agama:Devel](https://build.opensuse.org/project/show/systemsmanagement:Agama:Devel) -OBS project. - -## Files - -- `playwright.config.ts` - Playwright configuration, see [documentation]( - https://playwright.dev/docs/test-configuration) for more details -- `global-setup.ts` - a helper for logging-in -- `tests/\*.spec.ts` - individual test files -- `lib/*` - shared library files - -## Running the Tests - -*Note: If you install Playwright from the RPM package then omit the `npx` -tool from all commands below.* - -By default the tests are used against the local running server, if you -want to test a remote server see the [Target Server](#target-server) -section below. - -To run all tests use this command: - -``` -npx playwright test -``` - -To run just a specific test: - -``` -npx playwright test tests/root_password.spec.ts -``` - -By default it runs tests in all configured projects (browsers), -if you want to use only a specific browser use the `--project` option: - -``` -npx playwright test --project chromium -- tests/root_password.spec.ts -``` - -See the `playwright.config.ts` file for the list of configured projects. - -### Running Tests Directly from the Live ISO - -You can download the `openSUSE-Playwright` image type from the [systemsmanagement:Agama:Devel]( -https://download.opensuse.org/repositories/systemsmanagement:/Agama:/Devel/images/iso/) repository. - -This ISO additionally includes the Playwright tool, Chromium browser and the -Agama integration tests. - -To start a test in a console or in a SSH session use this command: - -``` -playwright test --project chromium --config /usr/share/agama-playwright -``` - -*Note the missing `npx` tool in the command, in this case Playwright is -installed into the system directories.* - -You can also start the tests in headed mode (with `--headed` option) either -via `ssh -X` or using a local X terminal session: - -```shell -# from a Linux console -DISPLAY=:0 xterm & -# then switch to console 7 using the Alt+F7 keyboard shortcut -# and run Playwright from the xterm -``` - -## Updating the Screenshots - -There is one test specially designed for refreshing the screenshots displayed -in the main `README.md` file. - -To fully run the installation type this: - -``` -SCREENSHOT_MODE=1 RUN_INSTALLATION=1 BASE_URL=https://:9090 npx playwright test --headed --project chromium take_screenshots -``` - -The `--headed` option shows the browser window so you can see the progress. -You can use the `--debug` option to run the test step-by-step. - -The screenshots are saved to the `screenshots/` subdirectory. - -## Target Server - -By default the tests use the installer instance running locally at -`http://localhost:9090`. If you want to run the tests against -another instance set the `BASE_URL` environment variable: - -``` -BASE_URL=https://192.168.1.12:9090 npx playwright ... -``` - -You can use it also with the [webpack development server]( -../web/README.md#using-a-development-server): - -``` -BASE_URL=https://localhost:8080/ npx playwright ... -``` - -### Options - -The tests by default run in a headless mode, if you want to see the actions -in the browser use the `--headed` option. - -If you want to manually run a test step by step use the `--debug` option. This -also allows to easily get the object selectors using the `Explore` button. - -## Links - -- https://playwright.dev/docs/intro - Playwright Documentation -- https://playwright.dev/docs/test-assertions - Test assetions (`expect`) -- https://playwright.dev/docs/api/class-locator - Finding the elements on the page - -## Tips for Writing Tests - -The installer runs in another process in a browser, it does not run -synchronously with the test. Additionally the installer uses the React -framework, that means the initial web page is empty and the content is added -asynchronously by Javascript code. - -These features have some consequences for writing the tests. - -### Timeouts - -As mentioned above, the page content is updated asynchronously so if something -is missing on the page it does not mean the test fails immediately. -The tested object might appear on the page after a small delay, -Playwright uses timeouts for most of the checks. If something is missing -then it tries the search again until the timeout is reached. - -The default timeout is set in the `playwright.config.ts` configuration file. -That should be enough for most operations even on a slow machine. -However, for some long running operations like refreshing repositories or -installing packages you might need to use a longer timeout. - -Playwright allows setting explicit timeout for each test or action: - -```js -// refreshing the repositories and evaluating the package dependencies might take long time -await expect(page.getByText("Installation will take")).toBeVisible({timeout: 2 * minute}); -``` - -or you can set how long the whole test should run: - -```js -test.setTimeout(60 * minute); -``` - -### Testing Not Displayed Elements - -This is also related to the asynchronous work. You should never test that something -is NOT displayed on the page because it is not guaranteed that it will not -be displayed one millisecond after you check for it. - -The only exception is that you first check that an element is displayed, do some -action and then check that the element is not displayed anymore. - -```js -// clicking a 'Details' button -await page.getByText('Details').click(); -// opens a modal dialog (popup) -await expect(page.locator('[role="dialog"]')).toBeVisible(); - -// after clicking the 'Close' button -await page.getByText('Close').click(); -// the popup disappears -await expect(page.locator('[role="dialog"]')).not.toBeVisible(); -``` - -The last check actually waits until the popup disappears. It is OK if it takes -some short time to close the popup. - -### Locators - -By default the text locators search for a *substring*! If there are similar -labels present you might get errors for multiple elements found. - -For example when there are "Password" and "Password Confirmation" fields -displayed on the page then simple - -```js -await page.getByLabel('Password').click() -``` - -would actually match *both* elements and Playwright would not know which one you -wanted to click. In that case the test would fail with an error. - -The solution is to use the exact matching: - -```js -await page.getByLabel('Password', { exact: true }).click() -``` - -This will match only one field without any conflict. - -## Troubleshooting Failed Integration Tests in CI - -### Single Test Failure - -There are stored artifacts in the GitHub CI. Go to the failed job and there is -a link "Summary". At the bottom of the page there is "Artifacts" section which -contains the `y2log` and also `trace.zip` file. The trace can be browsed using -the playwright tool locally or at page https://trace.playwright.dev/ to get -details of the failure. - -### Stuck at D-Bus Loading - -It usually indicates an issue with the Agama D-Bus services. There is a step -called "Show D-Bus Services Logs" which should give a hint what is going wrong. -Additional help can be the `y2log` file in the artifacts (see above). - -### Missing Package/Wrong Container - -Packages lives in container at https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/agama-testing. -Feel free to modify it as the only purpose of this container is CI testing. diff --git a/playwright/config/agama.yaml b/playwright/config/agama.yaml deleted file mode 100644 index cff3888d3f..0000000000 --- a/playwright/config/agama.yaml +++ /dev/null @@ -1,112 +0,0 @@ ---- -products: - Tumbleweed: - name: openSUSE Tumbleweed - description: The Tumbleweed distribution is a pure rolling release version of - openSUSE containing the latest "stable" versions of all software instead of - relying on rigid periodic release cycles. The project does this for users that - want the newest stable software. -web: - ssl: - ssl_cert: - ssl_key: -Tumbleweed: - software: - installation_repositories: - - url: https://download.opensuse.org/tumbleweed/repo/oss/ - archs: x86_64 - - url: https://download.opensuse.org/ports/aarch64/tumbleweed/repo/oss/ - archs: aarch64 - - url: https://download.opensuse.org/ports/zsystems/tumbleweed/repo/oss/ - archs: s390 - - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ - archs: ppc - - url: https://download.opensuse.org/tumbleweed/repo/non-oss/ - archs: x86_64 - - url: https://download.opensuse.org/ports/zsystems/tumbleweed/repo/non-oss/ - archs: s390 - - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/non-oss/ - archs: ppc - - url: https://download.opensuse.org/update/tumbleweed/ - archs: x86_64 - - url: https://download.opensuse.org/ports/aarch64/update/tumbleweed/ - archs: aarch64 - - url: https://download.opensuse.org/ports/zsystems/update/tumbleweed/ - archs: s390 - - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ - archs: ppc - mandatory_patterns: - - enhanced_base - optional_patterns: - mandatory_packages: - - NetworkManager - optional_packages: - base_product: openSUSE - security: - lsm: apparmor - available_lsms: - apparmor: - patterns: - - apparmor - selinux: - patterns: - - selinux - policy: permissive - none: - patterns: - storage: - volumes: - - mount_point: "/" - fs_type: btrfs - min_size: 5 GiB - max_size: 15 GiB - weight: 1 - proposed_configurable: false - snapshots: true - snapshots_percentage: 250 - snapshots_configurable: true - disable_order: 3 - btrfs_default_subvolume: "@" - subvolumes: - - path: home - - path: opt - - path: root - - path: srv - - path: usr/local - - path: var - copy_on_write: false - - path: boot/grub2/arm64-efi - archs: aarch64 - - path: boot/grub2/arm-efi - archs: arm - - path: boot/grub2/i386-pc - archs: x86_64 - - path: boot/grub2/powerpc-ieee1275 - archs: ppc,!board_powernv - - path: boot/grub2/s390x-emu - archs: s390 - - path: boot/grub2/x86_64-efi - archs: x86_64 - - path: boot/grub2/riscv64-efi - archs: riscv64 - - mount_point: "/home" - fs_type: xfs - min_size: 10 GiB - max_size: unlimited - max_size_lvm: unlimited - weight: 1 - proposed: false - proposed_configurable: true - disable_order: 1 - fallback_for_max_size: "/" - fallback_for_max_size_lvm: "/" - - mount_point: swap - fs_type: swap - desired_size: 2 GiB - min_size: 1 GiB - max_size: 2 GiB - weight: 1 - adjust_by_ram: false - adjust_by_ram_configurable: true - proposed_configurable: true - disable_order: 2 diff --git a/playwright/global-setup.ts b/playwright/global-setup.ts deleted file mode 100644 index ff16dc3309..0000000000 --- a/playwright/global-setup.ts +++ /dev/null @@ -1,76 +0,0 @@ -// See https://playwright.dev/docs/auth#reuse-signed-in-state -// and https://playwright.dev/docs/test-global-setup-teardown -import { expect, chromium, firefox, FullConfig, BrowserType, FullProject } from '@playwright/test'; -const fs = require('fs'); - -function browserType(typeName:string):BrowserType { - if (typeName === "firefox") { - return firefox; - } else if (typeName === "chromium") { - return chromium; - } else { - throw new Error(`Unsupported browser type "${typeName}"`); - } -} - -// find the project (browser) to use for logging in: -// - specified by the "--project" command line option -// - or find the first installed browser -function findProject(config: FullConfig):FullProject { - let project : FullProject; - - const optionIndex = process.argv.findIndex(a => a === "--project"); - if (optionIndex >= 0) { - const projectName = process.argv[optionIndex + 1]; - project = config.projects.find(p => p.name === projectName); - } - else { - project = config.projects.find(p => fs.existsSync(p.use.launchOptions.executablePath)); - } - - if (project === undefined) { - throw new Error("Web browser not found"); - } - - return project; -} - -async function globalSetup(config: FullConfig) { - // baseURL and storageState are the same for all projects, use the first one - const { baseURL, storageState } = config.projects[0].use; - // storage not configure => login was disabled - if (storageState === undefined) return; - - const project = findProject(config); - const browser = await browserType(project.use.defaultBrowserType).launch(project.use.launchOptions); - const page = await browser.newPage({ - baseURL, - ignoreHTTPSErrors: true - }); - - // go to the terminal app to see if the user needs to log into the system - await page.goto("/cockpit/@localhost/system/terminal.html"); - - // login page displayed? - try { - await page.waitForSelector("#login-user-input", { timeout: 5000 }); - } - catch { - // form not found, login not required - return; - } - - await page.getByLabel('User name').fill('root'); - await page.getByLabel('Password', { exact: true }).fill('linux'); - - await page.getByRole('button', { name: 'Log in' }).click(); - - await expect(page.getByLabel('Password', { exact: true })).not.toBeVisible(); - - // Save the signed-in state to a file - await page.context().storageState({ path: storageState as string }); - - await browser.close(); -} - -export default globalSetup; diff --git a/playwright/lib/installer.ts b/playwright/lib/installer.ts deleted file mode 100644 index 8c547e3677..0000000000 --- a/playwright/lib/installer.ts +++ /dev/null @@ -1,15 +0,0 @@ -// shared functions - -// return the URL path to the installer plugin -function mainPagePath():string { - let baseURL = new URL(process.env.BASE_URL || "http://localhost:9090"); - - // when running at the default cockpit port use the full cockpit path, - // otherwise expect the webpack development server where the installer - // is available at the root path - return (baseURL.port == "9090") ? "/cockpit/@localhost/agama/index.html" : "/"; -} - -export { - mainPagePath -}; diff --git a/playwright/package/_service b/playwright/package/_service deleted file mode 100644 index 183d4985f7..0000000000 --- a/playwright/package/_service +++ /dev/null @@ -1,20 +0,0 @@ - - - @PARENT_TAG@+@TAG_OFFSET@ - v(.*) - https://github.com/openSUSE/agama.git - git - master - playwright - enable - package/agama-playwright.changes - package/agama-playwright.spec - - - agama.obsinfo - agama - - - agama - - diff --git a/playwright/package/agama-playwright.changes b/playwright/package/agama-playwright.changes deleted file mode 100644 index 6f7f7dfd40..0000000000 --- a/playwright/package/agama-playwright.changes +++ /dev/null @@ -1,5 +0,0 @@ -------------------------------------------------------------------- -Thu Mar 30 07:32:54 UTC 2023 - Ladislav Slezák - -- Initial version - diff --git a/playwright/package/agama-playwright.spec b/playwright/package/agama-playwright.spec deleted file mode 100644 index 35d2be7dd7..0000000000 --- a/playwright/package/agama-playwright.spec +++ /dev/null @@ -1,52 +0,0 @@ -# -# spec file for package agama-playwright -# -# Copyright (c) 2023 SUSE LLC -# -# All modifications and additions to the file contributed by third parties -# remain the property of their copyright owners, unless otherwise agreed -# upon. The license for this file, and modifications and additions to the -# file, is the same license as for the pristine package itself (unless the -# license for the pristine package is not an Open Source License, in which -# case the license is the MIT License). An "Open Source License" is a -# license that conforms to the Open Source Definition (Version 1.9) -# published by the Open Source Initiative. - -# Please submit bugfixes or comments via https://bugs.opensuse.org/ -# - - -Name: agama-playwright -Version: 0 -Release: 0 -Summary: Integration tests for the Agama installer -License: GPL-2.0-only -URL: https://github.com/openSUSE/agama -# source_validator insists that if obscpio has no version then -# tarball must neither -Source0: agama.tar -BuildArch: noarch -BuildRequires: coreutils -Requires: playwright - -%description -Playwright integration tests for the Agama installer. - -%prep -%autosetup -p1 -n agama - -%build - -%install -mkdir -p %{buildroot}%{_datadir} -tar -xf %{SOURCE0} -C %{buildroot}%{_datadir} -# rename the target directory -mv %{buildroot}%{_datadir}/agama %{buildroot}%{_datadir}/agama-playwright - -%files -%defattr(-,root,root,-) -%doc README.md -%license LICENSE -%{_datadir}/agama-playwright - -%changelog diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts deleted file mode 100644 index da6f8a5a51..0000000000 --- a/playwright/playwright.config.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; -import { devices } from '@playwright/test'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -const config: PlaywrightTestConfig = { - testDir: './tests', - /* Maximum time one test can run for. */ - timeout: 60 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 60 * 1000 - }, - /* Do not run tests in files in parallel by default */ - fullyParallel: false, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Because of test dependencies or race conditions run all tests in sequence, */ - /* later we could use a serial-parallel approach using the test lists, */ - /* see https://playwright.dev/docs/test-parallel#use-a-test-list-file */ - workers: 1, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.GITHUB_ACTIONS ? 'github' : 'list', - globalSetup: require.resolve('./global-setup'), - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 30000, - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.BASE_URL || 'http://localhost:9090', - - /* load signed-in state from 'storageState.json' */ - storageState: process.env.SKIP_LOGIN ? undefined : 'storageState.json', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - contextOptions: { - ignoreHTTPSErrors: true - }, - launchOptions: { - executablePath: '/usr/bin/chromium', - }, - }, - }, - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - contextOptions: { - ignoreHTTPSErrors: true - }, - launchOptions: { - executablePath: '/usr/bin/firefox', - }, - }, - }, - ], -}; - -export default config; diff --git a/playwright/tests/main_page.spec.ts b/playwright/tests/main_page.spec.ts deleted file mode 100644 index abdf8cbaa1..0000000000 --- a/playwright/tests/main_page.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { mainPagePath } from "../lib/installer"; - -test.describe('The main page', () => { - test.beforeEach(async ({ page }) => { - await page.goto(mainPagePath()); - }); - - test('has the "Agama" title', async ({ page }) => { - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Agama/); - }); - - test('has the Install button', async ({ page }) => { - await expect(page.getByRole('button', { name: 'Install', exact: true })).toBeVisible(); - }); -}) diff --git a/playwright/tests/root_password.spec.ts b/playwright/tests/root_password.spec.ts deleted file mode 100644 index 93ea8a8332..0000000000 --- a/playwright/tests/root_password.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { mainPagePath } from "../lib/installer"; - -test.describe('The user section', () => { - test.beforeEach(async ({ page }) => { - await page.goto(mainPagePath()); - }); - - test('can set the root password', async ({ page }) => { - // See https://playwright.dev/docs/api/class-locator - - // initial expectation - the root password is not configured yet - await expect(page.getByText("No root authentication method defined")).toBeVisible(); - - // click the "Users" header - await page.locator("a[href='#/users']").click(); - - // click on the "Set a password" button - await page.getByRole("button", { name: "Set a password" }).click(); - - // fill a new password - await page.locator('#password').fill('agama'); - await page.locator('#passwordConfirmation').fill('agama'); - await page.locator('button[type="submit"]').click(); - - // wait until the popup is closed - await expect(page.locator('[role="dialog"]')).not.toBeVisible(); - - // go back to the main page - await page.getByRole('button', { name: 'Back', exact: true }).click(); - - // check the summary text - await expect(page.getByText("Root authentication set")).toBeVisible(); - }); -}) diff --git a/playwright/tests/take_screenshots.spec.ts b/playwright/tests/take_screenshots.spec.ts deleted file mode 100644 index d4853e412b..0000000000 --- a/playwright/tests/take_screenshots.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { mainPagePath } from "../lib/installer"; - -// This test was designed for collecting the screenshots for the main -// README.md file. To take screenshots of the installation process run this: -// -// RUN_INSTALLATION=1 BASE_URL=https:// npx playwright test --headed take_screenshots -// -// The "--headed" option shows the browser window so you can see the progress. -// You can use the "--debug" option to run the test step-by-step. -// The screenshots are saved to the "screenshots/" subdirectory. - -// minute in miliseconds -const minute = 60 * 1000; - -test.describe("The Installer", () => { - test.beforeEach(async ({ page }) => { - await page.goto(mainPagePath()); - }); - - test("installs the system", async ({ page }) => { - // maximum time for this test to run - test.setTimeout(60 * minute); - - if (process.env.SCREENSHOT_MODE === "1") { - // set screenshot size to 768x1024 - page.setViewportSize({ width: 768, height: 1024 }); - } - - // optional actions done on the page - const actions = Object.freeze({ - setProduct: Symbol("product"), - setPassword: Symbol("password"), - done: Symbol("done") - }); - - // check for multiple texts in parallel, avoid waiting for timeouts - let action = await Promise.any([ - page.getByText("Product selection").waitFor().then(() => actions.setProduct), - page.getByText("No root authentication method").waitFor().then(() => actions.setPassword), - page.getByText("Root authentication set").waitFor().then(() => actions.done), - ]); - - // optional product selection - if (action === actions.setProduct) { - await test.step("Select the product", async () => { - // select openSUSE Tumbleweed - await page.getByText("openSUSE Tumbleweed").click(); - await page.screenshot({ path: "screenshots/product-selection.png" }); - await page.getByRole("button", { name: "Select" }).click(); - }); - - // update the action for the next step - action = await Promise.any([ - page.getByText("No root authentication method").waitFor().then(() => actions.setPassword), - page.getByText("Root authentication set").waitFor().then(() => actions.done), - ]); - } - - if (action === actions.setPassword) { - // the the root password must be set - await test.step("Set the root password", async () => { - await page.locator("a[href='#/users']").click(); - // Create users page screenshot - await page.screenshot({ path: "screenshots/users-page.png" }); - // click on the "Set a password" button - await page.getByRole("button", { name: "Set a password" }).click(); - await page.locator("#password").fill("linux"); - await page.locator("#passwordConfirmation").fill("linux"); - await page.locator('button[type="submit"]').click(); - await page.getByText("Back").click(); - }); - } - - // ensure the software proposal is ready, use longer timeout, - // refreshing the repositories takes some time - await expect(page.getByText("Installation will take")).toBeVisible({timeout: 2 * minute}); - await page.screenshot({ path: "screenshots/overview.png" }); - - await test.step("Storage configuration", async () => { - // create storage page screenshot - await page.locator("a[href='#/storage']").click(); - await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible(); - // skip this in GitHub CI, there is no suitable disk for installation - if (process.env.GITHUB_ACTIONS !== "true") { - await expect(page.getByText("Installation device")).toBeVisible({timeout: minute / 2}); - } - await page.screenshot({ path: "screenshots/storage-page.png" }); - }); - - // confirm the installation only when explicitly set via the environment - if (process.env.RUN_INSTALLATION === "1") { - await test.step("Run installation", async () => { - // start the installation - await page.getByRole("button", { name: "Install", exact: true }).click(); - await expect(page.getByText("Confirm Installation")).toBeVisible(); - await page.getByRole("button", { name: "Continue" }).click(); - - // wait for the package installation progress - await expect(page.getByText("Installing packages")).toBeVisible({timeout: 5 * minute}); - - // create package installation screenshot every half a minute - let screenshot_index = 0; - while (true) { - await page.screenshot({ path: `screenshots/installation_${screenshot_index++}.png` }); - - try { - await page.getByRole("heading", { name: "Congratulations!" }).waitFor({timeout: minute / 2}); - // the finish screen is displayed - await page.screenshot({ path: "screenshots/finished.png" }); - break; - } - catch (error) { - // do not ignore other errors - if (error.constructor.name !== "TimeoutError") throw(error); - } - } - }); - } - }); -}) From d92deef4ded5eb6f400e9bad54f080ff392cd229 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 19 Jul 2024 10:30:13 +0200 Subject: [PATCH 247/430] fix getting answer --- rust/agama-lib/src/error.rs | 2 ++ rust/agama-server/src/questions/web.rs | 43 +++++++++++++------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index f40278ed7e..b5809c73b7 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -37,6 +37,8 @@ pub enum ServiceError { UnsuccessfulAction(String), #[error("Unknown installation phase: {0}")] UnknownInstallationPhase(u32), + #[error("Question with id {0} does not exist")] + QuestionNotExist(u32), #[error("Backend call failed with status {0} and text '{1}'")] BackendError(u16, String), #[error("You are not logged in. Please use: agama auth login")] diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index d1c1077eef..6fd1788cdf 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -7,6 +7,7 @@ use crate::{error::Error, web::Event}; use agama_lib::{ + dbus::{extract_id_from_path, get_property}, error::ServiceError, proxies::{GenericQuestionProxy, QuestionWithPasswordProxy, Questions1Proxy}, questions::model::{Answer, GenericQuestion, PasswordAnswer, Question, QuestionWithPassword}, @@ -19,7 +20,6 @@ use axum::{ routing::{delete, get}, Json, Router, }; -use regex::Regex; use std::{collections::HashMap, pin::Pin}; use tokio_stream::{Stream, StreamExt}; use zbus::{ @@ -84,13 +84,7 @@ impl<'a> QuestionsClient<'a> { .await? }; let mut res = question.clone(); - // we are sure that regexp is correct, so use unwrap - let id_matcher = Regex::new(r"/(?\d+)$").unwrap(); - let Some(id_cap) = id_matcher.captures(path.as_str()) else { - let msg = format!("Failed to get ID for new question: {}", path.as_str()).to_string(); - return Err(ServiceError::UnsuccessfulAction(msg)); - }; // TODO: better error if path does not contain id - res.generic.id = id_cap["id"].parse::().unwrap(); + res.generic.id = extract_id_from_path(&path)?; tracing::info!("new question gets id {}", res.generic.id); Ok(res) } @@ -167,24 +161,29 @@ impl<'a> QuestionsClient<'a> { ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) .context("Failed to create dbus path")?, ); + let objects = self.objects_proxy.get_managed_objects().await?; + let password_interface = OwnedInterfaceName::from( + InterfaceName::from_static_str("org.opensuse.Agama1.Questions.WithPassword") + .context("Failed to create interface name for question with password")?, + ); let mut result = Answer::default(); - let dbus_password_res = QuestionWithPasswordProxy::builder(&self.connection) - .path(&question_path)? - .cache_properties(zbus::CacheProperties::No) - .build() - .await; - if let Ok(dbus_password) = dbus_password_res { + let question = objects + .get(&question_path) + .ok_or(ServiceError::QuestionNotExist(id))?; + + if let Some(password_iface) = question.get(&password_interface) { result.with_password = Some(PasswordAnswer { - password: dbus_password.password().await?, + password: get_property(password_iface, "password")?, }); } - - let dbus_generic = GenericQuestionProxy::builder(&self.connection) - .path(&question_path)? - .cache_properties(zbus::CacheProperties::No) - .build() - .await?; - let answer = dbus_generic.answer().await?; + let generic_interface = OwnedInterfaceName::from( + InterfaceName::from_static_str("org.opensuse.Agama1.Questions.Generic") + .context("Failed to create interface name for generic question")?, + ); + let generic_iface = question + .get(&generic_interface) + .context("Question does not have generic interface")?; + let answer: String = get_property(generic_iface, "answer")?; if answer.is_empty() { Ok(None) } else { From d3cc748bb309aea6f82e18e125502927d7defd46 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 19 Jul 2024 11:34:34 +0200 Subject: [PATCH 248/430] JSON schema: more language id examples --- rust/agama-lib/share/profile.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index b1607caad8..0f96b94760 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -273,7 +273,7 @@ "language": { "title": "System language ID", "type": "string", - "examples": ["en_US"] + "examples": ["en_US.UTF-8", "en_US"] }, "keyboard": { "title": "Keyboard layout ID", From b1e7ee040951d6dc57c697390191bc74fdaaaebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 11:57:30 +0100 Subject: [PATCH 249/430] feat(live): rename to agama-installer --- .github/workflows/obs-staging-live.yml | 10 +++++----- doc/obs_integration.md | 6 +++--- ...taller-openSUSE.changes => agama-installer.changes} | 0 ...ma-installer-openSUSE.kiwi => agama-installer.kiwi} | 0 4 files changed, 8 insertions(+), 8 deletions(-) rename live/src/{agama-installer-openSUSE.changes => agama-installer.changes} (100%) rename live/src/{agama-installer-openSUSE.kiwi => agama-installer.kiwi} (100%) diff --git a/.github/workflows/obs-staging-live.yml b/.github/workflows/obs-staging-live.yml index b9ae184f67..02b530129b 100644 --- a/.github/workflows/obs-staging-live.yml +++ b/.github/workflows/obs-staging-live.yml @@ -1,4 +1,4 @@ -name: Submit agama-installer-openSUSE +name: Submit agama-installer on: # runs on pushes targeting the default branch @@ -43,8 +43,8 @@ jobs: OBS_USER: ${{ secrets.OBS_USER }} OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }} - - name: Checkout ${{ vars.OBS_PROJECT }} agama-installer-openSUSE - run: osc co -o dist ${{ vars.OBS_PROJECT }} agama-installer-openSUSE + - name: Checkout ${{ vars.OBS_PROJECT }} agama-installer + run: osc co -o dist ${{ vars.OBS_PROJECT }} agama-installer working-directory: ./live - name: Build sources @@ -59,6 +59,6 @@ jobs: run: osc diff && osc status working-directory: ./live/dist - - name: Commit agama-installer-openSUSE to ${{ vars.OBS_PROJECT }} - run: osc commit -m "Updated to Agama $GITHUB_SHA" + - name: Commit agama-installer to ${{ vars.OBS_PROJECT }} + run: osc commit -m "Updated to Agama $GITHUB_SHA" working-directory: ./live/dist diff --git a/doc/obs_integration.md b/doc/obs_integration.md index f4cef2bf34..9b6c347b24 100644 --- a/doc/obs_integration.md +++ b/doc/obs_integration.md @@ -126,11 +126,11 @@ osc detachbranch home:$OBS_USER:branches:systemsmanagement:Agama:Devel agama-web ``` If you want to also build the Live ISO from your modified packaged then you need -to branch (and detach) also the `agama-installer-openSUSE` package: +to branch (and detach) also the `agama-installer` package: ``` shell -osc branch systemsmanagement:Agama:Devel agama-installer-openSUSE -osc detachbranch home:$OBS_USER:branches:systemsmanagement:Agama:Devel agama-installer-openSUSE +osc branch systemsmanagement:Agama:Devel agama-installer +osc detachbranch home:$OBS_USER:branches:systemsmanagement:Agama:Devel agama-installer ``` *Please delete your branched OBS project once you do not need it anymore, it diff --git a/live/src/agama-installer-openSUSE.changes b/live/src/agama-installer.changes similarity index 100% rename from live/src/agama-installer-openSUSE.changes rename to live/src/agama-installer.changes diff --git a/live/src/agama-installer-openSUSE.kiwi b/live/src/agama-installer.kiwi similarity index 100% rename from live/src/agama-installer-openSUSE.kiwi rename to live/src/agama-installer.kiwi From 6627d89116e269165ad43b0190659460b562bc70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 17 Jul 2024 13:19:32 +0100 Subject: [PATCH 250/430] feat(live): add an SLE profile --- live/src/agama-installer.kiwi | 44 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/live/src/agama-installer.kiwi b/live/src/agama-installer.kiwi index d5908bfb60..a7bb15a756 100644 --- a/live/src/agama-installer.kiwi +++ b/live/src/agama-installer.kiwi @@ -2,7 +2,7 @@ - + YaST Team yast2-maintainers@suse.de @@ -10,6 +10,7 @@ + @@ -23,17 +24,17 @@ bgrt openSUSE - + - + - + @@ -93,9 +94,7 @@ - - @@ -133,36 +132,48 @@ - - - - - + + + + - + - - + + + + + + + + + + + + + + + @@ -171,7 +182,12 @@ + + + + + From e69b4bd988c3eaea4f0469b48651ad2c51eabba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 18 Jul 2024 10:40:36 +0100 Subject: [PATCH 251/430] feat(live): use openSUSE-repos-Tumbleweed * Do not include our own repo definition for Tumbleweed. --- live/root/etc/zypp/repos.d/repo-oss.repo | 8 -------- live/src/agama-installer.kiwi | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 live/root/etc/zypp/repos.d/repo-oss.repo diff --git a/live/root/etc/zypp/repos.d/repo-oss.repo b/live/root/etc/zypp/repos.d/repo-oss.repo deleted file mode 100644 index 39546139aa..0000000000 --- a/live/root/etc/zypp/repos.d/repo-oss.repo +++ /dev/null @@ -1,8 +0,0 @@ -[repo-oss] -name=openSUSE-Tumbleweed-Oss -type=rpm-md -enabled=1 -autorefresh=1 -gpgcheck=1 -baseurl=https://download.opensuse.org/tumbleweed/repo/oss - diff --git a/live/src/agama-installer.kiwi b/live/src/agama-installer.kiwi index a7bb15a756..6adc773307 100644 --- a/live/src/agama-installer.kiwi +++ b/live/src/agama-installer.kiwi @@ -161,6 +161,7 @@ + From ff5dc7347e0d704c0941eef30346f2f41034c7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 18 Jul 2024 12:05:06 +0100 Subject: [PATCH 252/430] feat(live): add Agama repo depending on the distro --- live/root/etc/zypp/repos.d/agama-SLES.repo | 8 ++++++++ .../{agama-devel.repo => agama-openSUSE_Tumbleweed.repo} | 6 +++--- live/src/config.sh | 7 +++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 live/root/etc/zypp/repos.d/agama-SLES.repo rename live/root/etc/zypp/repos.d/{agama-devel.repo => agama-openSUSE_Tumbleweed.repo} (77%) diff --git a/live/root/etc/zypp/repos.d/agama-SLES.repo b/live/root/etc/zypp/repos.d/agama-SLES.repo new file mode 100644 index 0000000000..2c31f7efe1 --- /dev/null +++ b/live/root/etc/zypp/repos.d/agama-SLES.repo @@ -0,0 +1,8 @@ +[agama-SLES] +name=Agama (SLES) +type=rpm-md +enabled=0 +autorefresh=1 +gpgcheck=1 +baseurl=http://download.suse.de/ibs/Devel:/YaST:/Agama:/Head/SLES-16.0/ +gpgkey=http://download.suse.de/ibs/Devel:/YaST:/Agama:/Head/SLES-16.0/repodata/repomd.xml.key diff --git a/live/root/etc/zypp/repos.d/agama-devel.repo b/live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo similarity index 77% rename from live/root/etc/zypp/repos.d/agama-devel.repo rename to live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo index fd862c4b4a..6ac8b482dd 100644 --- a/live/root/etc/zypp/repos.d/agama-devel.repo +++ b/live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo @@ -1,7 +1,7 @@ -[agama-devel] -name=Agama-Devel +[agama-openSUSE_Tumbleweed] +name=Agama-Devel (openSUSE Tumbleweed) type=rpm-md -enabled=1 +enabled=0 autorefresh=1 gpgcheck=1 baseurl=https://download.opensuse.org/repositories/systemsmanagement:/Agama:/Devel/openSUSE_Tumbleweed/ diff --git a/live/src/config.sh b/live/src/config.sh index 695c8813d7..0d88949c72 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -12,6 +12,13 @@ echo "Configure image: [$kiwi_iname]..." # setup baseproduct link suseSetupProduct +# enable the corresponding repository +DISTRO=$(grep "^NAME" /etc/os-release | cut -f2 -d\= | tr -d '"' | tr " " "_") +REPO="agama-${DISTRO}" +if zypper lr $REPO; then + zypper mr --enable $REPO +fi + # configure the repositories in the Live system # import the OBS key for the systemsmanagement OBS project rpm --import /tmp/systemsmanagement_key.gpg From aeeeb79a9a1d6113a7cef07da7b67c2c72564b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 19 Jul 2024 10:38:57 +0100 Subject: [PATCH 253/430] fix(live): adapt the self-update script --- ...ama-SLES.repo => agama-SLES.repo.disabled} | 6 +++--- ...> agama-openSUSE_Tumbleweed.repo.disabled} | 4 ++-- live/root/tmp/Devel_YaST_Agama_Head_key.gpg | 19 +++++++++++++++++++ live/root/usr/bin/agama-self-update | 11 ++++++----- live/src/config.sh | 10 +++++++--- 5 files changed, 37 insertions(+), 13 deletions(-) rename live/root/etc/zypp/repos.d/{agama-SLES.repo => agama-SLES.repo.disabled} (80%) rename live/root/etc/zypp/repos.d/{agama-openSUSE_Tumbleweed.repo => agama-openSUSE_Tumbleweed.repo.disabled} (88%) create mode 100644 live/root/tmp/Devel_YaST_Agama_Head_key.gpg diff --git a/live/root/etc/zypp/repos.d/agama-SLES.repo b/live/root/etc/zypp/repos.d/agama-SLES.repo.disabled similarity index 80% rename from live/root/etc/zypp/repos.d/agama-SLES.repo rename to live/root/etc/zypp/repos.d/agama-SLES.repo.disabled index 2c31f7efe1..a1a81029a8 100644 --- a/live/root/etc/zypp/repos.d/agama-SLES.repo +++ b/live/root/etc/zypp/repos.d/agama-SLES.repo.disabled @@ -1,7 +1,7 @@ -[agama-SLES] -name=Agama (SLES) +[agama-devel] +name=Agama Devel (SLES) type=rpm-md -enabled=0 +enabled=1 autorefresh=1 gpgcheck=1 baseurl=http://download.suse.de/ibs/Devel:/YaST:/Agama:/Head/SLES-16.0/ diff --git a/live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo b/live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo.disabled similarity index 88% rename from live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo rename to live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo.disabled index 6ac8b482dd..0f08d8c50c 100644 --- a/live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo +++ b/live/root/etc/zypp/repos.d/agama-openSUSE_Tumbleweed.repo.disabled @@ -1,7 +1,7 @@ -[agama-openSUSE_Tumbleweed] +[agama-devel] name=Agama-Devel (openSUSE Tumbleweed) type=rpm-md -enabled=0 +enabled=1 autorefresh=1 gpgcheck=1 baseurl=https://download.opensuse.org/repositories/systemsmanagement:/Agama:/Devel/openSUSE_Tumbleweed/ diff --git a/live/root/tmp/Devel_YaST_Agama_Head_key.gpg b/live/root/tmp/Devel_YaST_Agama_Head_key.gpg new file mode 100644 index 0000000000..cb40185b58 --- /dev/null +++ b/live/root/tmp/Devel_YaST_Agama_Head_key.gpg @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2.0.15 (GNU/Linux) + +mQENBFErWUoBCADTYW0S3mAHLCLLFuaRWxsK6NwGDmW6vSSkRt8OTXSAJyz2yFUS +oQ7dmw5ce6uyChoMBap0+2xiGyBzMZ4nP3ADD6voG5FXxg6QTfsDyz/sg5BRbS2W +q2e3XU4qEiJn357mXd53YfXKYpA7d2Ct5LKtKGph0DeGsi2zDKnjUCkXrOGNI+VH +qUCardfjAxgsA+m2bN/GKBOdHiHEHlQ0+GyLaoDi8utQz+L42iEQK4uvUQDKa+6k +fbocsR0z55meVCf5ZuEg/katzhtrtesG/sqTLCBMLsLGSIllHSZRxQPbBNf8l7wT +CPqg9JqXHT17aO3O+ZTkqv1dTIwXtmyPkIAjABEBAAG0JVVuc3VwcG9ydGVkIDx1 +bnN1cHBvcnRlZC1yc2FAc3VzZS5kZT6JATwEEwECACYCGwMGCwkIBwMCBBUCCAME +FgIDAQIeAQIXgAUCY+utDQUJGkUhwwAKCRCcdTFJzjtnLkZzCACJV8KKLtRtlw5a +MZJdFz1TKFzpAYd2Wcdtl7R/gYa+z4A91UbqtFHdK91EKXAvEkf4f88wj2Dvqj/1 +omBhO8si+p3Hxm1UVD91nTBNVWyjn33uai5YP2VEKQOzgBV2swVWuzSsJq0Kc1Yk +zKCPdtFpENdHfRKIXVgaDmUy0EAQJhBLx4SGD9l3DnG5Lt4d+jXe49BgCaGn+5eB +zl23nwL02wSD6uTShsqtdGQCaZtl2WjIb81H6rTRLcSPhyClpy/s3GV9wFri0zLL +Q6eiIbNj8uAvCDdGACEpmXic0lgIQdKsnJLelqJtwCpAmi3ma4cV1kCLD9z2Qtxl +ePb5BMBz +=xYx6 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/live/root/usr/bin/agama-self-update b/live/root/usr/bin/agama-self-update index b35393fb69..6cd8fb8688 100755 --- a/live/root/usr/bin/agama-self-update +++ b/live/root/usr/bin/agama-self-update @@ -6,17 +6,18 @@ # Agama Devel OBS project. -# first try a quick and simple solution, refreshing the OSS repository takes a +# first try a quick and simple solution, refreshing the distributions repository takes a # lot of time so try using only the agama-devel for update -zypper modifyrepo --disable repo-oss +zypper lr | grep "^[1-9]" | cut -f1 -d\| | xargs zypper modifyrepo --disable +zypper modifyrepo --enable agama-devel zypper refresh zypper --non-interactive dup --details --from agama-devel STATUS=$? -# enable OSS back -zypper modifyrepo --enable repo-oss +# enable all repositories back +zypper lr | egrep "^[1-9]" | cut -f1 -d\| | xargs zypper modifyrepo --enable -# if it failed try it again with the OSS repo enabled, maybe there was some +# if it failed try it again with all the repos enabled, maybe there was some # dependency problem which hopefully will be OK now if [ "$?" != "0" ]; then zypper --non-interactive dup --details --from agama-devel diff --git a/live/src/config.sh b/live/src/config.sh index 0d88949c72..b267c7633f 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -14,15 +14,19 @@ suseSetupProduct # enable the corresponding repository DISTRO=$(grep "^NAME" /etc/os-release | cut -f2 -d\= | tr -d '"' | tr " " "_") -REPO="agama-${DISTRO}" -if zypper lr $REPO; then - zypper mr --enable $REPO +REPO="/etc/zypp/repos.d/agama-${DISTRO}.repo" +if [ -f "${REPO}.disabled" ]; then + mv "${REPO}.disabled" $REPO fi +rm /etc/zypp/repos.d/*.disabled # configure the repositories in the Live system # import the OBS key for the systemsmanagement OBS project rpm --import /tmp/systemsmanagement_key.gpg rm /tmp/systemsmanagement_key.gpg +# import the OBS key for the Devel:YaST:Agama:Head project +rpm --import /tmp/Devel_YaST_Agama_Head_key.gpg +rm /tmp/Devel_YaST_Agama_Head_key.gpg # import the openSUSE keys rpm --import /usr/lib/rpm/gnupg/keys/*.asc From 7762e084d88f95d603642c964659a8b98c712d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 19 Jul 2024 10:48:53 +0100 Subject: [PATCH 254/430] doc(web): update the changes file --- live/src/agama-installer.changes | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/live/src/agama-installer.changes b/live/src/agama-installer.changes index dc8f179dbc..14b52910af 100644 --- a/live/src/agama-installer.changes +++ b/live/src/agama-installer.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Fri Jul 19 09:43:06 UTC 2024 - Imobach Gonzalez Sosa + +- Add a new profile for SLE-based distributions + (gh#openSUSE/agama#1475). +- Rename the package to "agama-installer". +- Do not include the full "base-x11" pattern but only the needed + packages. + ------------------------------------------------------------------- Thu Jul 18 15:25:39 UTC 2024 - Ladislav Slezák @@ -73,7 +82,7 @@ Thu Jun 6 14:30:19 UTC 2024 - Ladislav Slezák ------------------------------------------------------------------- Wed Jun 5 15:40:43 UTC 2024 - Knut Anderssen -- Fix the cd.ikr content for booting the s390x iso +- Fix the cd.ikr content for booting the s390x iso (gh#openSUSE/agama#1289). ------------------------------------------------------------------- From f34ed45eae904ae29379fa4ded70a234b68c6c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 19 Jul 2024 11:05:26 +0100 Subject: [PATCH 255/430] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ladislav Slezák --- live/root/usr/bin/agama-self-update | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/live/root/usr/bin/agama-self-update b/live/root/usr/bin/agama-self-update index 6cd8fb8688..e1958571f5 100755 --- a/live/root/usr/bin/agama-self-update +++ b/live/root/usr/bin/agama-self-update @@ -8,14 +8,14 @@ # first try a quick and simple solution, refreshing the distributions repository takes a # lot of time so try using only the agama-devel for update -zypper lr | grep "^[1-9]" | cut -f1 -d\| | xargs zypper modifyrepo --disable +zypper modifyrepo --disable --all zypper modifyrepo --enable agama-devel zypper refresh zypper --non-interactive dup --details --from agama-devel STATUS=$? # enable all repositories back -zypper lr | egrep "^[1-9]" | cut -f1 -d\| | xargs zypper modifyrepo --enable +zypper modifyrepo --enable --all # if it failed try it again with all the repos enabled, maybe there was some # dependency problem which hopefully will be OK now From 0df453566198ccb656798f8c979092b54baef829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 19 Jul 2024 11:34:47 +0100 Subject: [PATCH 256/430] fix(live): make clear when we use IBS --- live/src/config.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/live/src/config.sh b/live/src/config.sh index b267c7633f..580edf6fb9 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -24,7 +24,7 @@ rm /etc/zypp/repos.d/*.disabled # import the OBS key for the systemsmanagement OBS project rpm --import /tmp/systemsmanagement_key.gpg rm /tmp/systemsmanagement_key.gpg -# import the OBS key for the Devel:YaST:Agama:Head project +# import the IBS key for the Devel:YaST:Agama:Head project rpm --import /tmp/Devel_YaST_Agama_Head_key.gpg rm /tmp/Devel_YaST_Agama_Head_key.gpg # import the openSUSE keys From 1fc95dce39f482229aac288bb6087454776e6674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 19 Jul 2024 12:43:10 +0200 Subject: [PATCH 257/430] Downgrade Puppeteer from 22.13.1 to 22.13.0 The latest version fails to start with the Firefox browser :-( --- puppeteer/package-lock.json | 195 ++++++++++++++++++++++++------------ puppeteer/package.json | 2 +- 2 files changed, 134 insertions(+), 63 deletions(-) diff --git a/puppeteer/package-lock.json b/puppeteer/package-lock.json index 32814be03f..213a12b61d 100644 --- a/puppeteer/package-lock.json +++ b/puppeteer/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "chai": "^5.1.1", "mocha": "^10.6.0", - "puppeteer": "^22.13.0" + "puppeteer": "22.13.0" } }, "node_modules/@babel/code-frame": { @@ -109,18 +109,18 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.4.tgz", - "integrity": "sha512-BdG2qiI1dn89OTUUsx2GZSpUzW+DRffR1wlMJyKxVHYrhnKoELSDxDd+2XImUkuWPEKk76H5FcM/gPFrEK1Tfw==", - "dependencies": { - "debug": "^4.3.5", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.4.0", - "semver": "^7.6.2", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", - "yargs": "^17.7.2" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz", + "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ==", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.4.0", + "semver": "7.6.0", + "tar-fs": "3.0.5", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" @@ -142,6 +142,27 @@ "node": ">=12" } }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/@puppeteer/browsers/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -503,9 +524,9 @@ } }, "node_modules/chromium-bidi": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.1.tgz", - "integrity": "sha512-kSxJRj0VgtUKz6nmzc2JPfyfJGzwzt65u7PqhPHtgGQUZLF5oG+ST6l6e5ONfStUMAlhSutFCjaGKllXZa16jA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.0.tgz", + "integrity": "sha512-VnxVrpGojAjkiGFN2I+KtsDILFAjiGWVEDizOEnKzEDkT93eQT1cqTfUkqmOyLq33i1q4a1KDYbH+52CUe4Ufw==", "dependencies": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", @@ -1413,15 +1434,15 @@ } }, "node_modules/puppeteer": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.1.tgz", - "integrity": "sha512-PwXLDQK5u83Fm5A7TGMq+9BR7iHDJ8a3h21PSsh/E6VfhxiKYkU7+tvGZNSCap6k3pCNDd9oNteVBEctcBalmQ==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.0.tgz", + "integrity": "sha512-nmICzeHTBtZiu+y4vs0fboe/NKIFwH5W8RZuxmEVAKNfBQg/8u5FEQAvPlWmyVpJoAVM5kXD5PEl3GlK3F9pPA==", "hasInstallScript": true, "dependencies": { - "@puppeteer/browsers": "2.2.4", + "@puppeteer/browsers": "2.2.3", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1299070", - "puppeteer-core": "22.13.1" + "puppeteer-core": "22.13.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" @@ -1431,12 +1452,12 @@ } }, "node_modules/puppeteer-core": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.1.tgz", - "integrity": "sha512-NmhnASYp51QPRCAf9n0OPxuPMmzkKd8+2sB9Q+BjwwCG25gz6iuNc3LQDWa+cH2tyivmJppLhNNFt6Q3HmoOpw==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.0.tgz", + "integrity": "sha512-ZkpRX8nm/S39BnpcCverMzIc6oGWBPOUeOeaWRLKHqiKVCZ1l28HxPTYLitJlDiB16xZATSKpjul+sl+ZEm0HQ==", "dependencies": { - "@puppeteer/browsers": "2.2.4", - "chromium-bidi": "0.6.1", + "@puppeteer/browsers": "2.2.3", + "chromium-bidi": "0.6.0", "debug": "^4.3.5", "devtools-protocol": "0.0.1299070", "ws": "^8.18.0" @@ -1505,9 +1526,12 @@ ] }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" }, @@ -1515,6 +1539,17 @@ "node": ">=10" } }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -1635,9 +1670,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" @@ -1768,6 +1803,11 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -1914,18 +1954,18 @@ } }, "@puppeteer/browsers": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.4.tgz", - "integrity": "sha512-BdG2qiI1dn89OTUUsx2GZSpUzW+DRffR1wlMJyKxVHYrhnKoELSDxDd+2XImUkuWPEKk76H5FcM/gPFrEK1Tfw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz", + "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ==", "requires": { - "debug": "^4.3.5", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.4.0", - "semver": "^7.6.2", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", - "yargs": "^17.7.2" + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.4.0", + "semver": "7.6.0", + "tar-fs": "3.0.5", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" }, "dependencies": { "cliui": { @@ -1938,6 +1978,19 @@ "wrap-ansi": "^7.0.0" } }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -2198,9 +2251,9 @@ } }, "chromium-bidi": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.1.tgz", - "integrity": "sha512-kSxJRj0VgtUKz6nmzc2JPfyfJGzwzt65u7PqhPHtgGQUZLF5oG+ST6l6e5ONfStUMAlhSutFCjaGKllXZa16jA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.0.tgz", + "integrity": "sha512-VnxVrpGojAjkiGFN2I+KtsDILFAjiGWVEDizOEnKzEDkT93eQT1cqTfUkqmOyLq33i1q4a1KDYbH+52CUe4Ufw==", "requires": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", @@ -2834,23 +2887,23 @@ } }, "puppeteer": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.1.tgz", - "integrity": "sha512-PwXLDQK5u83Fm5A7TGMq+9BR7iHDJ8a3h21PSsh/E6VfhxiKYkU7+tvGZNSCap6k3pCNDd9oNteVBEctcBalmQ==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.0.tgz", + "integrity": "sha512-nmICzeHTBtZiu+y4vs0fboe/NKIFwH5W8RZuxmEVAKNfBQg/8u5FEQAvPlWmyVpJoAVM5kXD5PEl3GlK3F9pPA==", "requires": { - "@puppeteer/browsers": "2.2.4", + "@puppeteer/browsers": "2.2.3", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1299070", - "puppeteer-core": "22.13.1" + "puppeteer-core": "22.13.0" } }, "puppeteer-core": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.1.tgz", - "integrity": "sha512-NmhnASYp51QPRCAf9n0OPxuPMmzkKd8+2sB9Q+BjwwCG25gz6iuNc3LQDWa+cH2tyivmJppLhNNFt6Q3HmoOpw==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.0.tgz", + "integrity": "sha512-ZkpRX8nm/S39BnpcCverMzIc6oGWBPOUeOeaWRLKHqiKVCZ1l28HxPTYLitJlDiB16xZATSKpjul+sl+ZEm0HQ==", "requires": { - "@puppeteer/browsers": "2.2.4", - "chromium-bidi": "0.6.1", + "@puppeteer/browsers": "2.2.3", + "chromium-bidi": "0.6.0", "debug": "^4.3.5", "devtools-protocol": "0.0.1299070", "ws": "^8.18.0" @@ -2893,9 +2946,22 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + } + } }, "serialize-javascript": { "version": "6.0.2", @@ -2983,9 +3049,9 @@ } }, "tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", "requires": { "bare-fs": "^2.1.1", "bare-path": "^2.1.0", @@ -3085,6 +3151,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/puppeteer/package.json b/puppeteer/package.json index 26ef5b268d..1c09fea679 100644 --- a/puppeteer/package.json +++ b/puppeteer/package.json @@ -3,6 +3,6 @@ "dependencies": { "chai": "^5.1.1", "mocha": "^10.6.0", - "puppeteer": "^22.13.0" + "puppeteer": "22.13.0" } } From 819ae72f0b674f8745b0f91fd0f349d3a7332e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 19 Jul 2024 13:46:43 +0200 Subject: [PATCH 258/430] Added changes --- puppeteer/package/agama-integration-tests.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/puppeteer/package/agama-integration-tests.changes b/puppeteer/package/agama-integration-tests.changes index def90b35e9..54d96b1f8e 100644 --- a/puppeteer/package/agama-integration-tests.changes +++ b/puppeteer/package/agama-integration-tests.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Jul 19 10:18:37 UTC 2024 - Ladislav Slezák + +- Downgrade Puppeteer from 22.13.1 to 22.13.0, the latest version + fails to start with the Firefox browser (gh#openSUSE/agama#1485) + ------------------------------------------------------------------- Tue Jul 16 13:25:52 UTC 2024 - Ladislav Slezák From 4d57389647f4a2b61963522adcbe7ae32e84954a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 19 Jul 2024 17:36:57 +0200 Subject: [PATCH 259/430] Use "puppeteer-core" instead of full "puppeteer" NPM package The full "puppeteer" NPM package also takes care of downloading and managing the web browsers. But as we use the system browser we do not need this functionality. Installing just the "puppeteer-core" package makes the NPM bundle a little bit smaller. See more details in https://pptr.dev/guides/installation --- puppeteer/package-lock.json | 417 +----------------- puppeteer/package.json | 2 +- .../package/agama-integration-tests.spec | 2 +- puppeteer/tests/test_root_password.js | 2 +- 4 files changed, 4 insertions(+), 419 deletions(-) diff --git a/puppeteer/package-lock.json b/puppeteer/package-lock.json index 213a12b61d..9647e20115 100644 --- a/puppeteer/package-lock.json +++ b/puppeteer/package-lock.json @@ -7,105 +7,7 @@ "dependencies": { "chai": "^5.1.1", "mocha": "^10.6.0", - "puppeteer": "22.13.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "puppeteer-core": "22.13.0" } }, "node_modules/@puppeteer/browsers": { @@ -432,14 +334,6 @@ "node": "*" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -562,31 +456,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -674,22 +543,6 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -992,21 +845,6 @@ } ] }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1034,11 +872,6 @@ "node": ">= 12" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1104,11 +937,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1125,11 +953,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1141,11 +964,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1328,34 +1146,6 @@ "node": ">= 14" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1377,11 +1167,6 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1433,24 +1218,6 @@ "once": "^1.3.1" } }, - "node_modules/puppeteer": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.0.tgz", - "integrity": "sha512-nmICzeHTBtZiu+y4vs0fboe/NKIFwH5W8RZuxmEVAKNfBQg/8u5FEQAvPlWmyVpJoAVM5kXD5PEl3GlK3F9pPA==", - "hasInstallScript": true, - "dependencies": { - "@puppeteer/browsers": "2.2.3", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1299070", - "puppeteer-core": "22.13.0" - }, - "bin": { - "puppeteer": "lib/esm/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/puppeteer-core": { "version": "22.13.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.0.tgz", @@ -1498,14 +1265,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1877,82 +1636,6 @@ } }, "dependencies": { - "@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "requires": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" - }, - "@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "requires": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "@puppeteer/browsers": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz", @@ -2189,11 +1872,6 @@ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -2283,17 +1961,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "requires": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - } - }, "data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -2357,19 +2024,6 @@ "once": "^1.4.0" } }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -2560,15 +2214,6 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2592,11 +2237,6 @@ "sprintf-js": "^1.1.3" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2638,11 +2278,6 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2656,11 +2291,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2670,11 +2300,6 @@ "universalify": "^2.0.0" } }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2808,25 +2433,6 @@ "netmask": "^2.0.2" } }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2842,11 +2448,6 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, - "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" - }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2886,17 +2487,6 @@ "once": "^1.3.1" } }, - "puppeteer": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.0.tgz", - "integrity": "sha512-nmICzeHTBtZiu+y4vs0fboe/NKIFwH5W8RZuxmEVAKNfBQg/8u5FEQAvPlWmyVpJoAVM5kXD5PEl3GlK3F9pPA==", - "requires": { - "@puppeteer/browsers": "2.2.3", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1299070", - "puppeteer-core": "22.13.0" - } - }, "puppeteer-core": { "version": "22.13.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.0.tgz", @@ -2935,11 +2525,6 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/puppeteer/package.json b/puppeteer/package.json index 1c09fea679..71c5178aa1 100644 --- a/puppeteer/package.json +++ b/puppeteer/package.json @@ -3,6 +3,6 @@ "dependencies": { "chai": "^5.1.1", "mocha": "^10.6.0", - "puppeteer": "22.13.0" + "puppeteer-core": "22.13.0" } } diff --git a/puppeteer/package/agama-integration-tests.spec b/puppeteer/package/agama-integration-tests.spec index 7eb55e26ad..fcd28918dd 100644 --- a/puppeteer/package/agama-integration-tests.spec +++ b/puppeteer/package/agama-integration-tests.spec @@ -45,7 +45,7 @@ outside. %build rm -f package-lock.json -PUPPETEER_SKIP_DOWNLOAD=true local-npm-registry %{_sourcedir} install --omit=optional --with=dev --legacy-peer-deps || ( find ~/.npm/_logs -name '*-debug.log' -print0 | xargs -0 cat; false) +local-npm-registry %{_sourcedir} install --omit=optional --with=dev --legacy-peer-deps || ( find ~/.npm/_logs -name '*-debug.log' -print0 | xargs -0 cat; false) %install install -D -d -m 0755 %{buildroot}%{_datadir}/agama/integration-tests diff --git a/puppeteer/tests/test_root_password.js b/puppeteer/tests/test_root_password.js index 7bf938984b..2848df3339 100644 --- a/puppeteer/tests/test_root_password.js +++ b/puppeteer/tests/test_root_password.js @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; -import puppeteer from "puppeteer"; +import puppeteer from "puppeteer-core"; import { expect } from "chai"; // helper function for converting String to Boolean From 4d8c0ced453af80cda904aa3bfcdad18d7866ed5 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 19 Jul 2024 22:01:21 +0200 Subject: [PATCH 260/430] use correct property names --- rust/agama-server/src/questions/web.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 6fd1788cdf..7615c20911 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -173,7 +173,7 @@ impl<'a> QuestionsClient<'a> { if let Some(password_iface) = question.get(&password_interface) { result.with_password = Some(PasswordAnswer { - password: get_property(password_iface, "password")?, + password: get_property(password_iface, "Password")?, }); } let generic_interface = OwnedInterfaceName::from( @@ -183,7 +183,7 @@ impl<'a> QuestionsClient<'a> { let generic_iface = question .get(&generic_interface) .context("Question does not have generic interface")?; - let answer: String = get_property(generic_iface, "answer")?; + let answer: String = get_property(generic_iface, "Answer")?; if answer.is_empty() { Ok(None) } else { From 69839ed08091a4105ad14b1b9c9c85fda2a688cf Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 21 Jul 2024 02:50:58 +0000 Subject: [PATCH 261/430] Update web PO files Agama-weblate commit: 9aa82704acc632ca95b48fbae60571a1f5d0bbe7 --- web/po/ca.po | 46 +++++++---- web/po/cs.po | 204 +++++++++++++++++++++++++--------------------- web/po/de.po | 40 +++++---- web/po/es.po | 40 +++++---- web/po/fr.po | 39 +++++---- web/po/id.po | 39 +++++---- web/po/ja.po | 40 +++++---- web/po/ka.po | 39 +++++---- web/po/mk.po | 39 +++++---- web/po/nb_NO.po | 40 +++++---- web/po/nl.po | 40 +++++---- web/po/pt_BR.po | 40 +++++---- web/po/ru.po | 56 ++++++++----- web/po/sv.po | 40 +++++---- web/po/tr.po | 39 +++++---- web/po/uk.po | 39 +++++---- web/po/zh_Hans.po | 40 +++++---- 17 files changed, 520 insertions(+), 340 deletions(-) diff --git a/web/po/ca.po b/web/po/ca.po index b70ee9ae12..6b84b4dbc6 100644 --- a/web/po/ca.po +++ b/web/po/ca.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-14 02:32+0000\n" -"PO-Revision-Date: 2024-07-12 09:47+0000\n" +"POT-Creation-Date: 2024-07-18 02:24+0000\n" +"PO-Revision-Date: 2024-07-18 11:47+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan \n" @@ -697,39 +697,39 @@ msgstr "Connecteu-vos a una xarxa Wi-Fi" msgid "The system will use %s as its default language." msgstr "El sistema usarà el %s com a llengua per defecte." -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "Usuaris" -#: src/components/overview/OverviewPage.jsx:48 +#: src/components/overview/OverviewPage.jsx:50 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:307 +#: src/components/storage/ProposalPage.jsx:295 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "Emmagatzematge" -#: src/components/overview/OverviewPage.jsx:49 +#: src/components/overview/OverviewPage.jsx:51 #: src/components/overview/SoftwareSection.jsx:86 -#: src/components/software/SoftwarePage.jsx:155 +#: src/components/software/SoftwarePage.jsx:181 #: src/components/software/routes.js:32 msgid "Software" msgstr "Programari" -#: src/components/overview/OverviewPage.jsx:54 +#: src/components/overview/OverviewPage.jsx:56 msgid "Ready for installation" msgstr "A punt per a la instal·lació" -#: src/components/overview/OverviewPage.jsx:104 +#: src/components/overview/OverviewPage.jsx:102 msgid "Installation" msgstr "Instal·lació" -#: src/components/overview/OverviewPage.jsx:105 +#: src/components/overview/OverviewPage.jsx:103 msgid "Before installing, please check the following problems." msgstr "Abans d'instal·lar, comproveu els problemes següents." -#: src/components/overview/OverviewPage.jsx:116 +#: src/components/overview/OverviewPage.jsx:114 msgid "" "Take your time to check your configuration before starting the installation " "process." @@ -737,7 +737,7 @@ msgstr "" "Dediqueu el temps que calgui a comprovar la configuració abans de començar " "el procés d'instal·lació." -#: src/components/overview/OverviewPage.jsx:125 +#: src/components/overview/OverviewPage.jsx:123 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -910,14 +910,24 @@ msgid "The following software patterns are selected for installation:" msgstr "" "S'han seleccionat els patrons de programari següents per a la instal·lació:" -#: src/components/software/SoftwarePage.jsx:165 +#: src/components/software/SoftwarePage.jsx:105 +#: src/components/software/SoftwarePage.jsx:119 msgid "Selected patterns" msgstr "Patrons seleccionats" -#: src/components/software/SoftwarePage.jsx:168 +#: src/components/software/SoftwarePage.jsx:108 msgid "Change selection" msgstr "Canvia la selecció" +#: src/components/software/SoftwarePage.jsx:123 +msgid "" +"This product does not allow to select software patterns during installation. " +"However, you can add additional software once the installation is finished." +msgstr "" +"Aquest producte no permet seleccionar patrons de programari durant la instal·" +"lació. Tanmateix, hi podeu afegir programari addicional un cop acabada la " +"instal·lació." + #: src/components/software/SoftwarePatternsSelection.jsx:223 msgid "auto selected" msgstr "seleccionat automàticament" @@ -944,10 +954,12 @@ msgid "Installation will take %s." msgstr "La instal·lació necessitarà %s." #: src/components/software/UsedSize.jsx:37 -msgid "This space includes the base system and the selected software patterns." +msgid "" +"This space includes the base system and the selected software patterns, if " +"any." msgstr "" "Aquest espai inclou el sistema de base i els patrons de programari " -"seleccionats." +"seleccionats, si n'hi ha." #: src/components/storage/BootConfigField.jsx:43 msgid "Change boot options" @@ -1668,7 +1680,7 @@ msgstr[1] "Marca les %d accions planificades" msgid "Waiting for actions information..." msgstr "Esperant la informació de les accions..." -#: src/components/storage/ProposalPage.jsx:329 +#: src/components/storage/ProposalPage.jsx:317 msgid "Planned Actions" msgstr "Accions planificades" diff --git a/web/po/cs.po b/web/po/cs.po index 18235aa011..836ba34bab 100644 --- a/web/po/cs.po +++ b/web/po/cs.po @@ -8,33 +8,33 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-14 02:32+0000\n" -"PO-Revision-Date: 2023-12-31 20:39+0000\n" -"Last-Translator: Ladislav Slezák \n" -"Language-Team: Czech \n" +"POT-Creation-Date: 2024-07-18 02:24+0000\n" +"PO-Revision-Date: 2024-07-20 17:47+0000\n" +"Last-Translator: Aleš Kastner \n" +"Language-Team: Czech " +"\n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 4.9.1\n" +"X-Generator: Weblate 5.6.2\n" #: src/MainLayout.jsx:52 msgid "Agama" -msgstr "" +msgstr "Agama" #: src/MainLayout.jsx:94 msgid "Change product" -msgstr "" +msgstr "Změnit produkt" #: src/components/core/About.jsx:49 msgid "About" -msgstr "" +msgstr "O" #: src/components/core/About.jsx:69 msgid "About Agama" -msgstr "" +msgstr "O Agamě" #: src/components/core/About.jsx:74 msgid "" @@ -43,13 +43,17 @@ msgid "" "want to give it a try, we recommend using a virtual machine to prevent any " "possible data loss." msgstr "" +"Agama je experimentální instalátor pro (otevřené) systémy SUSE. Je stále ve " +"fázi vývoje, proto ji prosím nepoužívejte v produkčních prostředích. Pokud " +"ji chcete vyzkoušet, doporučujeme použít virtuální počítač, abyste předešli " +"případné ztrátě dat." #. TRANSLATORS: content of the "About" popup (2/2) #. %s is replaced by the project URL #: src/components/core/About.jsx:86 #, c-format msgid "For more information, please visit the project's repository at %s." -msgstr "" +msgstr "Další informace najdete v úložišti projektu na %s." #: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 #: src/components/software/SoftwarePatternsSelection.jsx:268 @@ -58,52 +62,56 @@ msgstr "Zavřít" #: src/components/core/InstallButton.jsx:32 msgid "Confirm Installation" -msgstr "" +msgstr "Potvrdit instalaci" #: src/components/core/InstallButton.jsx:36 msgid "" "If you continue, partitions on your hard disk will be modified according to " "the provided installation settings." msgstr "" +"Budete-li pokračovat, připravíme oddíly na pevném disku podle zadaných " +"instalačních nastavení." #: src/components/core/InstallButton.jsx:40 msgid "Please, cancel and check the settings if you are unsure." -msgstr "" +msgstr "Nejste-li si jisti, zrušte akci a zkontrolujte nastavení." #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:45 msgid "Continue" -msgstr "" +msgstr "Pokračovat" #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 #: src/components/core/Popup.jsx:132 #: src/components/network/WifiConnectionForm.jsx:134 msgid "Cancel" -msgstr "" +msgstr "Zrušit/Storno" #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:84 msgid "Install" -msgstr "" +msgstr "Instalovat" #: src/components/core/InstallationFinished.jsx:48 msgid "TPM sealing requires the new system to be booted directly." -msgstr "" +msgstr "Zapečetění čipem TPM vyžaduje přímé spuštění nového systému." #: src/components/core/InstallationFinished.jsx:53 msgid "" "If a local media was used to run this installer, remove it before the next " "boot." msgstr "" +"Bylo-li ke spuštění tohoto instalačního programu použito místní médium, před " +"dalším spuštěním ho odstraňte." #: src/components/core/InstallationFinished.jsx:57 msgid "Hide details" -msgstr "" +msgstr "Skrýt podrobnosti" #: src/components/core/InstallationFinished.jsx:57 msgid "See more details" -msgstr "" +msgstr "Ukázat podrobněji" #: src/components/core/InstallationFinished.jsx:62 msgid "" @@ -112,58 +120,62 @@ msgid "" "of the new system. For that to work, the machine needs to boot directly to " "the new boot loader." msgstr "" +"Poslední krok konfigurace modulu TPM (Trusted Platform Module) pro " +"automatické otevírání šifrovaných zařízení se provede při prvním spuštění " +"nového systému. Aby to fungovalo, musí se počítač spustit přímo novým " +"zavaděčem." #: src/components/core/InstallationFinished.jsx:107 msgid "Congratulations!" -msgstr "" +msgstr "Blahopřejeme!" #: src/components/core/InstallationFinished.jsx:116 msgid "The installation on your machine is complete." -msgstr "" +msgstr "Instalace na Váš počítač je dokončena." #: src/components/core/InstallationFinished.jsx:119 msgid "At this point you can power off the machine." -msgstr "" +msgstr "Nyní můžete počítač vypnout." #: src/components/core/InstallationFinished.jsx:121 msgid "At this point you can reboot the machine to log in to the new system." -msgstr "" +msgstr "Nyní můžete počítač restartovat a přihlásit se do nového systému." #: src/components/core/InstallationFinished.jsx:130 msgid "Finish" -msgstr "" +msgstr "Dokončeno" #: src/components/core/InstallationFinished.jsx:130 msgid "Reboot" -msgstr "" +msgstr "Restart systému" #: src/components/core/InstallationProgress.jsx:30 msgid "Installing the system, please wait ..." -msgstr "" +msgstr "Instaluji systém, čekejte ..." #: src/components/core/InstallerOptions.jsx:92 msgid "Show installer options" -msgstr "" +msgstr "Ukázat možnosti instalace" #: src/components/core/InstallerOptions.jsx:95 msgid "Installer options" -msgstr "" +msgstr "Možnosti instalace" #: src/components/core/InstallerOptions.jsx:98 #: src/components/core/InstallerOptions.jsx:102 #: src/components/core/InstallerOptions.jsx:103 #: src/components/l10n/L10nPage.jsx:60 msgid "Language" -msgstr "" +msgstr "Jazyk" #: src/components/core/InstallerOptions.jsx:115 #: src/components/core/InstallerOptions.jsx:120 msgid "Keyboard layout" -msgstr "" +msgstr "Rozložení kláves" #: src/components/core/InstallerOptions.jsx:129 msgid "Cannot be changed in remote installation" -msgstr "" +msgstr "U instalace na dálku nelze změnit" #: src/components/core/InstallerOptions.jsx:142 #: src/components/network/IpSettingsForm.jsx:228 @@ -176,78 +188,78 @@ msgstr "" #: src/components/storage/ZFCPPage.jsx:528 #: src/components/users/FirstUserForm.jsx:303 msgid "Accept" -msgstr "" +msgstr "Přijmout" #: src/components/core/IssuesHint.jsx:34 msgid "" "Before starting the installation, you need to address the following problems:" -msgstr "" +msgstr "Před zahájením instalace vyřešte tyto problémy:" #: src/components/core/ListSearch.jsx:48 msgid "Search" -msgstr "" +msgstr "Hledat" #: src/components/core/LoginPage.jsx:64 msgid "Could not log in. Please, make sure that the password is correct." -msgstr "" +msgstr "Nelze se přhlásit. Zkontrolujte správnost hesla." #: src/components/core/LoginPage.jsx:66 msgid "Could not authenticate against the server, please check it." -msgstr "" +msgstr "Nezdařilo se ověřit Vás u serveru, zkontrolujte to prosím." #. TRANSLATORS: Title for a form to provide the password for the root user. %s #. will be replaced by "root" #: src/components/core/LoginPage.jsx:74 #, c-format msgid "Log in as %s" -msgstr "" +msgstr "Přihlásit se jako %s" #: src/components/core/LoginPage.jsx:80 msgid "The installer requires [root] user privileges." -msgstr "" +msgstr "Instalátor vyžaduje oprávnění uživatele [root]." #: src/components/core/LoginPage.jsx:95 msgid "Please, provide its password to log in to the system." -msgstr "" +msgstr "Zadejte heslo pro přihlášení do systému." #: src/components/core/LoginPage.jsx:96 msgid "Login form" -msgstr "" +msgstr "Přihlašovací formulář" #: src/components/core/LoginPage.jsx:102 msgid "Password input" -msgstr "" +msgstr "Zadejte heslo" #: src/components/core/LoginPage.jsx:111 msgid "Log in" -msgstr "" +msgstr "Přihlásit se" #: src/components/core/LoginPage.jsx:121 msgid "More about this" -msgstr "" +msgstr "Více o tom" #: src/components/core/LogsButton.jsx:101 msgid "Collecting logs..." -msgstr "" +msgstr "Shromažďuji záznamy..." #: src/components/core/LogsButton.jsx:101 #: src/components/core/LogsButton.jsx:104 msgid "Download logs" -msgstr "" +msgstr "Stahuji zázbamy" #: src/components/core/LogsButton.jsx:111 msgid "" "The browser will run the logs download as soon as they are ready. Please, be " "patient." -msgstr "" +msgstr "Prohlížeč stáhne záznamy, jakmile budou připraveny. Čekejte prosím." #: src/components/core/LogsButton.jsx:121 msgid "Something went wrong while collecting logs. Please, try again." -msgstr "" +msgstr "Stahování záznamů se nezdařilo. Zkuste to znovu." #: src/components/core/PasswordAndConfirmationInput.jsx:55 msgid "Passwords do not match" -msgstr "" +msgstr "Hesla se neshodují" #: src/components/core/PasswordAndConfirmationInput.jsx:79 #: src/components/network/WifiConnectionForm.jsx:121 @@ -255,75 +267,74 @@ msgstr "" #: src/components/storage/iscsi/AuthFields.jsx:94 #: src/components/users/RootAuthMethods.jsx:165 msgid "Password" -msgstr "" +msgstr "Heslo" #: src/components/core/PasswordAndConfirmationInput.jsx:90 msgid "Password confirmation" -msgstr "" +msgstr "Potvrzení hesla" #: src/components/core/PasswordInput.jsx:61 msgid "Password visibility button" -msgstr "" +msgstr "Tlačítko viditelnosti hesla" #: src/components/core/Popup.jsx:92 msgid "Confirm" -msgstr "" +msgstr "Potvrdit" #. TRANSLATORS: progress message #: src/components/core/Popup.jsx:210 -#, fuzzy msgid "Loading data..." msgstr "Soubor se načítá…" #: src/components/core/ProgressReport.jsx:50 msgid "Finished" -msgstr "" +msgstr "Dokončeno" #: src/components/core/ProgressReport.jsx:59 msgid "In progress" -msgstr "" +msgstr "Probíhá" #: src/components/core/ProgressReport.jsx:74 msgid "Pending" -msgstr "" +msgstr "Čeká se na" #: src/components/core/ProgressReport.jsx:138 msgid "Waiting for progress status..." -msgstr "" +msgstr "Čekáme na stav postupu..." #: src/components/core/RowActions.jsx:64 #: src/components/storage/PartitionsField.jsx:491 #: src/components/storage/ProposalActionsSummary.jsx:233 msgid "Actions" -msgstr "" +msgstr "Akce" #: src/components/core/SectionSkeleton.jsx:27 msgid "Waiting" -msgstr "" +msgstr "Čekám" #: src/components/core/ServerError.jsx:47 msgid "Cannot connect to Agama server" -msgstr "" +msgstr "Nelze se připojit k serveru Agama" #: src/components/core/ServerError.jsx:51 msgid "Please, check whether it is running." -msgstr "" +msgstr "Zkontrolujte, zda je spuštěn." #: src/components/core/ServerError.jsx:56 msgid "Reload" -msgstr "" +msgstr "Znovu načíst" #: src/components/l10n/KeyboardSelection.jsx:41 msgid "Filter by description or keymap code" -msgstr "" +msgstr "Filtrování podle popisu nebo kódu mapy kláves" #: src/components/l10n/KeyboardSelection.jsx:71 msgid "None of the keymaps match the filter." -msgstr "" +msgstr "Žádná z map kláves neodpovídá filtru." #: src/components/l10n/KeyboardSelection.jsx:77 msgid "Keyboard selection" -msgstr "" +msgstr "Výběr klávesnice" #: src/components/l10n/KeyboardSelection.jsx:90 #: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 @@ -380,51 +391,51 @@ msgstr "" #: src/components/l10n/TimezoneSelection.jsx:101 msgid "None of the time zones match the filter." -msgstr "" +msgstr "Žádné z časových pásem neodpovídá filtru." #: src/components/l10n/TimezoneSelection.jsx:107 msgid " Timezone selection" -msgstr "" +msgstr " Výběr časového pásma" #: src/components/layout/Loading.jsx:31 msgid "Loading installation environment, please wait." -msgstr "" +msgstr "Načítá se instalační prostředí, vyčkejte prosím." #. TRANSLATORS: button label #: src/components/network/AddressesDataList.jsx:88 #: src/components/network/DnsDataList.jsx:95 msgid "Remove" -msgstr "" +msgstr "Odstranit" #. TRANSLATORS: input field name #: src/components/network/AddressesDataList.jsx:100 #: src/components/network/AddressesDataList.jsx:101 #: src/components/network/IpAddressInput.jsx:33 msgid "IP Address" -msgstr "" +msgstr "IP adresa" #. TRANSLATORS: input field name #: src/components/network/AddressesDataList.jsx:109 #: src/components/network/AddressesDataList.jsx:110 msgid "Prefix length or netmask" -msgstr "" +msgstr "Délka předpony nebo maska sítě" #: src/components/network/AddressesDataList.jsx:126 msgid "Add an address" -msgstr "" +msgstr "Přidat adresu" #. TRANSLATORS: button label #: src/components/network/AddressesDataList.jsx:126 msgid "Add another address" -msgstr "" +msgstr "Přidat další adresu" #: src/components/network/AddressesDataList.jsx:131 msgid "Addresses" -msgstr "" +msgstr "Adresy" #: src/components/network/AddressesDataList.jsx:133 msgid "Addresses data list" -msgstr "" +msgstr "Seznam údajů o adresách" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header @@ -437,13 +448,13 @@ msgstr "" #: src/components/storage/iscsi/NodesPresenter.jsx:98 #: src/components/storage/iscsi/NodesPresenter.jsx:119 msgid "Name" -msgstr "" +msgstr "Název" #. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:66 #: src/components/network/ConnectionsTable.jsx:93 msgid "IP addresses" -msgstr "" +msgstr "IP adresy" #: src/components/network/ConnectionsTable.jsx:74 #: src/components/network/WifiNetworksListPage.jsx:107 @@ -453,7 +464,7 @@ msgstr "" #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:120 msgid "Edit" -msgstr "" +msgstr "Upravit" #. TRANSLATORS: %s is replaced by a network connection name #: src/components/network/ConnectionsTable.jsx:77 @@ -673,45 +684,45 @@ msgstr "" msgid "The system will use %s as its default language." msgstr "" -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "" -#: src/components/overview/OverviewPage.jsx:48 +#: src/components/overview/OverviewPage.jsx:50 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:307 +#: src/components/storage/ProposalPage.jsx:295 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "" -#: src/components/overview/OverviewPage.jsx:49 +#: src/components/overview/OverviewPage.jsx:51 #: src/components/overview/SoftwareSection.jsx:86 -#: src/components/software/SoftwarePage.jsx:155 +#: src/components/software/SoftwarePage.jsx:181 #: src/components/software/routes.js:32 msgid "Software" msgstr "" -#: src/components/overview/OverviewPage.jsx:54 +#: src/components/overview/OverviewPage.jsx:56 msgid "Ready for installation" msgstr "" -#: src/components/overview/OverviewPage.jsx:104 +#: src/components/overview/OverviewPage.jsx:102 msgid "Installation" msgstr "" -#: src/components/overview/OverviewPage.jsx:105 +#: src/components/overview/OverviewPage.jsx:103 msgid "Before installing, please check the following problems." msgstr "" -#: src/components/overview/OverviewPage.jsx:116 +#: src/components/overview/OverviewPage.jsx:114 msgid "" "Take your time to check your configuration before starting the installation " "process." msgstr "" -#: src/components/overview/OverviewPage.jsx:125 +#: src/components/overview/OverviewPage.jsx:123 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -860,14 +871,21 @@ msgstr "" msgid "The following software patterns are selected for installation:" msgstr "" -#: src/components/software/SoftwarePage.jsx:165 +#: src/components/software/SoftwarePage.jsx:105 +#: src/components/software/SoftwarePage.jsx:119 msgid "Selected patterns" msgstr "" -#: src/components/software/SoftwarePage.jsx:168 +#: src/components/software/SoftwarePage.jsx:108 msgid "Change selection" msgstr "" +#: src/components/software/SoftwarePage.jsx:123 +msgid "" +"This product does not allow to select software patterns during installation. " +"However, you can add additional software once the installation is finished." +msgstr "" + #: src/components/software/SoftwarePatternsSelection.jsx:223 msgid "auto selected" msgstr "" @@ -894,7 +912,9 @@ msgid "Installation will take %s." msgstr "" #: src/components/software/UsedSize.jsx:37 -msgid "This space includes the base system and the selected software patterns." +msgid "" +"This space includes the base system and the selected software patterns, if " +"any." msgstr "" #: src/components/storage/BootConfigField.jsx:43 @@ -1595,7 +1615,7 @@ msgstr[2] "" msgid "Waiting for actions information..." msgstr "" -#: src/components/storage/ProposalPage.jsx:329 +#: src/components/storage/ProposalPage.jsx:317 msgid "Planned Actions" msgstr "" diff --git a/web/po/de.po b/web/po/de.po index b73e88bcb8..9b304decba 100644 --- a/web/po/de.po +++ b/web/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-14 02:32+0000\n" +"POT-Creation-Date: 2024-07-18 02:24+0000\n" "PO-Revision-Date: 2024-07-13 19:47+0000\n" "Last-Translator: Ettore Atalan \n" "Language-Team: German \n" "Language-Team: Spanish \n" "Language-Team: French \n" "Language-Team: Indonesian \n" "Language-Team: Japanese \n" "Language-Team: Georgian \n" "Language-Team: Macedonian \n" "Language-Team: Norwegian Bokmål \n" "Language-Team: Dutch \n" "Language-Team: Portuguese (Brazil) \n" "Language-Team: Russian \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" -"X-Generator: Weblate 5.6\n" +"X-Generator: Weblate 5.6.2\n" #: src/MainLayout.jsx:52 msgid "Agama" @@ -696,45 +696,45 @@ msgstr "Подключиться к сети Wi-Fi" msgid "The system will use %s as its default language." msgstr "Система будет использовать %s в качестве языка по умолчанию." -#: src/components/overview/OverviewPage.jsx:47 +#: src/components/overview/OverviewPage.jsx:49 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" msgstr "Пользователи" -#: src/components/overview/OverviewPage.jsx:48 +#: src/components/overview/OverviewPage.jsx:50 #: src/components/overview/StorageSection.jsx:111 -#: src/components/storage/ProposalPage.jsx:307 +#: src/components/storage/ProposalPage.jsx:295 #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" msgstr "Хранилище" -#: src/components/overview/OverviewPage.jsx:49 +#: src/components/overview/OverviewPage.jsx:51 #: src/components/overview/SoftwareSection.jsx:86 -#: src/components/software/SoftwarePage.jsx:155 +#: src/components/software/SoftwarePage.jsx:181 #: src/components/software/routes.js:32 msgid "Software" msgstr "Программы" -#: src/components/overview/OverviewPage.jsx:54 +#: src/components/overview/OverviewPage.jsx:56 msgid "Ready for installation" msgstr "Готов к установке" -#: src/components/overview/OverviewPage.jsx:104 +#: src/components/overview/OverviewPage.jsx:102 msgid "Installation" msgstr "Установка" -#: src/components/overview/OverviewPage.jsx:105 +#: src/components/overview/OverviewPage.jsx:103 msgid "Before installing, please check the following problems." msgstr "Проверьте следующие проблемы перед установкой." -#: src/components/overview/OverviewPage.jsx:116 +#: src/components/overview/OverviewPage.jsx:114 msgid "" "Take your time to check your configuration before starting the installation " "process." msgstr "Проверьте свои настройки до начала процесса установки." -#: src/components/overview/OverviewPage.jsx:125 +#: src/components/overview/OverviewPage.jsx:123 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." @@ -908,14 +908,24 @@ msgstr "Никакого дополнительного программного msgid "The following software patterns are selected for installation:" msgstr "Для установки выбраны следующие образцы программного обеспечения:" -#: src/components/software/SoftwarePage.jsx:165 +#: src/components/software/SoftwarePage.jsx:105 +#: src/components/software/SoftwarePage.jsx:119 msgid "Selected patterns" msgstr "Выбранные шаблоны" -#: src/components/software/SoftwarePage.jsx:168 +#: src/components/software/SoftwarePage.jsx:108 msgid "Change selection" msgstr "Изменить выбор" +#: src/components/software/SoftwarePage.jsx:123 +msgid "" +"This product does not allow to select software patterns during installation. " +"However, you can add additional software once the installation is finished." +msgstr "" +"Данный продукт не позволяет выбирать шаблоны программного обеспечения во " +"время установки. Однако Вы можете добавить дополнительное программное " +"обеспечение после завершения установки." + #: src/components/software/SoftwarePatternsSelection.jsx:223 msgid "auto selected" msgstr "автоматический выбор" @@ -942,8 +952,12 @@ msgid "Installation will take %s." msgstr "Установка займёт %s." #: src/components/software/UsedSize.jsx:37 -msgid "This space includes the base system and the selected software patterns." -msgstr "В этот объём входят основная система и выбранные шаблоны ПО." +msgid "" +"This space includes the base system and the selected software patterns, if " +"any." +msgstr "" +"Это пространство включает в себя базовую систему и выбранные шаблоны " +"программного обеспечения, если таковые имеются." #: src/components/storage/BootConfigField.jsx:43 msgid "Change boot options" @@ -1664,7 +1678,7 @@ msgstr[2] "Проверить %d запланированных действий msgid "Waiting for actions information..." msgstr "Ожидание информации о действиях..." -#: src/components/storage/ProposalPage.jsx:329 +#: src/components/storage/ProposalPage.jsx:317 msgid "Planned Actions" msgstr "Планируемые действия" @@ -1728,16 +1742,16 @@ msgstr "" #: src/components/storage/SpaceActionsTable.jsx:68 #, c-format msgid "Up to %s can be recovered by shrinking the device." -msgstr "" +msgstr "До %s можно освободить, сократив устройство." #: src/components/storage/SpaceActionsTable.jsx:77 msgid "The device cannot be shrunk:" -msgstr "" +msgstr "Устройство не может быть сокращено:" #: src/components/storage/SpaceActionsTable.jsx:98 #, c-format msgid "Show information about %s" -msgstr "" +msgstr "Показать сведения о %s" #: src/components/storage/SpaceActionsTable.jsx:172 msgid "The content may be deleted" diff --git a/web/po/sv.po b/web/po/sv.po index f069971438..c465032743 100644 --- a/web/po/sv.po +++ b/web/po/sv.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-14 02:32+0000\n" +"POT-Creation-Date: 2024-07-18 02:24+0000\n" "PO-Revision-Date: 2024-07-11 10:47+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish \n" "Language-Team: Turkish \n" "Language-Team: Ukrainian \n" "Language-Team: Chinese (Simplified) Date: Sun, 21 Jul 2024 02:53:25 +0000 Subject: [PATCH 262/430] Update translations in the product files Agama-weblate commit: 9aa82704acc632ca95b48fbae60571a1f5d0bbe7 --- products.d/sles_160.yaml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index e87e36b030..a982bdce54 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -5,13 +5,27 @@ name: SUSE Linux Enteprise Server 16.0 Alpha # at the at translations/description key below to avoid using obsolete # translations!! # ------------------------------------------------------------------------------ -description: "SUSE Linux Enterprise Server is the open, reliable, compliant, and future-proof - Linux Server choice that ensures the enterprise's business continuity. It is the secure and - adaptable OS for long-term supported, innovation-ready infrastructure running business-critical - workloads on-premises, in the cloud, and at the edge." +description: "SUSE Linux Enterprise Server is the open, reliable, compliant, and + future-proof Linux Server choice that ensures the enterprise's business + continuity. It is the secure and adaptable OS for long-term supported, + innovation-ready infrastructure running business-critical workloads + on-premises, in the cloud, and at the edge." # Do not manually change any translations! See README.md for more details. translations: description: + ca: El SUSE Linux Enterprise Server és l'opció de servidor Linux oberta, fiable, + compatible i a prova de futur que garanteix la continuïtat del negoci de + l'empresa. És el sistema operatiu segur i adaptable per a una + infraestructura amb suport a llarg termini i preparada per a la innovació + que executa càrregues de treball crítiques per a l'empresa a les + instal·lacions, al núvol, i a l'avantguarda. + cs: SUSE Linux Enterprise Server je otevřený, spolehlivý, kompatibilní a + perspektivní linuxový server, který zajišťuje kontinuitu činnosti podniku. + Je to bezpečný a přizpůsobivý operační systém pro dlouhodobě podporovanou + infrastrukturu připravenou na inovace, na které běží kritické podnikové + úlohy v lokálním prostředí, v cloudu i na okraji sítě. + ja: SUSE Linux Enterprise Server はオープンで信頼性が高く、各種の標準にも準拠し、将来性とビジネスの継続性を支援する Linux + サーバです。長期のサポートが提供されていることから安全性と順応性に優れ、オンプレミスからクラウド、エッジ環境に至るまで、様々な場所で重要なビジネス処理をこなすことのできる革新性の高いインフラストラクチャです。 software: installation_repositories: - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0/product/repo/SLES-Packages-16.0-x86_64/ From 7e550a7575436eb8e65fcf6a4ba6823b2e53b98b Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Sun, 21 Jul 2024 23:13:33 +0200 Subject: [PATCH 263/430] fix questions list to list only unanswered ones --- rust/agama-server/src/questions/web.rs | 68 +++++++++++++------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 7615c20911..63400e4070 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -25,7 +25,7 @@ use tokio_stream::{Stream, StreamExt}; use zbus::{ fdo::ObjectManagerProxy, names::{InterfaceName, OwnedInterfaceName}, - zvariant::{ObjectPath, OwnedObjectPath}, + zvariant::{ObjectPath, OwnedObjectPath, OwnedValue}, }; // TODO: move to lib or maybe not and just have in lib client for http API? @@ -34,6 +34,8 @@ struct QuestionsClient<'a> { connection: zbus::Connection, objects_proxy: ObjectManagerProxy<'a>, questions_proxy: Questions1Proxy<'a>, + generic_interface: OwnedInterfaceName, + with_password_interface: OwnedInterfaceName, } impl<'a> QuestionsClient<'a> { @@ -48,6 +50,14 @@ impl<'a> QuestionsClient<'a> { .destination("org.opensuse.Agama1")? .build() .await?, + generic_interface: InterfaceName::from_str_unchecked( + "org.opensuse.Agama1.Questions.Generic", + ) + .into(), + with_password_interface: InterfaceName::from_str_unchecked( + "org.opensuse.Agama1.Questions.WithPassword", + ) + .into(), }) } @@ -96,37 +106,39 @@ impl<'a> QuestionsClient<'a> { .await .context("failed to get managed object with Object Manager")?; let mut result: Vec = Vec::with_capacity(objects.len()); - let password_interface = OwnedInterfaceName::from( - InterfaceName::from_static_str("org.opensuse.Agama1.Questions.WithPassword") - .context("Failed to create interface name for question with password")?, - ); - for (path, interfaces_hash) in objects.iter() { - if interfaces_hash.contains_key(&password_interface) { - result.push(self.build_question_with_password(path).await?) - } else { - result.push(self.build_generic_question(path).await?) + + for (_path, interfaces_hash) in objects.iter() { + let generic_properties = interfaces_hash + .get(&self.generic_interface) + .context("Failed to create interface name for generic question")?; + // skip if question is already answered + let answer: String = get_property(generic_properties, "Answer")?; + if !answer.is_empty() { + continue; } + let mut question = self.build_generic_question(generic_properties)?; + + if interfaces_hash.contains_key(&self.with_password_interface) { + question.with_password = Some(QuestionWithPassword {}); + } + + result.push(question); } Ok(result) } - async fn build_generic_question( + fn build_generic_question( &self, - path: &OwnedObjectPath, + properties: &HashMap, ) -> Result { - let dbus_question = GenericQuestionProxy::builder(&self.connection) - .path(path)? - .cache_properties(zbus::CacheProperties::No) - .build() - .await?; let result = Question { generic: GenericQuestion { - id: dbus_question.id().await?, - class: dbus_question.class().await?, - text: dbus_question.text().await?, - options: dbus_question.options().await?, - default_option: dbus_question.default_option().await?, - data: dbus_question.data().await?, + id: get_property(properties, "Id")?, + class: get_property(properties, "Class")?, + text: get_property(properties, "Text")?, + options: get_property(properties, "Options")?, + default_option: get_property(properties, "DefaultOption")?, + data: get_property(properties, "Data")?, }, with_password: None, }; @@ -134,16 +146,6 @@ impl<'a> QuestionsClient<'a> { Ok(result) } - async fn build_question_with_password( - &self, - path: &OwnedObjectPath, - ) -> Result { - let mut result = self.build_generic_question(path).await?; - result.with_password = Some(QuestionWithPassword {}); - - Ok(result) - } - pub async fn delete(&self, id: u32) -> Result<(), ServiceError> { let question_path = ObjectPath::from( ObjectPath::try_from(format!("/org/opensuse/Agama1/Questions/{}", id)) From 9deacdb04be6d0e47c2ddad8a761a2165723ed36 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Sun, 21 Jul 2024 23:14:06 +0200 Subject: [PATCH 264/430] implement general Question with Dialog component --- web/src/client/questions.js | 2 +- .../questions/QuestionWithPassword.jsx | 69 +++++++++++++++++++ web/src/components/questions/Questions.jsx | 7 +- web/src/components/questions/index.js | 1 + 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 web/src/components/questions/QuestionWithPassword.jsx diff --git a/web/src/client/questions.js b/web/src/client/questions.js index 1999d39ba8..d2e822c238 100644 --- a/web/src/client/questions.js +++ b/web/src/client/questions.js @@ -150,4 +150,4 @@ class QuestionsClient { } } -export { QuestionsClient }; +export { QUESTION_TYPES, QuestionsClient }; diff --git a/web/src/components/questions/QuestionWithPassword.jsx b/web/src/components/questions/QuestionWithPassword.jsx new file mode 100644 index 0000000000..984f3c5918 --- /dev/null +++ b/web/src/components/questions/QuestionWithPassword.jsx @@ -0,0 +1,69 @@ +/* + * Copyright (c) [2022] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; +import { Alert, Form, FormGroup, Text } from "@patternfly/react-core"; +import { Icon } from "~/components/layout"; +import { PasswordInput, Popup } from "~/components/core"; +import { QuestionActions } from "~/components/questions"; +import { _ } from "~/i18n"; + + +export default function QuestionWithPassword({ question, answerCallback }) { + const [password, setPassword] = useState(question.password || ""); + const defaultAction = question.defaultOption; + + const actionCallback = (option) => { + question.password = password; + question.answer = option; + answerCallback(question); + }; + + return ( + } + > + {question.text} + + {/* TRANSLATORS: field label */} + + setPassword(value)} + /> + + + + + + + + ); +} \ No newline at end of file diff --git a/web/src/components/questions/Questions.jsx b/web/src/components/questions/Questions.jsx index 14be33362d..b2f786ef41 100644 --- a/web/src/components/questions/Questions.jsx +++ b/web/src/components/questions/Questions.jsx @@ -22,8 +22,9 @@ import React, { useCallback, useEffect, useState } from "react"; import { useInstallerClient } from "~/context/installer"; import { useCancellablePromise } from "~/utils"; +import { QUESTION_TYPES } from "~/client/questions"; -import { GenericQuestion, LuksActivationQuestion } from "~/components/questions"; +import { GenericQuestion, QuestionWithPassword, LuksActivationQuestion } from "~/components/questions"; export default function Questions() { const client = useInstallerClient(); @@ -73,6 +74,10 @@ export default function Questions() { // Renders the first pending question const [currentQuestion] = pendingQuestions; let QuestionComponent = GenericQuestion; + // show specialized popup for question which need password + if (currentQuestion.type === QUESTION_TYPES.withPassword) { + QuestionComponent = QuestionWithPassword; + } // show specialized popup for luks activation question // more can follow as it will be needed if (currentQuestion.class === "storage.luks_activation") { diff --git a/web/src/components/questions/index.js b/web/src/components/questions/index.js index 2e9b51dc81..15cc7dee28 100644 --- a/web/src/components/questions/index.js +++ b/web/src/components/questions/index.js @@ -21,5 +21,6 @@ export { default as QuestionActions } from "./QuestionActions"; export { default as GenericQuestion } from "./GenericQuestion"; +export { default as QuestionWithPassword } from "./QuestionWithPassword"; export { default as LuksActivationQuestion } from "./LuksActivationQuestion"; export { default as Questions } from "./Questions"; From 7858e4830edf6b5a3a0a331ce9a8982f47cdb43f Mon Sep 17 00:00:00 2001 From: Knut Anderssen Date: Mon, 22 Jul 2024 09:36:56 +0100 Subject: [PATCH 265/430] Move network to TanStack query --- web/src/client/network/index.js | 23 +- web/src/client/network/model.js | 7 + .../components/network/ConnectionsTable.jsx | 3 +- web/src/components/network/NetworkPage.jsx | 73 +---- .../components/network/WifiConnectionForm.jsx | 13 +- .../network/WifiNetworksListPage.jsx | 35 +-- .../components/network/WifiSelectorPage.jsx | 117 +------- web/src/components/network/routes.js | 36 +-- web/src/queries/network.js | 260 ++++++++++++++++++ 9 files changed, 338 insertions(+), 229 deletions(-) create mode 100644 web/src/queries/network.js diff --git a/web/src/client/network/index.js b/web/src/client/network/index.js index e7a4df7545..103d177482 100644 --- a/web/src/client/network/index.js +++ b/web/src/client/network/index.js @@ -26,6 +26,8 @@ import { ConnectionTypes, createAccessPoint, createConnection, + DeviceState, + NetworkState, securityFromFlags, } from "./model"; import { formatIp, ipPrefixFor } from "./utils"; @@ -239,7 +241,26 @@ class NetworkClient { return this.client.get(`/network/connections/${connection.id}/disconnect`); } - async loadNetworks(devices, connections, accessPoints) { + networkStateFor(state) { + switch (state) { + case DeviceState.CONFIG: + case DeviceState.IPCHECK: + // TRANSLATORS: Wifi network status + return NetworkState.CONNECTING; + case DeviceState.ACTIVATED: + // TRANSLATORS: Wifi network status + return NetworkState.CONNECTED; + case DeviceState.DEACTIVATING: + case DeviceState.FAILED: + case DeviceState.DISCONNECTED: + // TRANSLATORS: Wifi network status + return NetworkState.DISCONNECTED; + default: + return ""; + } + } + + loadNetworks(devices, connections, accessPoints) { const knownSsids = []; return accessPoints diff --git a/web/src/client/network/model.js b/web/src/client/network/model.js index 14aa4b31bd..d28fcc849f 100644 --- a/web/src/client/network/model.js +++ b/web/src/client/network/model.js @@ -49,6 +49,12 @@ const DeviceState = Object.freeze({ FAILED: "failed", }); +const NetworkState = Object.freeze({ + DISCONNECTED: "disconnected", + CONNECTING: "connecting", + CONNECTED: "connected" +}); + /** * Returns a human readable connection state * @@ -302,6 +308,7 @@ export { createConnection, createDevice, DeviceState, + NetworkState, securityFromFlags, SecurityProtocols, }; diff --git a/web/src/components/network/ConnectionsTable.jsx b/web/src/components/network/ConnectionsTable.jsx index e925e3350c..22616074d7 100644 --- a/web/src/components/network/ConnectionsTable.jsx +++ b/web/src/components/network/ConnectionsTable.jsx @@ -64,7 +64,8 @@ export default function ConnectionsTable({ connections, devices, onForget }) { {_("Name")} {/* TRANSLATORS: table header */} {_("IP addresses")} - + {/* TRANSLATORS: table header aria label */} + diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index dc99f2d296..5f926da4d6 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -21,58 +21,21 @@ // @ts-check -import React, { useCallback, useEffect, useState } from "react"; -import { CardBody, Grid, GridItem, Skeleton, Split, Stack } from "@patternfly/react-core"; -import { useLoaderData } from "react-router-dom"; +import React from "react"; +import { CardBody, Grid, GridItem } from "@patternfly/react-core"; import { ButtonLink, CardField, EmptyState, Page } from "~/components/core"; import { ConnectionsTable } from "~/components/network"; -import { NetworkEventTypes } from "~/client/network"; -import { useInstallerClient } from "~/context/installer"; import { _ } from "~/i18n"; import { formatIp } from "~/client/network/utils"; import { sprintf } from "sprintf-js"; +import { useNetwork, useNetworkConfigChanges } from "~/queries/network"; /** * Page component holding Network settings * @component */ export default function NetworkPage() { - const { network: client } = useInstallerClient(); - // @ts-ignore - const { connections: initialConnections, devices: initialDevices, settings } = useLoaderData(); - const [connections, setConnections] = useState(initialConnections); - const [devices, setDevices] = useState(initialDevices); - const [updateState, setUpdateState] = useState(false); - - const fetchState = useCallback(async () => { - const devices = await client.devices(); - const connections = await client.connections(); - setDevices(devices); - setConnections(connections); - }, [client]); - - useEffect(() => { - if (!updateState) return; - - setUpdateState(false); - fetchState(); - }, [fetchState, updateState]); - - useEffect(() => { - return client.onNetworkChange(({ type }) => { - if ( - [ - NetworkEventTypes.DEVICE_ADDED, - NetworkEventTypes.DEVICE_UPDATED, - NetworkEventTypes.DEVICE_REMOVED, - // @ts-ignore - ].includes(type) - ) { - setUpdateState(true); - } - }); - }); - + const { connections, devices, settings } = useNetwork(); const connectionDevice = ({ id }) => devices?.find(({ connection }) => id === connection); const connectionAddresses = (connection) => { const device = connectionDevice(connection); @@ -80,8 +43,7 @@ export default function NetworkPage() { return addresses?.map(formatIp).join(", "); }; - - const ready = connections !== undefined && devices !== undefined; + useNetworkConfigChanges(); const WifiConnections = () => { const { wireless_enabled: wifiAvailable } = settings; @@ -132,22 +94,6 @@ export default function NetworkPage() { ); }; - const SectionSkeleton = () => ( - - - - - - - - - - - - - - ); - const WiredConnections = () => { const wiredConnections = connections.filter((c) => !c.wireless); const total = wiredConnections.length; @@ -155,13 +101,8 @@ export default function NetworkPage() { return ( 0 && _("Wired")}> - {!ready && } - {ready && total === 0 && ( - - )} - {ready && total !== 0 && ( - - )} + {total === 0 && ()} + {total !== 0 && ()} ); diff --git a/web/src/components/network/WifiConnectionForm.jsx b/web/src/components/network/WifiConnectionForm.jsx index d4c37e7543..cd745352cf 100644 --- a/web/src/components/network/WifiConnectionForm.jsx +++ b/web/src/components/network/WifiConnectionForm.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { ActionGroup, Alert, @@ -32,7 +32,11 @@ import { } from "@patternfly/react-core"; import { PasswordInput } from "~/components/core"; import { useInstallerClient } from "~/context/installer"; +import { useNetwork, useNetworkConfigChanges, useSelectedWifiChange } from "~/queries/network"; import { _ } from "~/i18n"; +import { NetworkEventTypes } from "~/client/network"; +import { DeviceState } from "~/client/network/model"; +import { QueryClient, useQueryClient } from "@tanstack/react-query"; /* * FIXME: it should be moved to the SecurityProtocols enum that already exists or to a class based @@ -57,6 +61,8 @@ const securityFrom = (supported) => { export default function WifiConnectionForm({ network, onCancel, onSubmitCallback }) { const { network: client } = useInstallerClient(); + const queryClient = useQueryClient(); + const { networks } = useNetwork(); const [error, setError] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [ssid, setSsid] = useState(network?.ssid || ""); @@ -64,6 +70,8 @@ export default function WifiConnectionForm({ network, onCancel, onSubmitCallback const [security, setSecurity] = useState(securityFrom(network?.security || [])); const hidden = network?.hidden || false; + useNetworkConfigChanges(); + const accept = async (e) => { e.preventDefault(); setError(false); @@ -76,7 +84,8 @@ export default function WifiConnectionForm({ network, onCancel, onSubmitCallback client .addAndConnectTo(ssid, { security, password, hidden }) .catch(() => setError(true)) - .finally(() => setIsConnecting(false)); + .finally(() => setIsConnecting(false) && queryClient.invalidateQueries({ "queryKey": ["network"] })); + }; return ( diff --git a/web/src/components/network/WifiNetworksListPage.jsx b/web/src/components/network/WifiNetworksListPage.jsx index b0b21f0f31..d00b4cd2b9 100644 --- a/web/src/components/network/WifiNetworksListPage.jsx +++ b/web/src/components/network/WifiNetworksListPage.jsx @@ -45,12 +45,13 @@ import { } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { WifiConnectionForm } from "~/components/network"; -import { ButtonLink, EmptyState } from "~/components/core"; +import { ButtonLink } from "~/components/core"; import { DeviceState } from "~/client/network/model"; import { useInstallerClient } from "~/context/installer"; import { _ } from "~/i18n"; import { formatIp } from "~/client/network/utils"; -import { sprintf } from "sprintf-js"; +import { useQueryClient } from "@tanstack/react-query"; +import { useSelectedWifi, useSelectedWifiChange } from "~/queries/network"; const HIDDEN_NETWORK = Object.freeze({ hidden: true }); @@ -85,11 +86,13 @@ const ConnectionData = ({ network }) => { return {connectionAddresses(network)}; }; -const WifiDrawerPanelBody = ({ network, onCancel, onForget }) => { +const WifiDrawerPanelBody = ({ network, onCancel }) => { const client = useInstallerClient(); + const queryClient = useQueryClient(); + const { data } = useSelectedWifi(); const forgetNetwork = async () => { await client.network.deleteConnection(network.settings.id); - onForget(); + queryClient.invalidateQueries({ queryKey: ["network", "connections"] }) }; if (!network) return; @@ -98,6 +101,8 @@ const WifiDrawerPanelBody = ({ network, onCancel, onForget }) => { if (network === HIDDEN_NETWORK) return
      ; + if (data.needsAuth) return ; + if (network.settings && !network.device) { return ( @@ -180,22 +185,21 @@ const NetworkListName = ({ network }) => { * @param {function} props.onSelectionCallback - the function to trigger when user selects a network * @param {function} props.onCancelCallback - the function to trigger when user cancel dismiss before connecting to a network */ -function WifiNetworksListPage({ - selected, - onSelectionChange, - networks = [], - forceUpdateNetworksCallback = () => {}, -}) { - const selectHiddenNetwork = () => { - onSelectionChange(HIDDEN_NETWORK); +function WifiNetworksListPage({ networks = [] }) { + const { data } = useSelectedWifi(); + const selected = data.ssid === undefined ? HIDDEN_NETWORK : networks.find(n => n.ssid === data.ssid); + const changeSelected = useSelectedWifiChange(); + + const selectHiddneNetwork = () => { + changeSelected.mutate({ ssid: undefined, needsAuth: null }); }; const selectNetwork = (ssid) => { - onSelectionChange(networks.find((n) => n.ssid === ssid)); + changeSelected.mutate({ ssid, needsAuth: null }); }; const unselectNetwork = () => { - onSelectionChange(undefined); + changeSelected.mutate({ ssid: null, needsAuth: null }); }; const renderElements = () => { @@ -246,7 +250,6 @@ function WifiNetworksListPage({ @@ -261,7 +264,7 @@ function WifiNetworksListPage({ > {renderElements()} - diff --git a/web/src/components/network/WifiSelectorPage.jsx b/web/src/components/network/WifiSelectorPage.jsx index 1f0a82efa0..ed01d6fe5d 100644 --- a/web/src/components/network/WifiSelectorPage.jsx +++ b/web/src/components/network/WifiSelectorPage.jsx @@ -19,116 +19,20 @@ * find current contact information at www.suse.com. */ -import React, { useCallback, useEffect, useState } from "react"; -import { useInstallerClient } from "~/context/installer"; -import { NetworkEventTypes } from "~/client/network"; +import React from "react"; import { Page } from "~/components/core"; import { WifiNetworksListPage } from "~/components/network"; import { _ } from "~/i18n"; -import { DeviceState } from "~/client/network/model"; -import { Grid, GridItem, Timestamp } from "@patternfly/react-core"; -import { useLoaderData } from "react-router-dom"; -import { useLocalStorage } from "~/utils"; +import { Grid, GridItem } from "@patternfly/react-core"; +import { useNetwork, useNetworkConfigChanges } from "~/queries/network"; const networksFromValues = (networks) => Object.values(networks).flat(); -const baseHiddenNetwork = { ssid: undefined, hidden: true }; // FIXME: use a reducer function WifiSelectorPage() { - const { network: client } = useInstallerClient(); - const { - connections: initialConnections, - devices: initialDevices, - accessPoints, - networks: initialNetworks, - } = useLoaderData(); - const [data, saveData] = useLocalStorage("agama-network", { selectedWifi: null }); - // Reevaluate how to keep the state in the future - const [selected, setSelected] = useState(data.selectedWifi); - const [networks, setNetworks] = useState(initialNetworks); - const [showHiddenForm, setShowHiddenForm] = useState(false); - const [devices, setDevices] = useState(initialDevices); - const [connections, setConnections] = useState(initialConnections); - const [activeNetwork, setActiveNetwork] = useState(null); - const [updateNetworks, setUpdateNetworks] = useState(false); - const [needAuth, setNeedAuth] = useState(null); - - const reloadNetworks = () => setUpdateNetworks(true); - - const selectNetwork = (network) => { - saveData({ selectedWifi: network }); - setSelected(network); - }; - - const switchSelectedNetwork = (network) => { - setSelected(network === baseHiddenNetwork); - }; - - const fetchNetworks = useCallback(async () => { - const devices = await client.devices(); - const connections = await client.connections(); - const networks = await client.loadNetworks(devices, connections, accessPoints); - setDevices(devices); - setConnections(connections); - setNetworks(networks); - }, [client, accessPoints]); - - useEffect(() => { - saveData(data); - }, [data, saveData]); - - useEffect(() => { - // Let's keep the selected network up to date after networks information is - // updated (e.g., if the network status change); - if (networks) { - setSelected((prev) => { - return networksFromValues(networks).find((n) => n.ssid === prev?.ssid); - }); - } - }, [networks]); - - useEffect(() => { - setActiveNetwork(networksFromValues(networks).find((d) => d.device)); - }, [networks]); - - useEffect(() => { - if (!updateNetworks) return; - - setUpdateNetworks(false); - fetchNetworks(); - }, [fetchNetworks, updateNetworks]); - - useEffect(() => { - return client.onNetworkChange(({ type, payload }) => { - switch (type) { - case NetworkEventTypes.DEVICE_ADDED: { - reloadNetworks(); - break; - } - - case NetworkEventTypes.DEVICE_UPDATED: { - const [name, data] = payload; - const current_device = devices.find((d) => d.name === name); - - if (data.state === DeviceState.FAILED) { - if (current_device && data.stateReason === 7) { - console.log(`FAILED Device ${name} updated' with data`, data); - setNeedAuth(current_device.connection); - } - } - - reloadNetworks(); - break; - } - - case NetworkEventTypes.DEVICE_REMOVED: { - reloadNetworks(); - break; - } - } - }); - }); + const { networks } = useNetwork(); + useNetworkConfigChanges(); return ( <> @@ -138,16 +42,7 @@ function WifiSelectorPage() { - + diff --git a/web/src/components/network/routes.js b/web/src/components/network/routes.js index 2627c6381f..48a8947469 100644 --- a/web/src/components/network/routes.js +++ b/web/src/components/network/routes.js @@ -23,35 +23,9 @@ import React from "react"; import { Page } from "~/components/core"; import NetworkPage from "./NetworkPage"; import IpSettingsForm from "./IpSettingsForm"; -import { createDefaultClient } from "~/client"; import WifiSelectorPage from "./WifiSelectorPage"; import { N_ } from "~/i18n"; -// FIXME: just to be discussed, most probably we should reading data directly in -// the component in order to get it subscribed to changes. -const client = await createDefaultClient(); - -const loaders = { - all: async () => { - const devices = await client.network.devices(); - const connections = await client.network.connections(); - const settings = await client.network.settings(); - return { connections, devices, settings }; - }, - connection: async ({ params }) => { - const connections = await client.network.connections(); - return connections.find((c) => c.id === params.id); - }, - wifis: async () => { - const connections = await client.network.connections(); - const devices = await client.network.devices(); - const accessPoints = await client.network.accessPoints(); - const networks = await client.network.loadNetworks(devices, connections, accessPoints); - - return { connections, devices, accessPoints, networks }; - }, -}; - const routes = { path: "/network", element: , @@ -60,18 +34,16 @@ const routes = { icon: "settings_ethernet", }, children: [ - { index: true, element: , loader: loaders.all }, + { index: true, element: }, { path: "connections/:id/edit", - element: , - loader: loaders.connection, + element: }, { path: "wifis", element: , - loader: loaders.wifis, - }, - ], + } + ] }; export default routes; diff --git a/web/src/queries/network.js b/web/src/queries/network.js new file mode 100644 index 0000000000..f476b72a89 --- /dev/null +++ b/web/src/queries/network.js @@ -0,0 +1,260 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { useQueryClient, useMutation, useSuspenseQuery, useSuspenseQueries } from "@tanstack/react-query"; +import { useInstallerClient } from "~/context/installer"; +import { DeviceState, createAccessPoint, securityFromFlags } from "~/client/network/model"; +import { ipPrefixFor } from "~/client/network/utils"; +/** + * Returns the device settings + * + * @param {object} device - device settings from the API server + * @return {Device} + */ +const fromApiDevice = (device) => { + const nameservers = (device?.ipConfig?.nameservers || []); + const { ipConfig = {}, ...dev } = device; + const routes4 = (ipConfig.routes4 || []).map((route) => { + const [ip, netmask] = route.destination.split("/"); + const destination = (netmask !== undefined) ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; + + return { ...route, destination }; + }); + + const routes6 = (ipConfig.routes6 || []).map((route) => { + const [ip, netmask] = route.destination.split("/"); + const destination = (netmask !== undefined) ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; + + return { ...route, destination }; + }); + + const addresses = (ipConfig.addresses || []).map((address) => { + const [ip, netmask] = address.split("/"); + if (netmask !== undefined) { + return { address: ip, prefix: ipPrefixFor(netmask) }; + } else { + return { address: ip }; + } + }); + + return { ...dev, ...ipConfig, addresses, nameservers, routes4, routes6 }; +}; + +const fromApiConnection = (connection) => { + const nameservers = (connection.nameservers || []); + const addresses = (connection.addresses || []).map((address) => { + const [ip, netmask] = address.split("/"); + if (netmask !== undefined) { + return { address: ip, prefix: ipPrefixFor(netmask) }; + } else { + return { address: ip }; + } + }); + + return { ...connection, addresses, nameservers }; +}; + +/** + * Returns a query for retrieving the network configuration + */ +const stateQuery = () => { + return { + queryKey: ["network", "state"], + queryFn: () => fetch("/api/network/state").then((res) => res.json()), + }; +}; + +/** + * Returns a query for retrieving the list of known devices + */ +const devicesQuery = () => ({ + queryKey: ["network", "devices"], + queryFn: async () => { + const response = await fetch("/api/network/devices"); + const devices = await response.json(); + + return devices.map(fromApiDevice); + }, + staleTime: Infinity +}); + +/** + * Returns a query for retrieving the list of known connections + */ +const connectionsQuery = () => ({ + queryKey: ["network", "connections"], + queryFn: async () => { + const response = await fetch("/api/network/connections"); + const connections = await response.json(); + return connections.map(fromApiConnection); + }, + staleTime: Infinity +}); + +/** + * Returns a query for retrieving the list of known access points + */ +const accessPointsQuery = () => ({ + queryKey: ["network", "accessPoints"], + queryFn: async () => { + const response = await fetch("/api/network/wifi"); + const json = await response.json(); + const access_points = json.map((ap) => { + return createAccessPoint({ + ssid: ap.ssid, + hwAddress: ap.hw_address, + strength: ap.strength, + security: securityFromFlags(ap.flags, ap.wpaFlags, ap.rsnFlags) + }); + }); + return access_points.sort((a, b) => (a.strength < b.strength) ? -1 : 1); + }, + staleTime: Infinity +}); + +/** + * Hook that builds a mutation to update a network connections + * + * It does not require to call `useMutation`. + */ +const useConnectionMutation = () => { + const query = { + mutationFn: (newConnection) => + fetch(`/api/network/connection/${newConnection.id}`, { + method: "PUT", + body: JSON.stringify(newConnection), + headers: { + "Content-Type": "application/json", + }, + }) + }; + return useMutation(query); +}; + +/** + * Returns selected Wi-Fi network + */ +const selectedWiFiNetworkQuery = () => ({ + // queryKey: ["network", "wifi", "selected"], + // TODO: use right key, once we stop invalidating everything under network + queryKey: ["wifi", "selected"], + queryFn: async () => { + return Promise.resolve({ ssid: null, needsAuth: null }); + }, + staleTime: Infinity +}); + +const useSelectedWifi = () => { + // TODO: evaluate if useSuspenseQuery is really needed, probably not. + return useSuspenseQuery(selectedWiFiNetworkQuery()); +} + +const useSelectedWifiChange = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (data) => Promise.resolve(data), + onSuccess: (data) => { + queryClient.setQueryData(["wifi", "selected"], (prev) => ({ ...prev, ...data })); + } + }); + + return mutation; +} + +/** + * Hook that returns a useEffect to listen for NetworkChanged events + * + * When the configuration changes, it invalidates the config query and forces the router to + * revalidate its data (executing the loaders again). + */ +const useNetworkConfigChanges = () => { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + const changeSelected = useSelectedWifiChange(); + + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + if (event.type === "NetworkChange") { + if (event.deviceRemoved || event.deviceAdded) { + queryClient.invalidateQueries({ queryKey: ["network"] }); + } + + if (event.deviceUpdated) { + const [name, data] = event.deviceUpdated; + const devices = queryClient.getQueryData(["network", "devices"]); + if (!devices) return; + + if (name !== data.name) { + return queryClient.invalidateQueries({ queryKey: ["network"] }); + } + + const current_device = devices.find((d) => d.name === name); + if ([DeviceState.DISCONNECTED, DeviceState.ACTIVATED].includes(data.state)) { + if (current_device.state !== data.state) { + return queryClient.invalidateQueries({ queryKey: ["network"] }); + } + } + if (data.state === DeviceState.FAILED) { + return changeSelected.mutate({ needsAuth: true }); + } + } + } + }); + }, [client, queryClient, changeSelected]); +}; + +const useNetwork = () => { + const [ + { data: state }, + { data: devices }, + { data: connections }, + { data: accessPoints } + ] = useSuspenseQueries({ + queries: [ + stateQuery(), + devicesQuery(), + connectionsQuery(), + accessPointsQuery() + ] + }); + const client = useInstallerClient(); + const networks = client.network.loadNetworks(devices, connections, accessPoints); + + return { connections, settings: state, devices, accessPoints, networks }; +}; + + +export { + stateQuery, + devicesQuery, + connectionsQuery, + accessPointsQuery, + selectedWiFiNetworkQuery, + useConnectionMutation, + useNetwork, + useSelectedWifi, + useSelectedWifiChange, + useNetworkConfigChanges +}; From 5e85d43a2748a4088a1d748acc9c4b5f363c46b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 22 Jul 2024 13:03:48 +0100 Subject: [PATCH 266/430] refactor(web): use queries to handle the first user --- web/src/components/users/FirstUser.jsx | 58 ++------- web/src/components/users/FirstUserForm.jsx | 38 ++---- web/src/queries/users.ts | 144 +++++++++++++++++++++ web/src/types/users.ts | 34 +++++ 4 files changed, 205 insertions(+), 69 deletions(-) create mode 100644 web/src/queries/users.ts create mode 100644 web/src/types/users.ts diff --git a/web/src/components/users/FirstUser.jsx b/web/src/components/users/FirstUser.jsx index d0677714c9..8a81b04cf2 100644 --- a/web/src/components/users/FirstUser.jsx +++ b/web/src/components/users/FirstUser.jsx @@ -20,13 +20,17 @@ */ import React, { useState, useEffect } from "react"; -import { Skeleton, Split, Stack } from "@patternfly/react-core"; +import { Split, Stack } from "@patternfly/react-core"; import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { useNavigate } from "react-router-dom"; import { RowActions, ButtonLink } from "~/components/core"; import { _ } from "~/i18n"; -import { useCancellablePromise } from "~/utils"; -import { useInstallerClient } from "~/context/installer"; +import { + useFirstUser, + useFirstUserChanges, + useFirstUserMutation, + useRemoveFirstUserMutation, +} from "~/queries/users"; const UserNotDefined = ({ actionCb }) => { return ( @@ -73,48 +77,14 @@ const UserData = ({ user, actions }) => { ); }; -const initialUser = { - userName: "", - fullName: "", - autologin: false, - password: "", -}; - export default function FirstUser() { - const client = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - const [user, setUser] = useState({}); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - cancellablePromise(client.users.getUser()).then((userValues) => { - setUser(userValues); - setIsLoading(false); - }); - }, [client.users, cancellablePromise]); - - useEffect(() => { - return client.users.onUsersChange((changes) => { - if (changes.firstUser !== undefined) { - setUser(changes.firstUser); - } - }); - }, [client.users]); - - const remove = async () => { - setIsLoading(true); - - const result = await client.users.removeUser(); + const { data: user } = useFirstUser(); + const removeUser = useRemoveFirstUserMutation(); + const navigate = useNavigate(); - if (result) { - setUser(initialUser); - setIsLoading(false); - } - }; + useFirstUserChanges(); const isUserDefined = user?.userName && user?.userName !== ""; - const navigate = useNavigate(); - const actions = [ { title: _("Edit"), @@ -122,14 +92,12 @@ export default function FirstUser() { }, { title: _("Discard"), - onClick: remove, + onClick: () => removeUser.mutate(), isDanger: true, }, ]; - if (isLoading) { - return ; - } else if (isUserDefined) { + if (isUserDefined) { return ; } else { return ; diff --git a/web/src/components/users/FirstUserForm.jsx b/web/src/components/users/FirstUserForm.jsx index 9915c2d980..d2f752a221 100644 --- a/web/src/components/users/FirstUserForm.jsx +++ b/web/src/components/users/FirstUserForm.jsx @@ -42,6 +42,7 @@ import { _ } from "~/i18n"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; import { suggestUsernames } from "~/components/users/utils"; +import { useFirstUser, useFirstUserMutation } from "~/queries/users"; const UsernameSuggestions = ({ isOpen = false, @@ -82,6 +83,8 @@ const UsernameSuggestions = ({ // close to the related input. // TODO: extract the suggestions logic. export default function FirstUserForm() { + const { data: firstUser } = useFirstUser(); + const setFirstUser = useFirstUserMutation(); const client = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); const [state, setState] = useState({}); @@ -96,24 +99,14 @@ export default function FirstUserForm() { const passwordRef = useRef(); useEffect(() => { - cancellablePromise(client.users.getUser()).then((userValues) => { - const editing = userValues.userName !== ""; - setState({ - load: true, - user: userValues, - isEditing: editing, - }); - setChangePassword(!editing); + const editing = firstUser.userName !== ""; + setState({ + load: true, + user: firstUser, + isEditing: editing, }); - }, [client.users, cancellablePromise]); - - useEffect(() => { - return client.users.onUsersChange(({ firstUser }) => { - if (firstUser !== undefined) { - setState({ ...state, user: firstUser }); - } - }); - }, [client.users, state]); + setChangePassword(!editing); + }, [firstUser]); useEffect(() => { if (showSuggestions) { @@ -152,13 +145,10 @@ export default function FirstUserForm() { return; } - const { result, issues = [] } = await client.users.setUser({ ...state.user, ...user }); - if (!result || issues.length) { - // FIXME: improve error handling. See client. - setErrors(issues.length ? issues : [_("Please, try again.")]); - } else { - navigate(".."); - } + setFirstUser + .mutateAsync({ ...state.user, ...user }) + .catch((e) => setErrors(e)) + .then(() => navigate("..")); }; const onSuggestionSelected = (suggestion) => { diff --git a/web/src/queries/users.ts b/web/src/queries/users.ts new file mode 100644 index 0000000000..4f5da5a4b2 --- /dev/null +++ b/web/src/queries/users.ts @@ -0,0 +1,144 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { QueryClient, useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { useInstallerClient } from "~/context/installer"; +import { _ } from "~/i18n"; +import { FirstUser, RootUser } from "~/types/users"; + +/** + * Returns a query for retrieving the first user configuration + */ +const firstUserQuery = () => ({ + queryKey: ["users", "firstUser"], + queryFn: () => fetch("/api/users/first").then((res) => res.json()), +}); + +/** + * Hook that returns the first user. + */ +const useFirstUser = () => useSuspenseQuery(firstUserQuery()); + +/* + * Hook that returns a mutation to change the first user. + */ +const useFirstUserMutation = () => { + const queryClient = useQueryClient(); + const query = { + mutationFn: (user: FirstUser) => + fetch("/api/users/first", { + method: "PUT", + body: JSON.stringify({ ...user, data: {} }), + headers: { + "Content-Type": "application/json", + }, + }).then((response) => { + if (response.ok) { + return response.json(); + } else { + throw new Error(_("Please, try again")); + } + }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users", "firstUser"] }), + }; + return useMutation(query); +}; + +const useRemoveFirstUserMutation = () => { + const queryClient = useQueryClient(); + const query = { + mutationFn: () => + fetch("/api/users/first", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users", "firstUser"] }); + }, + }; + return useMutation(query); +}; + +/** + * Listens for first user changes. + */ +const useFirstUserChanges = () => { + const client = useInstallerClient(); + const queryClient = useQueryClient(); + + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + if (event.type === "FirstUserChanged") { + const { fullName, userName, password, autologin, data } = event; + queryClient.setQueryData(["users", "firstUser"], { + fullName, + userName, + password, + autologin, + data, + }); + } + }); + }); +}; + +/** + * Returns a query for retrieving the root user configuration. + */ +const rootUserQuery = () => ({ + queryKey: ["users", "root"], + queryFn: () => fetch("/api/users/root").then((res) => res.json()), +}); + +const useRootUser = () => useSuspenseQuery(rootUserQuery()); + +/* + * Hook that returns a mutation to change the root user configuration. + */ +const useRootUserMutation = () => { + const queryClient = new QueryClient(); + const query = { + mutationFn: (root: RootUser) => + fetch("/api/users/root", { + method: "PATCH", + body: JSON.stringify(root), + headers: { + "Content-Type": "application/json", + }, + }), + success: queryClient.invalidateQueries({ queryKey: ["users", "firstUser"] }), + }; + return useMutation(query); +}; + +export { + useFirstUser, + useFirstUserMutation, + useRemoveFirstUserMutation, + useRootUser, + useRootUserMutation, + useFirstUserChanges, +}; diff --git a/web/src/types/users.ts b/web/src/types/users.ts new file mode 100644 index 0000000000..7fce70b720 --- /dev/null +++ b/web/src/types/users.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +type FirstUser = { + fullName: string; + userName: string; + password: string; + autologin: boolean; +}; + +type RootUser = { + password: string | null; + sshkey: string | null; +}; + +export type { FirstUser, RootUser }; From e5b5cc3df58af41cf5cde6b84c7b9c782465d9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 22 Jul 2024 14:10:24 +0100 Subject: [PATCH 267/430] refactor(web): use queries to handle root auth --- web/src/components/users/RootAuthMethods.jsx | 53 ++++--------------- .../components/users/RootPasswordPopup.jsx | 5 +- web/src/components/users/RootSSHKeyPopup.jsx | 6 +-- web/src/queries/users.ts | 44 ++++++++++++--- web/src/types/users.ts | 10 +++- 5 files changed, 62 insertions(+), 56 deletions(-) diff --git a/web/src/components/users/RootAuthMethods.jsx b/web/src/components/users/RootAuthMethods.jsx index c34cf4b5fe..ff24fa5076 100644 --- a/web/src/components/users/RootAuthMethods.jsx +++ b/web/src/components/users/RootAuthMethods.jsx @@ -28,6 +28,7 @@ import { RootPasswordPopup, RootSSHKeyPopup } from "~/components/users"; import { _ } from "~/i18n"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; +import { useRootUser, useRootUserChanges, useRootUserMutation } from "~/queries/users"; const MethodsNotDefined = ({ setPassword, setSSHKey }) => { return ( @@ -54,42 +55,17 @@ const MethodsNotDefined = ({ setPassword, setSSHKey }) => { ); }; export default function RootAuthMethods() { - const { users: client } = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - const [sshKey, setSSHKey] = useState(""); - const [isPasswordDefined, setIsPasswordDefined] = useState(false); + const setRootUser = useRootUserMutation(); const [isSSHKeyFormOpen, setIsSSHKeyFormOpen] = useState(false); const [isPasswordFormOpen, setIsPasswordFormOpen] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const loadData = async () => { - try { - const isPasswordSet = await cancellablePromise(client.isRootPasswordSet()); - const sshKey = await cancellablePromise(client.getRootSSHKey()); - - setIsPasswordDefined(isPasswordSet); - setSSHKey(sshKey); - } catch (error) { - // TODO: handle/display errors - console.log(error); - } finally { - setIsLoading(false); - } - }; - - loadData(); - }, [client, cancellablePromise]); - - useEffect(() => { - return client.onUsersChange((changes) => { - if (changes.rootPasswordSet !== undefined) setIsPasswordDefined(changes.rootPasswordSet); - if (changes.rootSSHKey !== undefined) setSSHKey(changes.rootSSHKey); - }); - }, [client]); - const isSSHKeyDefined = sshKey !== ""; + const { + data: { password: isPasswordDefined, sshkey: sshKey }, + } = useRootUser(); + + useRootUserChanges(); + const isSSHKeyDefined = sshKey !== ""; const openPasswordForm = () => setIsPasswordFormOpen(true); const openSSHKeyForm = () => setIsSSHKeyFormOpen(true); const closePasswordForm = () => setIsPasswordFormOpen(false); @@ -102,7 +78,7 @@ export default function RootAuthMethods() { }, isPasswordDefined && { title: _("Discard"), - onClick: () => client.removeRootPassword(), + onClick: () => setRootUser.mutate({ password: "" }), isDanger: true, }, ].filter(Boolean); @@ -114,20 +90,11 @@ export default function RootAuthMethods() { }, sshKey && { title: _("Discard"), - onClick: () => client.setRootSSHKey(""), + onClick: () => setRootUser.mutate({ sshkey: "" }), isDanger: true, }, ].filter(Boolean); - if (isLoading) { - return ( - <> - - - - ); - } - const PasswordLabel = () => { return isPasswordDefined ? _("Already set") : _("Not set"); }; diff --git a/web/src/components/users/RootPasswordPopup.jsx b/web/src/components/users/RootPasswordPopup.jsx index ea7cdd12b9..b696ae4acf 100644 --- a/web/src/components/users/RootPasswordPopup.jsx +++ b/web/src/components/users/RootPasswordPopup.jsx @@ -27,6 +27,7 @@ import { PasswordAndConfirmationInput, Popup } from "~/components/core"; import { _ } from "~/i18n"; import { useInstallerClient } from "~/context/installer"; +import { useRootUser, useRootUserMutation } from "~/queries/users"; /** * A dialog holding the form to change the root password @@ -41,7 +42,7 @@ import { useInstallerClient } from "~/context/installer"; * @param {function} props.onClose - the function to be called when the dialog is closed */ export default function RootPasswordPopup({ title = _("Root password"), isOpen, onClose }) { - const { users: client } = useInstallerClient(); + const setRootUser = useRootUserMutation(); const [password, setPassword] = useState(""); const [isValidPassword, setIsValidPassword] = useState(true); const passwordRef = useRef(); @@ -54,7 +55,7 @@ export default function RootPasswordPopup({ title = _("Root password"), isOpen, const accept = async (e) => { e.preventDefault(); // TODO: handle errors - if (password !== "") await client.setRootPassword(password); + if (password !== "") await setRootUser.mutateAsync({ password }); close(); }; diff --git a/web/src/components/users/RootSSHKeyPopup.jsx b/web/src/components/users/RootSSHKeyPopup.jsx index cd444f5bf4..8e8b9b48b6 100644 --- a/web/src/components/users/RootSSHKeyPopup.jsx +++ b/web/src/components/users/RootSSHKeyPopup.jsx @@ -24,7 +24,7 @@ import { Form, FormGroup, FileUpload } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { Popup } from "~/components/core"; -import { useInstallerClient } from "~/context/installer"; +import { useRootUserMutation } from "~/queries/users"; /** * A dialog holding the form to set the SSH Public key for root @@ -45,7 +45,7 @@ export default function RootSSHKeyPopup({ isOpen, onClose, }) { - const client = useInstallerClient(); + const setRootUser = useRootUserMutation(); const [isLoading, setIsLoading] = useState(false); const [sshKey, setSSHKey] = useState(currentKey); @@ -60,7 +60,7 @@ export default function RootSSHKeyPopup({ const accept = async (e) => { e.preventDefault(); - client.users.setRootSSHKey(sshKey); + await setRootUser.mutateAsync({ sshkey: sshKey }); // TODO: handle/display errors close(); }; diff --git a/web/src/queries/users.ts b/web/src/queries/users.ts index 4f5da5a4b2..6ec88957a0 100644 --- a/web/src/queries/users.ts +++ b/web/src/queries/users.ts @@ -23,7 +23,7 @@ import React from "react"; import { QueryClient, useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { _ } from "~/i18n"; -import { FirstUser, RootUser } from "~/types/users"; +import { FirstUser, RootUser, RootUserChanges } from "~/types/users"; /** * Returns a query for retrieving the first user configuration @@ -119,26 +119,58 @@ const useRootUser = () => useSuspenseQuery(rootUserQuery()); * Hook that returns a mutation to change the root user configuration. */ const useRootUserMutation = () => { - const queryClient = new QueryClient(); + const queryClient = useQueryClient(); const query = { - mutationFn: (root: RootUser) => + mutationFn: (changes: Partial) => fetch("/api/users/root", { method: "PATCH", - body: JSON.stringify(root), + body: JSON.stringify({ ...changes, passwordEncrypted: false }), headers: { "Content-Type": "application/json", }, }), - success: queryClient.invalidateQueries({ queryKey: ["users", "firstUser"] }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users", "root"] }), }; return useMutation(query); }; +/** + * Listens for first user changes. + */ +const useRootUserChanges = () => { + const client = useInstallerClient(); + const queryClient = useQueryClient(); + + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + console.log("event.type", event.type); + if (event.type === "RootChanged") { + const { password, sshkey } = event; + queryClient.setQueryData(["users", "root"], (oldRoot: RootUser) => { + const newRoot = { ...oldRoot }; + if (password !== undefined) { + newRoot.password = password; + } + + if (sshkey) { + newRoot.sshkey = sshkey; + } + + return newRoot; + }); + } + }); + }); +}; + export { useFirstUser, + useFirstUserChanges, useFirstUserMutation, useRemoveFirstUserMutation, useRootUser, + useRootUserChanges, useRootUserMutation, - useFirstUserChanges, }; diff --git a/web/src/types/users.ts b/web/src/types/users.ts index 7fce70b720..bd88452d4c 100644 --- a/web/src/types/users.ts +++ b/web/src/types/users.ts @@ -27,8 +27,14 @@ type FirstUser = { }; type RootUser = { - password: string | null; + password: boolean; sshkey: string | null; }; -export type { FirstUser, RootUser }; +type RootUserChanges = { + password: string; + passwordEncrypted: boolean; + sshkey: string; +}; + +export type { FirstUser, RootUserChanges, RootUser }; From 62621564ca50897ae637b7fceb0851805e7217fd Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 22 Jul 2024 15:28:18 +0200 Subject: [PATCH 268/430] fix path for deleting question --- rust/agama-lib/src/questions/http_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/agama-lib/src/questions/http_client.rs b/rust/agama-lib/src/questions/http_client.rs index e498fce21e..0be917d658 100644 --- a/rust/agama-lib/src/questions/http_client.rs +++ b/rust/agama-lib/src/questions/http_client.rs @@ -63,7 +63,7 @@ impl HTTPClient { } pub async fn delete_question(&self, question_id: u32) -> Result<(), ServiceError> { - let path = format!("/questions/{}/answer", question_id); + let path = format!("/questions/{}", question_id); self.client.delete(path.as_str()).await } } From 78d997c30979cadece596d3ab2514b8716c174cb Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 22 Jul 2024 15:50:31 +0200 Subject: [PATCH 269/430] fix parsing of result --- service/lib/agama/autoyast/report_patching.rb | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb index a102ac1cba..36f4194862 100644 --- a/service/lib/agama/autoyast/report_patching.rb +++ b/service/lib/agama/autoyast/report_patching.rb @@ -49,17 +49,17 @@ def show(message, details: "", headline: "", timeout: 0, focus: nil, question = { # TODO: id for newly created question is ignored, but maybe it will # be better to not have to specify it at all? - id: 0, - class: "autoyast.popup", - text: text, - options: generate_options(buttons), - default_option: focus || options.first, - data: {} + id: 0, + class: "autoyast.popup", + text: text, + options: generate_options(buttons), + defaultOption: focus || options.first, + data: {} } data = { generic: question }.to_json - answer_json = Yast::Execute.locally!("agama", "questions", "ask", + answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) - answer = JSON.parse!(answer_json) + answer = JSON.parse!(answer_yaml) answer["generic"]["answer"].to_sym end @@ -106,14 +106,14 @@ def run "defaultOption" => "cancel", "data" => {} } - data = { "generic" => question, "withPassword" => {} }.to_json - answer_json = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, + data = { generic: question, withPassword: {} }.to_json + answer_yaml = Yast::Execute.locally!("agama", "questions", "ask", stdin: data, stdout: :capture) - answer = JSON.parse!(answer_json) + answer = JSON.parse!(answer_yaml) result = answer["generic"]["answer"].to_sym return nil if result == "cancel" - answer["with_password"]["password"] + answer["withPassword"]["password"] end end end From 7879a7c938ced061e6e075b6c7e974af4a8a9885 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 22 Jul 2024 15:50:56 +0200 Subject: [PATCH 270/430] Apply suggestions from code review Co-authored-by: Martin Vidner --- service/lib/agama/autoyast/report_patching.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb index 36f4194862..0ecfcb3edf 100644 --- a/service/lib/agama/autoyast/report_patching.rb +++ b/service/lib/agama/autoyast/report_patching.rb @@ -111,7 +111,7 @@ def run stdout: :capture) answer = JSON.parse!(answer_yaml) result = answer["generic"]["answer"].to_sym - return nil if result == "cancel" + return nil if result == :cancel answer["withPassword"]["password"] end From b7e1a9e660bb5c3099469bac9f1eeb920083ad72 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 22 Jul 2024 17:16:00 +0200 Subject: [PATCH 271/430] do not require id for newly created question --- rust/agama-cli/src/questions.rs | 7 +++++-- rust/agama-lib/src/questions/model.rs | 3 ++- rust/agama-server/src/questions/web.rs | 6 +++--- service/lib/agama/autoyast/report_patching.rb | 6 ------ 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index 7dfe1ae709..fa2ef1d34b 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -72,11 +72,14 @@ async fn ask_question() -> Result<(), ServiceError> { let question = serde_json::from_reader(std::io::stdin())?; let created_question = client.create_question(&question).await?; - let answer = client.get_answer(created_question.generic.id).await?; + let Some(id) = created_question.generic.id else { + return Err(ServiceError::QuestionNotExist(0)); + }; + let answer = client.get_answer(id).await?; let answer_json = serde_json::to_string_pretty(&answer).map_err(Into::::into)?; println!("{}", answer_json); - client.delete_question(created_question.generic.id).await?; + client.delete_question(id).await?; Ok(()) } diff --git a/rust/agama-lib/src/questions/model.rs b/rust/agama-lib/src/questions/model.rs index 6d1a87d72f..df35c0763f 100644 --- a/rust/agama-lib/src/questions/model.rs +++ b/rust/agama-lib/src/questions/model.rs @@ -19,7 +19,8 @@ pub struct Question { #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct GenericQuestion { - pub id: u32, + /// id is optional as newly created questions does not have it assigned + pub id: Option, pub class: String, pub text: String, pub options: Vec, diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index 63400e4070..f7054193ee 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -94,8 +94,8 @@ impl<'a> QuestionsClient<'a> { .await? }; let mut res = question.clone(); - res.generic.id = extract_id_from_path(&path)?; - tracing::info!("new question gets id {}", res.generic.id); + res.generic.id = Some(extract_id_from_path(&path)?); + tracing::info!("new question gets id {:?}", res.generic.id); Ok(res) } @@ -133,7 +133,7 @@ impl<'a> QuestionsClient<'a> { ) -> Result { let result = Question { generic: GenericQuestion { - id: get_property(properties, "Id")?, + id: Some(get_property(properties, "Id")?), class: get_property(properties, "Class")?, text: get_property(properties, "Text")?, options: get_property(properties, "Options")?, diff --git a/service/lib/agama/autoyast/report_patching.rb b/service/lib/agama/autoyast/report_patching.rb index 0ecfcb3edf..79f37943f2 100644 --- a/service/lib/agama/autoyast/report_patching.rb +++ b/service/lib/agama/autoyast/report_patching.rb @@ -47,9 +47,6 @@ def show(message, details: "", headline: "", timeout: 0, focus: nil, text += "\n\n" + details unless details.empty? options = generate_options(buttons) question = { - # TODO: id for newly created question is ignored, but maybe it will - # be better to not have to specify it at all? - id: 0, class: "autoyast.popup", text: text, options: generate_options(buttons), @@ -97,9 +94,6 @@ def run # at first construct agama question to display. text = @label question = { - # TODO: id for newly created question is ignored, but maybe it will - # be better to not have to specify it at all? - "id" => 0, "class" => "autoyast.password", "text" => text, "options" => ["ok", "cancel"], From 85a71598e6b2bb2e322166f20d4b917f093f0ff3 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Mon, 22 Jul 2024 17:29:09 +0200 Subject: [PATCH 272/430] changes --- rust/package/agama.changes | 7 +++++++ service/package/rubygem-agama-yast.changes | 6 ++++++ web/package/agama-web-ui.changes | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 970368fce3..422153939d 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Mon Jul 22 15:27:44 UTC 2024 - Josef Reidinger + +- Fix `agama questions list` to list only unaswered questions and + improve its performance + (gh#openSUSE/agama#1476) + ------------------------------------------------------------------- Wed Jul 17 11:15:33 UTC 2024 - Jorik Cronenberg diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 5defe09148..35da1c5e61 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Jul 22 15:26:48 UTC 2024 - Josef Reidinger + +- Autoyast convert script: Use agama questions to report errors + and ask when encrypted profile is used (gh#openSUSE/agama#1476) + ------------------------------------------------------------------- Fri Jul 12 11:03:14 UTC 2024 - Imobach Gonzalez Sosa diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 9bbd0fac95..2b3de5b9e8 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Jul 22 15:28:42 UTC 2024 - Josef Reidinger + +- Add support for generic questions with password + (gh#openSUSE/agama#1476) + ------------------------------------------------------------------- Wed Jul 17 09:52:36 UTC 2024 - Imobach Gonzalez Sosa From 285e9fbffee6e6d71436513be6d807f68c77f8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 22 Jul 2024 10:02:11 +0200 Subject: [PATCH 273/430] Updated CI integration test --- .github/workflows/ci-integration-tests.yml | 33 +++++++++++++--------- puppeteer/agama-integration-tests | 20 ++++++++++--- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 5e8bbc944a..990d52c049 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -29,17 +29,20 @@ jobs: fetch-tags: true - name: Created shared YaST log directory - run: mkdir -p /tmp/log/YaST2 + run: mkdir -p /tmp/log/YaST2 /tmp/log/puppeteer - name: Start container - run: podman run --privileged --detach --name agama --ipc=host -e CI -e GITHUB_ACTIONS -v /dev:/dev -v .:/checkout -v /tmp/log/YaST2:/var/log/YaST2 registry.opensuse.org/systemsmanagement/agama/staging/containers/opensuse/agama-testing:latest + run: podman run --privileged --detach --name agama --ipc=host -e CI -e GITHUB_ACTIONS -v /dev:/dev -v .:/checkout -v /tmp/log/puppeteer:/var/log/puppeteer -v /tmp/log/YaST2:/var/log/YaST2 registry.opensuse.org/systemsmanagement/agama/devel/containers/opensuse/agama-testing:latest - name: Environment run: podman exec agama bash -c "env | sort" - - name: Set a testing Agama configuration - # delete all products except TW to skip the product selection at the beginning - run: podman exec agama bash -c "ls /checkout/products.d/*.yaml | grep -v tumbleweed.yaml | xargs rm" + - name: Packages + run: podman exec agama bash -c "rpm -qa | sort" + + - name: Set the root password + # this allows to login into Agama + run: podman exec agama bash -c "echo linux | passwd --stdin" - name: Build the frontend run: podman exec agama bash -c "cd /checkout; ./setup-web.sh" @@ -68,13 +71,16 @@ jobs: - name: Run the Agama smoke test run: podman exec agama curl http://localhost - - name: Check Playwright version - run: podman exec agama playwright --version + - name: Run the Puppeteer tests + # update the test file and the runner script from git + run: podman exec agama bash -c + "cp /checkout/puppeteer/tests/test_root_password.js /usr/share/agama/integration-tests/tests && + cp /checkout/puppeteer/agama-integration-tests /usr/bin/agama-integration-tests" - - name: Run the Playwright tests - # user authentication is not required when cockpit runs a local session - # run the tests in the Chromium browser - run: podman exec agama bash -c "cd /checkout/playwright && SKIP_LOGIN=true playwright test --trace on --project chromium" + - name: Run the Puppeteer tests + # run the test + run: podman exec agama bash -c "cd /var/log/puppeteer && + agama-integration-tests /usr/share/agama/integration-tests/tests/test_root_password.js" - name: Again show the D-Bus services log # run even when any previous step fails @@ -82,15 +88,14 @@ jobs: run: podman exec agama journalctl - name: Upload the test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 # run even when any previous step fails if: always() with: name: test-results retention-days: 30 path: | - playwright/test-results/**/* - /tmp/log/YaST2/y2log + /tmp/log - name: IRC notification # see https://github.com/marketplace/actions/irc-message-action diff --git a/puppeteer/agama-integration-tests b/puppeteer/agama-integration-tests index 2617846ef6..304810c73e 100755 --- a/puppeteer/agama-integration-tests +++ b/puppeteer/agama-integration-tests @@ -1,16 +1,28 @@ -#! /usr/bin/sh +#! /usr/bin/bash + +# A helper script for running the Puppeteer integration tests. +# +# Usage: +# agama-integration-tests [mochajs-options] # exit on error, unset variables are an error set -eu MYDIR=$(realpath "$(dirname "$0")") +# options passed to mocha.js: +# --bail: stop at the first failure (otherwise the test would continue and very +# likely fail at the next steps as well, this prevents from false alarms) +# --slow: report tests as slow when they take more than 10 seconds, the default +# is 75ms which is too small for Agama +MOCHA_OPTIONS=(--bail --slow 10000) + if [ -e "$MYDIR/../.git/" ]; then - PUPPETEER_SKIP_DOWNLOAD=true npm install --omit=optional - npx mocha --bail "$@" + npm install --omit=optional + npx mocha "${MOCHA_OPTIONS[@]}" "$@" else # set the default load path export NODE_PATH=/usr/share/agama/integration-tests/node_modules # run the CLI script directly, npm/npx might not be installed - /usr/bin/env node /usr/share/agama/integration-tests/node_modules/mocha/bin/mocha.js --bail "$@" + /usr/bin/env node /usr/share/agama/integration-tests/node_modules/mocha/bin/mocha.js "${MOCHA_OPTIONS[@]}" "$@" fi From c82cc1e8ff4dc14ada13663e63699da9a8442c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 23 Jul 2024 08:43:11 +0100 Subject: [PATCH 274/430] fix(web) make failing tests works again --- web/src/components/network/WifiConnectionForm.jsx | 12 +++++------- .../components/network/WifiConnectionForm.test.jsx | 4 ++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/web/src/components/network/WifiConnectionForm.jsx b/web/src/components/network/WifiConnectionForm.jsx index cd745352cf..275a083303 100644 --- a/web/src/components/network/WifiConnectionForm.jsx +++ b/web/src/components/network/WifiConnectionForm.jsx @@ -32,11 +32,9 @@ import { } from "@patternfly/react-core"; import { PasswordInput } from "~/components/core"; import { useInstallerClient } from "~/context/installer"; -import { useNetwork, useNetworkConfigChanges, useSelectedWifiChange } from "~/queries/network"; +import { useNetworkConfigChanges } from "~/queries/network"; import { _ } from "~/i18n"; -import { NetworkEventTypes } from "~/client/network"; -import { DeviceState } from "~/client/network/model"; -import { QueryClient, useQueryClient } from "@tanstack/react-query"; +import { useQueryClient } from "@tanstack/react-query"; /* * FIXME: it should be moved to the SecurityProtocols enum that already exists or to a class based @@ -62,7 +60,6 @@ const securityFrom = (supported) => { export default function WifiConnectionForm({ network, onCancel, onSubmitCallback }) { const { network: client } = useInstallerClient(); const queryClient = useQueryClient(); - const { networks } = useNetwork(); const [error, setError] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [ssid, setSsid] = useState(network?.ssid || ""); @@ -84,8 +81,9 @@ export default function WifiConnectionForm({ network, onCancel, onSubmitCallback client .addAndConnectTo(ssid, { security, password, hidden }) .catch(() => setError(true)) - .finally(() => setIsConnecting(false) && queryClient.invalidateQueries({ "queryKey": ["network"] })); - + .finally( + () => setIsConnecting(false) && queryClient.invalidateQueries({ queryKey: ["network"] }), + ); }; return ( diff --git a/web/src/components/network/WifiConnectionForm.test.jsx b/web/src/components/network/WifiConnectionForm.test.jsx index b332ec1069..b500af8e10 100644 --- a/web/src/components/network/WifiConnectionForm.test.jsx +++ b/web/src/components/network/WifiConnectionForm.test.jsx @@ -28,6 +28,10 @@ import { WifiConnectionForm } from "~/components/network"; jest.mock("~/client"); +jest.mock("~/queries/network", () => ({ + useNetworkConfigChanges: jest.fn(), +})); + Element.prototype.scrollIntoView = jest.fn(); const hiddenNetworkMock = { From 5c380406570bd5b32d6960cf1affad348291013a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 23 Jul 2024 10:41:38 +0200 Subject: [PATCH 275/430] Added comment --- puppeteer/tests/test_root_password.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/puppeteer/tests/test_root_password.js b/puppeteer/tests/test_root_password.js index 2848df3339..02e5f27d78 100644 --- a/puppeteer/tests/test_root_password.js +++ b/puppeteer/tests/test_root_password.js @@ -4,6 +4,12 @@ import path from "path"; import puppeteer from "puppeteer-core"; import { expect } from "chai"; +// This is an example file for running Agama integration tests using Puppeteer. +// +// If the test fails it saves the page screenshot and the HTML page dump to +// ./log/ subdirectory. +// For more details about customization see the README.md file. + // helper function for converting String to Boolean function booleanEnv(name, default_value) { const env = process.env[name]; From c44f3af735d946594c917daca5545efbd24d0f7f Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 11:03:26 +0200 Subject: [PATCH 276/430] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa Co-authored-by: David Díaz <1691872+dgdavid@users.noreply.github.com> --- rust/agama-cli/src/questions.rs | 4 ++-- rust/agama-server/src/questions/web.rs | 4 ++-- service/README.md | 8 ++++---- service/package/rubygem-agama-yast.changes | 2 +- web/src/components/questions/QuestionWithPassword.jsx | 3 +-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index fa2ef1d34b..484e0438e5 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -20,9 +20,9 @@ pub enum QuestionsCommands { /// Path to a file containing the answers in YAML format. path: String, }, - /// prints list of questions that is waiting for answer in JSON format + /// Prints the list of questions that are waiting for an answer in JSON format List, - /// Ask question from stdin in JSON format and print answer when it is answered. + /// Reads a question definition in JSON from stdin and prints the response when it is answered. Ask, } diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index f7054193ee..792ad7453c 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -71,7 +71,7 @@ impl<'a> QuestionsClient<'a> { .map(|(k, v)| (k.as_str(), v.as_str())) .collect(); let path = if question.with_password.is_some() { - tracing::info!("creating question with password"); + tracing::info!("creating a question with password"); self.questions_proxy .new_with_password( &generic.class, @@ -82,7 +82,7 @@ impl<'a> QuestionsClient<'a> { ) .await? } else { - tracing::info!("creating generic question"); + tracing::info!("creating a generic question"); self.questions_proxy .new_question( &generic.class, diff --git a/service/README.md b/service/README.md index 63261976aa..2db0a7d6d6 100644 --- a/service/README.md +++ b/service/README.md @@ -6,9 +6,9 @@ According to [Agama's architecture](../doc/architecture.md) this project impleme ## Testing Changes -The easiest way to test changes done to ruby code on agama liveCD is to build -gem with modified sources with `gem build agama-yast`. Then copy resulting file -to agama live ISO. There do this sequence of commands: +The easiest way to test changes done to Ruby code on Agama live media is to build +the gem with modified sources with `gem build agama-yast`. Then copy the resulting file +to Agama live image. Then run this sequence of commands: ```sh # ensure that only modified sources are installed @@ -17,5 +17,5 @@ gem uninstall agama-yast gem install --no-doc --no-format-executable ``` -If change modifies also dbus parts, then restart related dbus services. +If the changes modify the D-Bus part, then restart related D-Bus services. diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 35da1c5e61..5df57f925c 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,7 +1,7 @@ ------------------------------------------------------------------- Mon Jul 22 15:26:48 UTC 2024 - Josef Reidinger -- Autoyast convert script: Use agama questions to report errors +- AutoYaST convert script: use Agama questions to report errors and ask when encrypted profile is used (gh#openSUSE/agama#1476) ------------------------------------------------------------------- diff --git a/web/src/components/questions/QuestionWithPassword.jsx b/web/src/components/questions/QuestionWithPassword.jsx index 984f3c5918..582be2b07a 100644 --- a/web/src/components/questions/QuestionWithPassword.jsx +++ b/web/src/components/questions/QuestionWithPassword.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2024] SUSE LLC * * All Rights Reserved. * @@ -41,7 +41,6 @@ export default function QuestionWithPassword({ question, answerCallback }) { } > {question.text} From 24c414442a6e8e0b1dc8cdd60f272cb2568be6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 23 Jul 2024 12:19:34 +0100 Subject: [PATCH 277/430] test(web): fix root auth tests --- .../components/users/RootAuthMethods.test.jsx | 400 +++++++----------- .../users/RootPasswordPopup.test.jsx | 48 +-- .../components/users/RootSSHKeyPopup.test.jsx | 55 ++- 3 files changed, 211 insertions(+), 292 deletions(-) diff --git a/web/src/components/users/RootAuthMethods.test.jsx b/web/src/components/users/RootAuthMethods.test.jsx index bc41d28d5c..ea1c5f22dd 100644 --- a/web/src/components/users/RootAuthMethods.test.jsx +++ b/web/src/components/users/RootAuthMethods.test.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2024] SUSE LLC * * All Rights Reserved. * @@ -20,299 +20,225 @@ */ import React from "react"; - import { act, screen, within } from "@testing-library/react"; -import { installerRender, createCallbackMock } from "~/test-utils"; +import { plainRender, installerRender, createCallbackMock } from "~/test-utils"; import { noop } from "~/utils"; -import { createClient } from "~/client"; import { RootAuthMethods } from "~/components/users"; +import { useRootUser, useRootUserMutation } from "~/queries/users"; -jest.mock("~/client"); -jest.mock("@patternfly/react-core", () => { - const original = jest.requireActual("@patternfly/react-core"); +const mockRootUserMutation = { mutate: jest.fn(), mutateAsync: jest.fn() }; +let mockPassword; +let mockSSHKey; - return { - ...original, - Skeleton: () =>
      PFSkeleton
      , - }; -}); +jest.mock("~/queries/users", () => ({ + ...jest.requireActual("~/queries/users"), + useRootUser: () => ({ data: { password: mockPassword, sshkey: mockSSHKey } }), + useRootUserMutation: () => mockRootUserMutation, + useRootUserChanges: () => jest.fn(), +})); -let onUsersChangeFn = noop; const isRootPasswordSetFn = jest.fn(); -const getRootSSHKeyFn = jest.fn(); -const setRootSSHKeyFn = jest.fn(); const removeRootPasswordFn = jest.fn(); const testKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+ test@example"; beforeEach(() => { - isRootPasswordSetFn.mockResolvedValue(false); - getRootSSHKeyFn.mockResolvedValue(""); - - createClient.mockImplementation(() => { - return { - users: { - isRootPasswordSet: isRootPasswordSetFn, - getRootSSHKey: getRootSSHKeyFn, - setRootSSHKey: setRootSSHKeyFn, - onUsersChange: onUsersChangeFn, - removeRootPassword: removeRootPasswordFn, - }, - }; - }); + mockPassword = false; + mockSSHKey = ""; }); -describe("when loading initial data", () => { - it("renders a loading component", async () => { - installerRender(); - await screen.findAllByText("PFSkeleton"); +describe("when no method is defined", () => { + it("renders a text inviting the user to define at least one", () => { + plainRender(); + + screen.getByText("No root authentication method defined yet."); + screen.getByText(/at least one/); }); }); -describe("when ready", () => { - describe("and no method is defined", () => { - it("renders a text inviting the user to define at least one", async () => { - installerRender(); - - await screen.findByText("No root authentication method defined yet."); - screen.getByText(/at least one/); - }); - - it("renders buttons for setting either, a password or a SSH Public Key", async () => { - installerRender(); +describe("and the password has been set", () => { + beforeEach(() => { + mockPassword = true; + }); - await screen.findByRole("button", { name: "Set a password" }); - screen.getByRole("button", { name: "Upload a SSH Public Key" }); - }); + it("renders the 'Already set' status", async () => { + plainRender(); - it("allows setting the password", async () => { - const { user } = installerRender(); + const table = await screen.findByRole("grid"); + const passwordRow = within(table).getByText("Password").closest("tr"); + within(passwordRow).getByText("Already set"); + }); - const button = await screen.findByRole("button", { name: "Set a password" }); - await user.click(button); + it("does not renders the 'Set' action", async () => { + const { user } = plainRender(); - screen.getByRole("dialog", { name: "Set a root password" }); - }); + const table = await screen.findByRole("grid"); + const passwordRow = within(table).getByText("Password").closest("tr"); + const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const setAction = within(passwordRow).queryByRole("menuitem", { name: "Set" }); + expect(setAction).toBeNull(); + }); - it("allows setting the SSH Public Key", async () => { - const { user } = installerRender(); + it("allows the user to change the already set password", async () => { + const { user } = plainRender(); - const button = await screen.findByRole("button", { name: "Upload a SSH Public Key" }); - await user.click(button); + const table = await screen.findByRole("grid"); + const passwordRow = within(table).getByText("Password").closest("tr"); + const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const changeAction = within(passwordRow).queryByRole("menuitem", { name: "Change" }); + await user.click(changeAction); - screen.getByRole("dialog", { name: "Add a SSH Public Key for root" }); - }); + screen.getByRole("dialog", { name: "Change the root password" }); }); - describe("and at least one method is already defined", () => { - beforeEach(() => isRootPasswordSetFn.mockResolvedValue(true)); + it("allows the user to discard the chosen password", async () => { + const { user } = plainRender(); - it("renders a table with available methods", async () => { - installerRender(); + const table = await screen.findByRole("grid"); + const passwordRow = within(table).getByText("Password").closest("tr"); + const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const discardAction = within(passwordRow).queryByRole("menuitem", { name: "Discard" }); + await user.click(discardAction); + + expect(mockRootUserMutation.mutate).toHaveBeenCalledWith({ password: "" }); + }); +}); - const table = await screen.findByRole("grid"); - within(table).getByText("Password"); - within(table).getByText("SSH Key"); - }); +describe("the password is not set yet", () => { + // Mock another auth method for reaching the table + beforeEach(() => { + mockSSHKey = "Fake"; }); - describe("and the password has been set", () => { - beforeEach(() => isRootPasswordSetFn.mockResolvedValue(true)); - - it("renders the 'Already set' status", async () => { - installerRender(); - - const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password").closest("tr"); - within(passwordRow).getByText("Already set"); - }); - - it("does not renders the 'Set' action", async () => { - const { user } = installerRender(); - - const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password").closest("tr"); - const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const setAction = within(passwordRow).queryByRole("menuitem", { name: "Set" }); - expect(setAction).toBeNull(); - }); - - it("allows the user to change the already set password", async () => { - const { user } = installerRender(); - - const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password").closest("tr"); - const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const changeAction = await within(passwordRow).queryByRole("menuitem", { name: "Change" }); - await user.click(changeAction); - - screen.getByRole("dialog", { name: "Change the root password" }); - }); - - it("allows the user to discard the chosen password", async () => { - const { user } = installerRender(); - - const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password").closest("tr"); - const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const discardAction = await within(passwordRow).queryByRole("menuitem", { name: "Discard" }); - await user.click(discardAction); - - expect(removeRootPasswordFn).toHaveBeenCalled(); - }); + it("renders the 'Not set' status", async () => { + plainRender(); + + const table = await screen.findByRole("grid"); + const passwordRow = within(table).getByText("Password").closest("tr"); + within(passwordRow).getByText("Not set"); }); - describe("but the password is not set yet", () => { - // Mock another auth method for reaching the table - beforeEach(() => getRootSSHKeyFn.mockResolvedValue("Fake")); + it("allows the user to set a password", async () => { + const { user } = plainRender(); - it("renders the 'Not set' status", async () => { - installerRender(); + const table = await screen.findByRole("grid"); + const passwordRow = within(table).getByText("Password").closest("tr"); + const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const setAction = within(passwordRow).getByRole("menuitem", { name: "Set" }); + await user.click(setAction); + screen.getByRole("dialog", { name: "Set a root password" }); + }); - const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password").closest("tr"); - within(passwordRow).getByText("Not set"); - }); + it("does not render the 'Change' nor the 'Discard' actions", async () => { + const { user } = plainRender(); - it("allows the user to set a password", async () => { - const { user } = installerRender(); + const table = await screen.findByRole("grid"); + const passwordRow = within(table).getByText("Password").closest("tr"); + const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); - const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password").closest("tr"); - const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const setAction = within(passwordRow).getByRole("menuitem", { name: "Set" }); - await user.click(setAction); - screen.getByRole("dialog", { name: "Set a root password" }); - }); + const changeAction = within(passwordRow).queryByRole("menuitem", { name: "Change" }); + const discardAction = within(passwordRow).queryByRole("menuitem", { name: "Discard" }); - it("does not render the 'Change' nor the 'Discard' actions", async () => { - const { user } = installerRender(); + expect(changeAction).toBeNull(); + expect(discardAction).toBeNull(); + }); +}); - const table = await screen.findByRole("grid"); - const passwordRow = within(table).getByText("Password").closest("tr"); - const actionsToggler = within(passwordRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); +describe("and the SSH Key has been set", () => { + beforeEach(() => { + mockSSHKey = testKey; + }); - const changeAction = await within(passwordRow).queryByRole("menuitem", { name: "Change" }); - const discardAction = await within(passwordRow).queryByRole("menuitem", { name: "Discard" }); + it("renders its truncated content keeping the comment visible when possible", async () => { + plainRender(); - expect(changeAction).toBeNull(); - expect(discardAction).toBeNull(); - }); + const table = await screen.findByRole("grid"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); + within(sshKeyRow).getByText("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+"); + within(sshKeyRow).getByText("test@example"); }); - describe("and the SSH Key has been set", () => { - beforeEach(() => getRootSSHKeyFn.mockResolvedValue(testKey)); - - it("renders its truncated content keeping the comment visible when possible", async () => { - installerRender(); - - const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); - within(sshKeyRow).getByText("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+"); - within(sshKeyRow).getByText("test@example"); - }); - - it("does not renders the 'Set' action", async () => { - const { user } = installerRender(); - - const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); - const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const setAction = within(sshKeyRow).queryByRole("menuitem", { name: "Set" }); - expect(setAction).toBeNull(); - }); - - it("allows the user to change it", async () => { - const { user } = installerRender(); - - const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); - const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const changeAction = await within(sshKeyRow).queryByRole("menuitem", { name: "Change" }); - await user.click(changeAction); - - screen.getByRole("dialog", { name: "Edit the SSH Public Key for root" }); - }); - - it("allows the user to discard it", async () => { - const { user } = installerRender(); - - const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); - const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const discardAction = await within(sshKeyRow).queryByRole("menuitem", { name: "Discard" }); - await user.click(discardAction); - - expect(setRootSSHKeyFn).toHaveBeenCalledWith(""); - }); + it("does not renders the 'Set' action", async () => { + const { user } = plainRender(); + + const table = await screen.findByRole("grid"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); + const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const setAction = within(sshKeyRow).queryByRole("menuitem", { name: "Set" }); + expect(setAction).toBeNull(); }); - describe("but the SSH Key is not set yet", () => { - // Mock another auth method for reaching the table - beforeEach(() => isRootPasswordSetFn.mockResolvedValue(true)); + it("allows the user to change it", async () => { + const { user } = plainRender(); - it("renders the 'Not set' status", async () => { - installerRender(); + const table = await screen.findByRole("grid"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); + const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const changeAction = within(sshKeyRow).queryByRole("menuitem", { name: "Change" }); + await user.click(changeAction); - const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); - within(sshKeyRow).getByText("Not set"); - }); + screen.getByRole("dialog", { name: "Edit the SSH Public Key for root" }); + }); - it("allows the user to set a key", async () => { - const { user } = installerRender(); + it("allows the user to discard it", async () => { + const { user } = plainRender(); - const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); - const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); - const setAction = within(sshKeyRow).getByRole("menuitem", { name: "Set" }); - await user.click(setAction); - screen.getByRole("dialog", { name: "Add a SSH Public Key for root" }); - }); + const table = await screen.findByRole("grid"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); + const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const discardAction = within(sshKeyRow).queryByRole("menuitem", { name: "Discard" }); + await user.click(discardAction); - it("does not render the 'Change' nor the 'Discard' actions", async () => { - const { user } = installerRender(); + expect(mockRootUserMutation.mutate).toHaveBeenCalledWith({ sshkey: "" }); + }); +}); - const table = await screen.findByRole("grid"); - const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); - const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); - await user.click(actionsToggler); +describe("but the SSH Key is not set yet", () => { + // Mock another auth method for reaching the table + beforeEach(() => { + mockPassword = true; + }); - const changeAction = await within(sshKeyRow).queryByRole("menuitem", { name: "Change" }); - const discardAction = await within(sshKeyRow).queryByRole("menuitem", { name: "Discard" }); + it("renders the 'Not set' status", async () => { + plainRender(); - expect(changeAction).toBeNull(); - expect(discardAction).toBeNull(); - }); + const table = await screen.findByRole("grid"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); + within(sshKeyRow).getByText("Not set"); }); - describe("and user settings changes", () => { - // Mock an auth method for reaching the table - beforeEach(() => isRootPasswordSetFn.mockResolvedValue(true)); + it("allows the user to set a key", async () => { + const { user } = plainRender(); + + const table = await screen.findByRole("grid"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); + const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); + const setAction = within(sshKeyRow).getByRole("menuitem", { name: "Set" }); + await user.click(setAction); + screen.getByRole("dialog", { name: "Add a SSH Public Key for root" }); + }); - it("updates the UI accordingly", async () => { - const [mockFunction, callbacks] = createCallbackMock(); - onUsersChangeFn = mockFunction; + it("does not render the 'Change' nor the 'Discard' actions", async () => { + const { user } = plainRender(); - installerRender(); - await screen.findAllByText("Not set"); + const table = await screen.findByRole("grid"); + const sshKeyRow = within(table).getByText("SSH Key").closest("tr"); + const actionsToggler = within(sshKeyRow).getByRole("button", { name: "Actions" }); + await user.click(actionsToggler); - const [cb] = callbacks; - act(() => { - cb({ rootPasswordSet: true, rootSSHKey: testKey }); - }); + const changeAction = within(sshKeyRow).queryByRole("menuitem", { name: "Change" }); + const discardAction = within(sshKeyRow).queryByRole("menuitem", { name: "Discard" }); - await screen.findByText("Already set"); - await screen.findByText("test@example"); - }); + expect(changeAction).toBeNull(); + expect(discardAction).toBeNull(); }); }); diff --git a/web/src/components/users/RootPasswordPopup.test.jsx b/web/src/components/users/RootPasswordPopup.test.jsx index 4340e75475..833e388019 100644 --- a/web/src/components/users/RootPasswordPopup.test.jsx +++ b/web/src/components/users/RootPasswordPopup.test.jsx @@ -22,49 +22,45 @@ import React from "react"; import { screen, waitFor, within } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; -import { createClient } from "~/client"; - +import { plainRender } from "~/test-utils"; import { RootPasswordPopup } from "~/components/users"; -jest.mock("~/client"); +const mockRootUserMutation = { mutateAsync: jest.fn() }; +let mockPassword; +let mockSSHKey; + +jest.mock("~/queries/users", () => ({ + ...jest.requireActual("~/queries/users"), + useRootUser: () => ({ data: { password: mockPassword, sshkey: "" } }), + useRootUserMutation: () => mockRootUserMutation, + useRootUserChanges: () => jest.fn(), +})); const onCloseCallback = jest.fn(); -const setRootPasswordFn = jest.fn(); const password = "nots3cr3t"; -beforeEach(() => { - createClient.mockImplementation(() => { - return { - users: { - setRootPassword: setRootPasswordFn, - }, - }; - }); -}); - describe("when it is closed", () => { it("renders nothing", async () => { - const { container } = installerRender(); + const { container } = plainRender(); await waitFor(() => expect(container).toBeEmptyDOMElement()); }); }); describe("when it is open", () => { - it("renders default title when none if given", async () => { - installerRender(); - const dialog = await screen.findByRole("dialog"); + it("renders default title when none if given", () => { + plainRender(); + const dialog = screen.queryByRole("dialog"); within(dialog).getByText("Root password"); }); - it("renders the given title", async () => { - installerRender(); - const dialog = await screen.findByRole("dialog"); + it("renders the given title", () => { + plainRender(); + const dialog = screen.getByRole("dialog"); within(dialog).getByText("Change The Root Password"); }); it("allows changing the password", async () => { - const { user } = installerRender(); + const { user } = plainRender(); await screen.findByRole("dialog"); @@ -79,17 +75,17 @@ describe("when it is open", () => { expect(confirmButton).toBeEnabled(); await user.click(confirmButton); - expect(setRootPasswordFn).toHaveBeenCalledWith(password); + expect(mockRootUserMutation.mutateAsync).toHaveBeenCalledWith({ password }); expect(onCloseCallback).toHaveBeenCalled(); }); it("allows dismissing the dialog without changing the password", async () => { - const { user } = installerRender(); + const { user } = plainRender(); await screen.findByRole("dialog"); const cancelButton = await screen.findByRole("button", { name: /Cancel/i }); await user.click(cancelButton); - expect(setRootPasswordFn).not.toHaveBeenCalled(); + expect(mockRootUserMutation.mutateAsync).not.toHaveBeenCalled(); expect(onCloseCallback).toHaveBeenCalled(); }); }); diff --git a/web/src/components/users/RootSSHKeyPopup.test.jsx b/web/src/components/users/RootSSHKeyPopup.test.jsx index 64ed1fd177..e68bcc8bcb 100644 --- a/web/src/components/users/RootSSHKeyPopup.test.jsx +++ b/web/src/components/users/RootSSHKeyPopup.test.jsx @@ -22,54 +22,51 @@ import React from "react"; import { screen, waitFor, within } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; -import { createClient } from "~/client"; +import { plainRender } from "~/test-utils"; import { RootSSHKeyPopup } from "~/components/users"; -jest.mock("~/client"); +const mockRootUserMutation = { mutateAsync: jest.fn() }; +let mockSSHKey; + +jest.mock("~/queries/users", () => ({ + ...jest.requireActual("~/queries/users"), + useRootUser: () => ({ data: { sshkey: mockSSHKey } }), + useRootUserMutation: () => mockRootUserMutation, + useRootUserChanges: () => jest.fn(), +})); const onCloseCallback = jest.fn(); const setRootSSHKeyFn = jest.fn(); const testKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+ test@example"; -beforeEach(() => { - createClient.mockImplementation(() => { - return { - users: { - setRootSSHKey: setRootSSHKeyFn, - }, - }; - }); -}); - describe("when it is closed", () => { - it("renders nothing", async () => { - const { container } = installerRender(); - await waitFor(() => expect(container).toBeEmptyDOMElement()); + it("renders nothing", () => { + const { container } = plainRender(); + expect(container).toBeEmptyDOMElement(); }); }); describe("when it is open", () => { - it("renders default title when none if given", async () => { - installerRender(); - const dialog = await screen.findByRole("dialog"); + it("renders default title when none if given", () => { + plainRender(); + const dialog = screen.getByRole("dialog"); within(dialog).getByText("Set root SSH public key"); }); - it("renders the given title", async () => { - installerRender(); - const dialog = await screen.findByRole("dialog"); + it("renders the given title", () => { + plainRender(); + const dialog = screen.getByRole("dialog"); within(dialog).getByText("Root SSHKey"); }); - it("contains the given key, if any", async () => { - installerRender(); - const dialog = await screen.findByRole("dialog"); + it("contains the given key, if any", () => { + plainRender(); + const dialog = screen.getByRole("dialog"); within(dialog).getByText(testKey); }); it("allows defining a new root SSH public key", async () => { - const { user } = installerRender(); + const { user } = plainRender(); const dialog = await screen.findByRole("dialog"); const sshKeyInput = within(dialog).getByLabelText("Root SSH public key"); @@ -80,12 +77,12 @@ describe("when it is open", () => { expect(confirmButton).toBeEnabled(); await user.click(confirmButton); - expect(setRootSSHKeyFn).toHaveBeenCalledWith(testKey); + expect(mockRootUserMutation.mutateAsync).toHaveBeenCalledWith({ sshkey: testKey }); expect(onCloseCallback).toHaveBeenCalled(); }); it("does not change anything if the user cancels", async () => { - const { user } = installerRender(); + const { user } = plainRender(); const dialog = await screen.findByRole("dialog"); const sshKeyInput = within(dialog).getByLabelText("Root SSH public key"); const cancelButton = within(dialog).getByRole("button", { name: /Cancel/i }); @@ -93,7 +90,7 @@ describe("when it is open", () => { await user.type(sshKeyInput, testKey); await user.click(cancelButton); - expect(setRootSSHKeyFn).not.toHaveBeenCalled(); + expect(mockRootUserMutation.mutateAsync).not.toHaveBeenCalled(); expect(onCloseCallback).toHaveBeenCalled(); }); }); From 8658920eb01abe5e387a809cd5df82aaabfab2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 23 Jul 2024 12:26:00 +0100 Subject: [PATCH 278/430] refactor(web): drop the UsersClient --- web/src/client/index.js | 4 - web/src/client/users.js | 184 -------------------------- web/src/client/users.test.js | 245 ----------------------------------- 3 files changed, 433 deletions(-) delete mode 100644 web/src/client/users.js delete mode 100644 web/src/client/users.test.js diff --git a/web/src/client/index.js b/web/src/client/index.js index 24282045ad..b1920f1f18 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -26,7 +26,6 @@ import { ManagerClient } from "./manager"; import { Monitor } from "./monitor"; import { ProductClient, SoftwareClient } from "./software"; import { StorageClient } from "./storage"; -import { UsersClient } from "./users"; import phase from "./phase"; import { QuestionsClient } from "./questions"; import { NetworkClient } from "./network"; @@ -41,7 +40,6 @@ import { HTTPClient, WSClient } from "./http"; * @property {ProductClient} product - product client. * @property {SoftwareClient} software - software client. * @property {StorageClient} storage - storage client. - * @property {UsersClient} users - users client. * @property {QuestionsClient} questions - questions client. * @property {() => WSClient} ws - Agama WebSocket client. * @property {() => boolean} isConnected - determines whether the client is connected @@ -68,7 +66,6 @@ const createClient = (url) => { const network = new NetworkClient(client); const software = new SoftwareClient(client); const storage = new StorageClient(client); - const users = new UsersClient(client); const questions = new QuestionsClient(client); const isConnected = () => client.ws().isConnected() || false; @@ -82,7 +79,6 @@ const createClient = (url) => { network, software, storage, - users, questions, isConnected, isRecoverable, diff --git a/web/src/client/users.js b/web/src/client/users.js deleted file mode 100644 index 304c7eec78..0000000000 --- a/web/src/client/users.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -const SERVICE_NAME = "org.opensuse.Agama.Manager1"; - -/** - * @typedef {object} UserResult - * @property {boolean} result - whether the action succeeded or not - * @property {string[]} issues - issues found when applying the action - */ - -/** - * @typedef {object} User - * @property {string} fullName - User full name - * @property {string} userName - userName - * @property {string} [password] - user password - * @property {boolean} autologin - Whether autologin is enabled - * @property {object} data - additional user data - */ - -/** - * @typedef {object} UserSettings - * @property {User} [firstUser] - first user - * @property {boolean} [rootPasswordSet] - whether the root password is set - * @property {string} [rootSSHKey] - root SSH public key - */ - -/** - * Client to interact with the Agama users service - * - * @ignore - */ -class UsersClient { - /** - * @param {import("./http").HTTPClient} client - HTTP client. - */ - constructor(client) { - this.client = client; - } - - /** - * Returns the first user structure - * - * @return {Promise} - */ - async getUser() { - const response = await this.client.get("/users/first"); - if (!response.ok) { - console.log("Failed to get first user config: ", response); - return { fullName: "", userName: "", password: "", autologin: false, data: {} }; - } - return response.json(); - } - - /** - * Returns true if the root password is set - * - * @return {Promise} - */ - async isRootPasswordSet() { - const response = await this.client.get("/users/root"); - if (!response.ok) { - console.log("Failed to get root config: ", response); - return false; - } - const config = await response.json(); - return config.password; - } - - /** - * Sets the first user - * - * @param {User} user - object with full name, user name, password and boolean for autologin - * @return {Promise} returns an object with the result and the issues found if error - */ - async setUser(user) { - const result = await this.client.put("/users/first", user); - - return { result: result.ok, issues: [] }; // TODO: check how to handle issues and result. Maybe separate call to validate? - } - - /** - * Removes the first user - * - * @return {Promise} whether the operation was successful or not - */ - async removeUser() { - return (await this.client.delete("/users/first")).ok; - } - - /** - * Sets the root password - * - * @param {String} password - plain text root password ( maybe allow client side encryption?) - * @return {Promise} whether the operation was successful or not - */ - async setRootPassword(password) { - const response = await this.client.patch("/users/root", { password, passwordEncrypted: false }); - return response.ok; - } - - /** - * Clears the root password - * - * @return {Promise} whether the operation was successful or not - */ - async removeRootPassword() { - return this.setRootPassword(""); - } - - /** - * Returns the root's public SSH key - * - * @return {Promise} SSH public key or an empty string if it is not set - */ - async getRootSSHKey() { - const response = await this.client.get("/users/root"); - if (!response.ok) { - console.log("Failed to get root config: ", response); - return ""; - } - const config = await response.json(); - return config.sshkey; - } - - /** - * Sets root's public SSH Key - * - * @param {String} key - plain text root ssh key. Empty string means disabled - * @return {Promise} whether the operation was successful or not - */ - async setRootSSHKey(key) { - const response = await this.client.patch("/users/root", { sshkey: key }); - return response.ok; - } - - /** - * Registers a callback to run when user properties change - * - * @param {(userSettings: UserSettings) => void} handler - callback function - * @return {import ("./dbus").RemoveFn} function to disable the callback - */ - onUsersChange(handler) { - return this.client.ws().onEvent((event) => { - if (event.type === "RootChanged") { - const res = {}; - if (event.password !== null) { - res.rootPasswordSet = event.password; - } - if (event.sshkey !== null) { - res.rootSSHKey = event.sshkey; - } - // @ts-ignore - return handler(res); - } else if (event.type === "FirstUserChanged") { - // @ts-ignore - const { fullName, userName, password, autologin, data } = event; - return handler({ firstUser: { fullName, userName, password, autologin, data } }); - } - }); - } -} - -export { UsersClient }; diff --git a/web/src/client/users.test.js b/web/src/client/users.test.js deleted file mode 100644 index 405d797b81..0000000000 --- a/web/src/client/users.test.js +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -import { HTTPClient } from "./http"; -import { UsersClient } from "./users"; - -const mockJsonFn = jest.fn(); -const mockGetFn = jest.fn().mockImplementation(() => { - return { ok: true, json: mockJsonFn }; -}); -const mockPatchFn = jest.fn().mockImplementation(() => { - return { ok: true }; -}); -const mockPutFn = jest.fn().mockImplementation(() => { - return { ok: true }; -}); -const mockDeleteFn = jest.fn().mockImplementation(() => { - return { ok: true }; -}); - -jest.mock("./http", () => { - return { - HTTPClient: jest.fn().mockImplementation(() => { - return { - get: mockGetFn, - patch: mockPatchFn, - put: mockPutFn, - delete: mockDeleteFn, - }; - }), - }; -}); - -let client; - -const firstUser = { - fullName: "Jane Doe", - userName: "jane", - password: "12345", - autologin: false, -}; - -beforeEach(() => { - client = new UsersClient(new HTTPClient(new URL("http://localhost"))); -}); - -describe("#getUser", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(firstUser); - }); - - it("returns the defined first user", async () => { - const user = await client.getUser(); - expect(user).toEqual(firstUser); - expect(mockGetFn).toHaveBeenCalledWith("/users/first"); - }); -}); - -describe("#isRootPasswordSet", () => { - describe("when the root password is set", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue({ password: true, sshkey: "" }); - }); - - it("returns true", async () => { - expect(await client.isRootPasswordSet()).toEqual(true); - expect(mockGetFn).toHaveBeenCalledWith("/users/root"); - }); - }); - - describe("when the root password is not set", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue({ password: false, sshkey: "" }); - }); - - it("returns false", async () => { - expect(await client.isRootPasswordSet()).toEqual(false); - expect(mockGetFn).toHaveBeenCalledWith("/users/root"); - }); - }); -}); - -describe("#getRootSSHKey", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue({ password: "", sshkey: "ssh-key" }); - }); - - it("returns the SSH key for the root user", async () => { - const result = expect(await client.getRootSSHKey()).toEqual("ssh-key"); - expect(mockGetFn).toHaveBeenCalledWith("/users/root"); - }); -}); - -describe("#setUser", () => { - it("sets the values of the first user and returns whether succeeded or not an errors found", async () => { - const user = { - fullName: "Jane Doe", - userName: "jane", - password: "12345", - autologin: false, - }; - const result = await client.setUser(user); - expect(mockPutFn).toHaveBeenCalledWith("/users/first", user); - expect(result); - }); - - describe("when setting the user fails because some issue", () => { - beforeEach(() => { - mockPutFn.mockResolvedValue({ ok: false }); - }); - - // issues are not included in the response - it.skip("returns an object with the result as false and the issues found", async () => { - const result = await client.setUser({ - fullName: "Jane Doe", - userName: "jane", - password: "12345", - autologin: false, - }); - - expect(mockPutFn).toHaveBeenCalledWith("/users/first"); - expect(result).toEqual({ result: false, issues: ["There is an error"] }); - }); - }); -}); - -describe("#removeUser", () => { - it("removes the first user and returns true", async () => { - const result = await client.removeUser(); - expect(result).toEqual(true); - expect(mockDeleteFn).toHaveBeenCalledWith("/users/first"); - }); - - describe("when removing the user fails", () => { - beforeEach(() => { - mockDeleteFn.mockResolvedValue({ ok: false }); - }); - - it("returns false", async () => { - const result = await client.removeUser(); - expect(result).toEqual(false); - expect(mockDeleteFn).toHaveBeenCalledWith("/users/first"); - }); - }); -}); - -describe("#setRootPassword", () => { - it("sets the root password and returns true", async () => { - const result = await client.setRootPassword("12345"); - expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { - password: "12345", - passwordEncrypted: false, - }); - expect(result).toEqual(true); - }); - - describe("when setting the password fails", () => { - beforeEach(() => { - mockPatchFn.mockResolvedValue({ ok: false }); - }); - - it("returns false", async () => { - const result = await client.setRootPassword("12345"); - expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { - password: "12345", - passwordEncrypted: false, - }); - expect(result).toEqual(false); - }); - }); -}); - -describe("#removeRootPassword", () => { - beforeEach(() => { - mockPatchFn.mockResolvedValue({ ok: true }); - }); - - it("removes the root password", async () => { - const result = await client.removeRootPassword(); - expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { - password: "", - passwordEncrypted: false, - }); - expect(result).toEqual(true); - }); - - describe("when setting the user fails", () => { - beforeEach(() => { - mockPatchFn.mockResolvedValue({ ok: false }); - }); - - it("returns false", async () => { - const result = await client.removeRootPassword(); - expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { - password: "", - passwordEncrypted: false, - }); - expect(result).toEqual(false); - }); - }); -}); - -describe("#setRootSSHKey", () => { - beforeEach(() => { - mockPatchFn.mockResolvedValue({ ok: true }); - }); - - it("sets the root password and returns true", async () => { - const result = await client.setRootSSHKey("ssh-key"); - expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { sshkey: "ssh-key" }); - expect(result).toEqual(true); - }); - - describe("when setting the user fails", () => { - beforeEach(() => { - mockPatchFn.mockResolvedValue({ ok: false }); - }); - - it("returns false", async () => { - const result = await client.setRootSSHKey("ssh-key"); - expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { sshkey: "ssh-key" }); - expect(result).toEqual(false); - }); - }); -}); From 2ed229fd761f5da5f88ffac79da64ecc65a5ed3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 23 Jul 2024 12:45:55 +0100 Subject: [PATCH 279/430] refactor(web): simplify users hooks API --- web/src/components/users/FirstUser.jsx | 9 ++------- web/src/components/users/FirstUserForm.jsx | 2 +- web/src/components/users/RootAuthMethods.jsx | 4 +--- web/src/queries/users.ts | 10 ++++++++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/web/src/components/users/FirstUser.jsx b/web/src/components/users/FirstUser.jsx index 8a81b04cf2..19c5c17f03 100644 --- a/web/src/components/users/FirstUser.jsx +++ b/web/src/components/users/FirstUser.jsx @@ -25,12 +25,7 @@ import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { useNavigate } from "react-router-dom"; import { RowActions, ButtonLink } from "~/components/core"; import { _ } from "~/i18n"; -import { - useFirstUser, - useFirstUserChanges, - useFirstUserMutation, - useRemoveFirstUserMutation, -} from "~/queries/users"; +import { useFirstUser, useFirstUserChanges, useRemoveFirstUserMutation } from "~/queries/users"; const UserNotDefined = ({ actionCb }) => { return ( @@ -78,7 +73,7 @@ const UserData = ({ user, actions }) => { }; export default function FirstUser() { - const { data: user } = useFirstUser(); + const user = useFirstUser(); const removeUser = useRemoveFirstUserMutation(); const navigate = useNavigate(); diff --git a/web/src/components/users/FirstUserForm.jsx b/web/src/components/users/FirstUserForm.jsx index d2f752a221..ad7322baa1 100644 --- a/web/src/components/users/FirstUserForm.jsx +++ b/web/src/components/users/FirstUserForm.jsx @@ -83,7 +83,7 @@ const UsernameSuggestions = ({ // close to the related input. // TODO: extract the suggestions logic. export default function FirstUserForm() { - const { data: firstUser } = useFirstUser(); + const firstUser = useFirstUser(); const setFirstUser = useFirstUserMutation(); const client = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); diff --git a/web/src/components/users/RootAuthMethods.jsx b/web/src/components/users/RootAuthMethods.jsx index ff24fa5076..b8b5143573 100644 --- a/web/src/components/users/RootAuthMethods.jsx +++ b/web/src/components/users/RootAuthMethods.jsx @@ -59,9 +59,7 @@ export default function RootAuthMethods() { const [isSSHKeyFormOpen, setIsSSHKeyFormOpen] = useState(false); const [isPasswordFormOpen, setIsPasswordFormOpen] = useState(false); - const { - data: { password: isPasswordDefined, sshkey: sshKey }, - } = useRootUser(); + const { password: isPasswordDefined, sshkey: sshKey } = useRootUser(); useRootUserChanges(); diff --git a/web/src/queries/users.ts b/web/src/queries/users.ts index 6ec88957a0..2404810de8 100644 --- a/web/src/queries/users.ts +++ b/web/src/queries/users.ts @@ -36,7 +36,10 @@ const firstUserQuery = () => ({ /** * Hook that returns the first user. */ -const useFirstUser = () => useSuspenseQuery(firstUserQuery()); +const useFirstUser = () => { + const { data: firstUser } = useSuspenseQuery(firstUserQuery()); + return firstUser; +}; /* * Hook that returns a mutation to change the first user. @@ -113,7 +116,10 @@ const rootUserQuery = () => ({ queryFn: () => fetch("/api/users/root").then((res) => res.json()), }); -const useRootUser = () => useSuspenseQuery(rootUserQuery()); +const useRootUser = () => { + const { data: rootUser } = useSuspenseQuery(rootUserQuery()); + return rootUser; +}; /* * Hook that returns a mutation to change the root user configuration. From 2e9d7e7839df2cb9185bdb366738e4b73f809537 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 14:05:19 +0200 Subject: [PATCH 280/430] introduce internal error --- rust/agama-cli/src/questions.rs | 8 +++----- rust/agama-lib/src/error.rs | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index 484e0438e5..d6861bd402 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -57,12 +57,10 @@ async fn set_answers(proxy: Questions1Proxy<'_>, path: String) -> Result<(), Ser async fn list_questions() -> Result<(), ServiceError> { let client = HTTPClient::new().await?; let questions = client.list_questions().await?; - // FIXME: that conversion to anyhow error is nasty, but we do not expect issue - // when questions are already read from json // FIXME: if performance is bad, we can skip converting json from http to struct and then // serialize it, but it won't be pretty string let questions_json = - serde_json::to_string_pretty(&questions).map_err(Into::::into)?; + serde_json::to_string_pretty(&questions).map_err(|e| ServiceError::InternalError(e.to_string()))?; println!("{}", questions_json); Ok(()) } @@ -73,10 +71,10 @@ async fn ask_question() -> Result<(), ServiceError> { let created_question = client.create_question(&question).await?; let Some(id) = created_question.generic.id else { - return Err(ServiceError::QuestionNotExist(0)); + return Err(ServiceError::InternalError("Created question does not get id".to_string())); }; let answer = client.get_answer(id).await?; - let answer_json = serde_json::to_string_pretty(&answer).map_err(Into::::into)?; + let answer_json = serde_json::to_string_pretty(&answer).map_err(|e| ServiceError::InternalError(e.to_string()))?; println!("{}", answer_json); client.delete_question(id).await?; diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index b5809c73b7..ed67f8f50f 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -43,6 +43,9 @@ pub enum ServiceError { BackendError(u16, String), #[error("You are not logged in. Please use: agama auth login")] NotAuthenticated, + // Specific error when something does not work as expected, but it is not user fault + #[error("Internal error. Please report a bug and attach logs. Details: {0}")] + InternalError(String), } #[derive(Error, Debug)] From 2d7fff4040545a8316c6c6ad42fb0aba73fad4a8 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 14:06:09 +0200 Subject: [PATCH 281/430] remove duplicite popup now --- service/lib/yast2/popup.rb | 74 -------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 service/lib/yast2/popup.rb diff --git a/service/lib/yast2/popup.rb b/service/lib/yast2/popup.rb deleted file mode 100644 index ec1af2a5b3..0000000000 --- a/service/lib/yast2/popup.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -# -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "yast" -require "agama/dbus/clients/questions" - -module Yast2 - # Replacement to the Yast2::Popup class to work with Agama. - class Popup - class << self - # rubocop:disable Metrics/ParameterLists - # rubocop:disable Lint/UnusedMethodArgument - def show(message, details: "", headline: "", timeout: 0, focus: nil, buttons: :ok, - richtext: false, style: :notice) - - question = Agama::Question.new( - qclass: "popup", - text: message, - options: generate_options(buttons), - default_option: focus - ) - questions_client.ask(question) - end - - private - - # FIXME: inject the logger - def logger - @logger = Logger.new($stdout) - end - - def generate_options(buttons) - case buttons - when :ok - [:ok] - when :continue_cancel - [:continue, :cancel] - when :yes_no - [:yes, :no] - else - raise ArgumentError, "Invalid value #{buttons.inspect} for buttons" - end - end - - # Returns the client to ask questions - # - # @return [Agama::DBus::Clients::Questions] - def questions_client - @questions_client ||= Agama::DBus::Clients::Questions.new(logger: logger) - end - end - end -end -# rubocop:enable Metrics/ParameterLists -# rubocop:enable Lint/UnusedMethodArgument From 20ea50a9b8872c9c5fa1bfaa89bb854767425db1 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 14:26:04 +0200 Subject: [PATCH 282/430] fix formatting --- .../questions/QuestionWithPassword.jsx | 3 +-- web/src/components/questions/Questions.jsx | 6 ++++- web/src/languages.json | 22 +++++++++---------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/web/src/components/questions/QuestionWithPassword.jsx b/web/src/components/questions/QuestionWithPassword.jsx index 582be2b07a..14e534ec44 100644 --- a/web/src/components/questions/QuestionWithPassword.jsx +++ b/web/src/components/questions/QuestionWithPassword.jsx @@ -26,7 +26,6 @@ import { PasswordInput, Popup } from "~/components/core"; import { QuestionActions } from "~/components/questions"; import { _ } from "~/i18n"; - export default function QuestionWithPassword({ question, answerCallback }) { const [password, setPassword] = useState(question.password || ""); const defaultAction = question.defaultOption; @@ -65,4 +64,4 @@ export default function QuestionWithPassword({ question, answerCallback }) {
      ); -} \ No newline at end of file +} diff --git a/web/src/components/questions/Questions.jsx b/web/src/components/questions/Questions.jsx index b2f786ef41..4280a4cf6b 100644 --- a/web/src/components/questions/Questions.jsx +++ b/web/src/components/questions/Questions.jsx @@ -24,7 +24,11 @@ import { useInstallerClient } from "~/context/installer"; import { useCancellablePromise } from "~/utils"; import { QUESTION_TYPES } from "~/client/questions"; -import { GenericQuestion, QuestionWithPassword, LuksActivationQuestion } from "~/components/questions"; +import { + GenericQuestion, + QuestionWithPassword, + LuksActivationQuestion, +} from "~/components/questions"; export default function Questions() { const client = useInstallerClient(); diff --git a/web/src/languages.json b/web/src/languages.json index 8c64959024..d406f186b0 100644 --- a/web/src/languages.json +++ b/web/src/languages.json @@ -1,12 +1,12 @@ { - "ca-es": "Català", - "de-de": "Deutsch", - "en-us": "English", - "es-es": "Español", - "ja-jp": "日本語", - "nb-NO": "Norsk bokmål", - "pt-BR": "Português", - "ru-ru": "Русский", - "sv-se": "Svenska", - "zh-Hans": "中文" -} \ No newline at end of file + "ca-es": "Català", + "de-de": "Deutsch", + "en-us": "English", + "es-es": "Español", + "ja-jp": "日本語", + "nb-NO": "Norsk bokmål", + "pt-BR": "Português", + "ru-ru": "Русский", + "sv-se": "Svenska", + "zh-Hans": "中文" +} From 097f14f49b5128b9e8a6b52f14c5c1d5b70b9351 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 14:26:34 +0200 Subject: [PATCH 283/430] add test for question with password component --- .../questions/QuestionWithPassword.test.jsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 web/src/components/questions/QuestionWithPassword.test.jsx diff --git a/web/src/components/questions/QuestionWithPassword.test.jsx b/web/src/components/questions/QuestionWithPassword.test.jsx new file mode 100644 index 0000000000..03603fedda --- /dev/null +++ b/web/src/components/questions/QuestionWithPassword.test.jsx @@ -0,0 +1,71 @@ +/* + * Copyright (c) [2022] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import { QuestionWithPassword } from "~/components/questions"; + +let question; +const answerFn = jest.fn(); + +const renderQuestion = () => + installerRender(); + +describe("QuestionWithPassword", () => { + beforeEach(() => { + question = { + id: 1, + class: "question.password", + text: "Random question. Will you provide random password?", + options: ["ok", "cancel"], + defaultOption: "cancel", + data: {}, + }; + }); + + it("renders the question text", async () => { + renderQuestion(); + + await screen.findByText(question.text); + }); + + it("contains a textinput for entering the password", async () => { + renderQuestion(); + + const passwordInput = await screen.findByLabelText("Password"); + expect(passwordInput).not.toBeNull(); + }); + + describe("when the user selects one of the options", () => { + it("calls the callback after setting both, answer and password", async () => { + const { user } = renderQuestion(); + + const passwordInput = await screen.findByLabelText("Password"); + await user.type(passwordInput, "notSecret"); + const skipButton = await screen.findByRole("button", { name: /Ok/ }); + await user.click(skipButton); + + expect(question).toEqual(expect.objectContaining({ password: "notSecret", answer: "ok" })); + expect(answerFn).toHaveBeenCalledWith(question); + }); + }); +}); From 33f7c75ab329693f1365689ac7d3994cf34f498e Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 14:29:01 +0200 Subject: [PATCH 284/430] relax condition of products test to not fail when we change product --- service/test/agama/software/manager_test.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb index 8643608c20..d090f027ce 100644 --- a/service/test/agama/software/manager_test.rb +++ b/service/test/agama/software/manager_test.rb @@ -251,11 +251,7 @@ it "returns the list of known products" do products = subject.products expect(products).to all(be_a(Agama::Software::Product)) - expect(products).to contain_exactly( - an_object_having_attributes(id: "Tumbleweed"), - an_object_having_attributes(id: "MicroOS"), - an_object_having_attributes(id: "Leap_16.0") - ) + expect(products).to_not be_empty end end From 8bc9b1b2b7b34a65d6efd392e2a8946e30f08459 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 14:39:47 +0200 Subject: [PATCH 285/430] fix formatting --- rust/agama-cli/src/questions.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index d6861bd402..6d0d9c05a8 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -59,8 +59,8 @@ async fn list_questions() -> Result<(), ServiceError> { let questions = client.list_questions().await?; // FIXME: if performance is bad, we can skip converting json from http to struct and then // serialize it, but it won't be pretty string - let questions_json = - serde_json::to_string_pretty(&questions).map_err(|e| ServiceError::InternalError(e.to_string()))?; + let questions_json = serde_json::to_string_pretty(&questions) + .map_err(|e| ServiceError::InternalError(e.to_string()))?; println!("{}", questions_json); Ok(()) } @@ -71,10 +71,13 @@ async fn ask_question() -> Result<(), ServiceError> { let created_question = client.create_question(&question).await?; let Some(id) = created_question.generic.id else { - return Err(ServiceError::InternalError("Created question does not get id".to_string())); + return Err(ServiceError::InternalError( + "Created question does not get id".to_string(), + )); }; let answer = client.get_answer(id).await?; - let answer_json = serde_json::to_string_pretty(&answer).map_err(|e| ServiceError::InternalError(e.to_string()))?; + let answer_json = serde_json::to_string_pretty(&answer) + .map_err(|e| ServiceError::InternalError(e.to_string()))?; println!("{}", answer_json); client.delete_question(id).await?; From 5bf23385b95cd13f1e1739dce44bc8bbd5fe7e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 23 Jul 2024 14:06:57 +0100 Subject: [PATCH 286/430] fix(web): fix RootUser definition --- web/src/types/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/types/users.ts b/web/src/types/users.ts index bd88452d4c..a89fe211bd 100644 --- a/web/src/types/users.ts +++ b/web/src/types/users.ts @@ -28,7 +28,7 @@ type FirstUser = { type RootUser = { password: boolean; - sshkey: string | null; + sshkey: string; }; type RootUserChanges = { From 783053efd0c96a48470f8d7cd4bd7908ff34e01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 23 Jul 2024 14:09:06 +0100 Subject: [PATCH 287/430] fix(web): fix root auth tests --- web/src/components/users/RootAuthMethods.test.jsx | 2 +- web/src/components/users/RootPasswordPopup.test.jsx | 2 +- web/src/components/users/RootSSHKeyPopup.test.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/users/RootAuthMethods.test.jsx b/web/src/components/users/RootAuthMethods.test.jsx index ea1c5f22dd..14b3222590 100644 --- a/web/src/components/users/RootAuthMethods.test.jsx +++ b/web/src/components/users/RootAuthMethods.test.jsx @@ -33,7 +33,7 @@ let mockSSHKey; jest.mock("~/queries/users", () => ({ ...jest.requireActual("~/queries/users"), - useRootUser: () => ({ data: { password: mockPassword, sshkey: mockSSHKey } }), + useRootUser: () => ({ password: mockPassword, sshkey: mockSSHKey }), useRootUserMutation: () => mockRootUserMutation, useRootUserChanges: () => jest.fn(), })); diff --git a/web/src/components/users/RootPasswordPopup.test.jsx b/web/src/components/users/RootPasswordPopup.test.jsx index 833e388019..b2a825c477 100644 --- a/web/src/components/users/RootPasswordPopup.test.jsx +++ b/web/src/components/users/RootPasswordPopup.test.jsx @@ -31,7 +31,7 @@ let mockSSHKey; jest.mock("~/queries/users", () => ({ ...jest.requireActual("~/queries/users"), - useRootUser: () => ({ data: { password: mockPassword, sshkey: "" } }), + useRootUser: () => ({ password: mockPassword, sshkey: "" }), useRootUserMutation: () => mockRootUserMutation, useRootUserChanges: () => jest.fn(), })); diff --git a/web/src/components/users/RootSSHKeyPopup.test.jsx b/web/src/components/users/RootSSHKeyPopup.test.jsx index e68bcc8bcb..da0dff615f 100644 --- a/web/src/components/users/RootSSHKeyPopup.test.jsx +++ b/web/src/components/users/RootSSHKeyPopup.test.jsx @@ -30,7 +30,7 @@ let mockSSHKey; jest.mock("~/queries/users", () => ({ ...jest.requireActual("~/queries/users"), - useRootUser: () => ({ data: { sshkey: mockSSHKey } }), + useRootUser: () => ({ sshkey: mockSSHKey }), useRootUserMutation: () => mockRootUserMutation, useRootUserChanges: () => jest.fn(), })); From 084e3d39a5cdbdf629eac1b222c6754fe2221371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 23 Jul 2024 14:10:37 +0100 Subject: [PATCH 288/430] refactor(web): users tests clean-up --- web/src/components/users/RootAuthMethods.test.jsx | 9 ++------- web/src/components/users/RootSSHKeyPopup.test.jsx | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/web/src/components/users/RootAuthMethods.test.jsx b/web/src/components/users/RootAuthMethods.test.jsx index 14b3222590..41b8ae686c 100644 --- a/web/src/components/users/RootAuthMethods.test.jsx +++ b/web/src/components/users/RootAuthMethods.test.jsx @@ -20,12 +20,9 @@ */ import React from "react"; -import { act, screen, within } from "@testing-library/react"; -import { plainRender, installerRender, createCallbackMock } from "~/test-utils"; -import { noop } from "~/utils"; - +import { screen, within } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; import { RootAuthMethods } from "~/components/users"; -import { useRootUser, useRootUserMutation } from "~/queries/users"; const mockRootUserMutation = { mutate: jest.fn(), mutateAsync: jest.fn() }; let mockPassword; @@ -38,8 +35,6 @@ jest.mock("~/queries/users", () => ({ useRootUserChanges: () => jest.fn(), })); -const isRootPasswordSetFn = jest.fn(); -const removeRootPasswordFn = jest.fn(); const testKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+ test@example"; beforeEach(() => { diff --git a/web/src/components/users/RootSSHKeyPopup.test.jsx b/web/src/components/users/RootSSHKeyPopup.test.jsx index da0dff615f..33230974d7 100644 --- a/web/src/components/users/RootSSHKeyPopup.test.jsx +++ b/web/src/components/users/RootSSHKeyPopup.test.jsx @@ -21,7 +21,7 @@ import React from "react"; -import { screen, waitFor, within } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { RootSSHKeyPopup } from "~/components/users"; @@ -36,7 +36,6 @@ jest.mock("~/queries/users", () => ({ })); const onCloseCallback = jest.fn(); -const setRootSSHKeyFn = jest.fn(); const testKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+ test@example"; describe("when it is closed", () => { From fc249f873021ffc2d2f7bb478493d5b4f3e2bc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 23 Jul 2024 14:28:33 +0100 Subject: [PATCH 289/430] test(web): simplify QuestionWithPassword tests --- .../questions/QuestionWithPassword.test.jsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/web/src/components/questions/QuestionWithPassword.test.jsx b/web/src/components/questions/QuestionWithPassword.test.jsx index 03603fedda..1bbc50ef51 100644 --- a/web/src/components/questions/QuestionWithPassword.test.jsx +++ b/web/src/components/questions/QuestionWithPassword.test.jsx @@ -21,14 +21,14 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; +import { plainRender } from "~/test-utils"; import { QuestionWithPassword } from "~/components/questions"; let question; const answerFn = jest.fn(); const renderQuestion = () => - installerRender(); + plainRender(); describe("QuestionWithPassword", () => { beforeEach(() => { @@ -42,17 +42,10 @@ describe("QuestionWithPassword", () => { }; }); - it("renders the question text", async () => { + it("renders the question text", () => { renderQuestion(); - await screen.findByText(question.text); - }); - - it("contains a textinput for entering the password", async () => { - renderQuestion(); - - const passwordInput = await screen.findByLabelText("Password"); - expect(passwordInput).not.toBeNull(); + screen.queryByText(question.text); }); describe("when the user selects one of the options", () => { From 22d0125423d3d48fa6eaff194f499534a5d7167e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:00:52 +0100 Subject: [PATCH 290/430] refactor(web): use queries for dealing with software (#1483) Similar to #1439, #1452, and #1470, this set of changes aims to replace the client/software with Tanstack queries. Note that was not possible to fully drop the software client. It has to wait until migration of [`WithStatus`](https://github.com/openSUSE/agama/blob/bd2f35d0ead6d74931189f5619579f6c3ffa2770/web/src/client/mixins.js#L83) and [`WithProgress`](https://github.com/openSUSE/agama/blob/bd2f35d0ead6d74931189f5619579f6c3ffa2770/web/src/client/mixins.js#L147) mixins too. --- web/package-lock.json | 2 +- web/package.json | 2 +- web/src/client/software.js | 59 +--- .../overview/SoftwareSection.test.jsx | 63 ---- .../overview/SoftwareSection.test.tsx | 68 +++++ ...oftwareSection.jsx => SoftwareSection.tsx} | 45 +-- .../components/software/SoftwarePage.test.jsx | 88 ------ .../components/software/SoftwarePage.test.tsx | 62 ++++ .../{SoftwarePage.jsx => SoftwarePage.tsx} | 104 +------ .../software/SoftwarePatternsSelection.jsx | 274 ------------------ .../SoftwarePatternsSelection.test.jsx | 93 ------ .../SoftwarePatternsSelection.test.tsx | 103 +++++++ .../software/SoftwarePatternsSelection.tsx | 215 ++++++++++++++ .../{UsedSize.test.jsx => UsedSize.test.tsx} | 1 - .../software/{UsedSize.jsx => UsedSize.tsx} | 3 +- ...Selection.test.json => patterns.test.json} | 97 ++++--- .../components/software/proposal.test.json | 35 +++ web/src/queries/software.js | 119 -------- web/src/queries/software.ts | 217 ++++++++++++++ web/src/types/registration.ts | 38 +++ web/src/types/software.ts | 77 +++++ web/src/utils.js | 11 + web/tsconfig.json | 2 + 23 files changed, 916 insertions(+), 862 deletions(-) delete mode 100644 web/src/components/overview/SoftwareSection.test.jsx create mode 100644 web/src/components/overview/SoftwareSection.test.tsx rename web/src/components/overview/{SoftwareSection.jsx => SoftwareSection.tsx} (59%) delete mode 100644 web/src/components/software/SoftwarePage.test.jsx create mode 100644 web/src/components/software/SoftwarePage.test.tsx rename web/src/components/software/{SoftwarePage.jsx => SoftwarePage.tsx} (50%) delete mode 100644 web/src/components/software/SoftwarePatternsSelection.jsx delete mode 100644 web/src/components/software/SoftwarePatternsSelection.test.jsx create mode 100644 web/src/components/software/SoftwarePatternsSelection.test.tsx create mode 100644 web/src/components/software/SoftwarePatternsSelection.tsx rename web/src/components/software/{UsedSize.test.jsx => UsedSize.test.tsx} (99%) rename web/src/components/software/{UsedSize.jsx => UsedSize.tsx} (95%) rename web/src/components/software/{SoftwarePatternsSelection.test.json => patterns.test.json} (83%) create mode 100644 web/src/components/software/proposal.test.json delete mode 100644 web/src/queries/software.js create mode 100644 web/src/queries/software.ts create mode 100644 web/src/types/registration.ts create mode 100644 web/src/types/software.ts diff --git a/web/package-lock.json b/web/package-lock.json index a2deca342b..2a2c00d278 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -33,7 +33,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@svgr/plugin-jsx": "^8.1.0", "@svgr/webpack": "^8.1.0", - "@testing-library/jest-dom": "^6.1.4", + "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", diff --git a/web/package.json b/web/package.json index b830def6a1..4dae56cd87 100644 --- a/web/package.json +++ b/web/package.json @@ -38,7 +38,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@svgr/plugin-jsx": "^8.1.0", "@svgr/webpack": "^8.1.0", - "@testing-library/jest-dom": "^6.1.4", + "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", diff --git a/web/src/client/software.js b/web/src/client/software.js index 9e48bcfeb8..a754d8588c 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -47,20 +47,6 @@ const SelectedBy = Object.freeze({ * @property {string} description - Product description */ -/** - * @typedef {object} Registration - * @property {string} requirement - Registration requirement (i.e., "not-required", "optional", - * "mandatory"). - * @property {string|null} code - Registration code, if any. - * @property {string|null} email - Registration email, if any. - */ - -/** - * @typedef {object} RegistrationFailure - * @property {Number} id - ID of error. - * @property {string} message - Failure message. - */ - /** * @typedef {object} ActionResult * @property {boolean} success - Whether the action was successfully done. @@ -115,43 +101,6 @@ class SoftwareBaseClient { return this.client.post("/software/probe", {}); } - /** - * Returns how much space installation takes on disk - * - * @return {Promise} - */ - async getProposal() { - const response = await this.client.get("/software/proposal"); - if (!response.ok) { - console.log("Failed to get software proposal: ", response); - } - - return response.json(); - } - - /** - * Returns available patterns - * - * @return {Promise} - */ - async getPatterns() { - const response = await this.client.get("/software/patterns"); - if (!response.ok) { - console.log("Failed to get software patterns: ", response); - return []; - } - /** @type Array<{ name: string, category: string, summary: string, description: string, order: string, icon: string }> */ - const patterns = await response.json(); - return patterns.map((pattern) => ({ - name: pattern.name, - category: pattern.category, - summary: pattern.summary, - description: pattern.description, - order: parseInt(pattern.order), - icon: pattern.icon, - })); - } - /** * @return {Promise} */ @@ -251,7 +200,7 @@ class ProductClient { /** * Returns the registration of the selected product. * - * @return {Promise} + * @return {Promise} */ async getRegistration() { const response = await this.client.get("/software/registration"); @@ -280,7 +229,7 @@ class ProductClient { async register(code, email = "") { const response = await this.client.post("/software/registration", { key: code, email }); if (response.status === 422) { - /** @type RegistrationFailure */ + /** @type import('~/types/registration').RegistrationFailure */ const body = await response.json(); return { success: false, @@ -303,7 +252,7 @@ class ProductClient { const response = await this.client.delete("/software/registration"); if (response.status === 422) { - /** @type RegistrationFailure */ + /** @type import('~/types/registration').RegistrationFailure */ const body = await response.json(); return { success: false, @@ -320,7 +269,7 @@ class ProductClient { /** * Registers a callback to run when the registration changes. * - * @param {(registration: Registration) => void} handler - Callback function. + * @param {(registration: import('~/types/registration').Registration) => void} handler - Callback function. */ onRegistrationChange(handler) { return this.client.ws().onEvent((event) => { diff --git a/web/src/components/overview/SoftwareSection.test.jsx b/web/src/components/overview/SoftwareSection.test.jsx deleted file mode 100644 index 5c9d779f1f..0000000000 --- a/web/src/components/overview/SoftwareSection.test.jsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; -import { noop } from "~/utils"; -import { createClient } from "~/client"; -import SoftwareSection from "~/components/overview/SoftwareSection"; - -jest.mock("~/client"); - -const gnomePattern = { - name: "gnome", - category: "Graphical Environments", - icon: "./pattern-gnome", - summary: "GNOME Desktop Environment (Wayland)", - order: 1120, -}; - -const kdePattern = { - name: "kde", - category: "Graphical Environments", - icon: "./pattern-kde", - summary: "KDE Applications and Plasma Desktop", - order: 1110, -}; - -beforeEach(() => { - createClient.mockImplementation(() => { - return { - software: { - onSelectedPatternsChanged: noop, - getProposal: jest.fn().mockResolvedValue({ size: "500 MiB", patterns: { kde: 1 } }), - getPatterns: jest.fn().mockResolvedValue([gnomePattern, kdePattern]), - }, - }; - }); -}); - -it.only("renders the required space and the selected patterns", async () => { - installerRender(); - await screen.findByText("500 MiB"); - await screen.findByText(kdePattern.summary); -}); diff --git a/web/src/components/overview/SoftwareSection.test.tsx b/web/src/components/overview/SoftwareSection.test.tsx new file mode 100644 index 0000000000..eb02e06e78 --- /dev/null +++ b/web/src/components/overview/SoftwareSection.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { act, screen } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import mockTestingPatterns from "~/components/software/patterns.test.json"; +import testingProposal from "~/components/software/proposal.test.json"; +import SoftwareSection from "~/components/overview/SoftwareSection"; +import { SoftwareProposal } from "~/types/software"; + +let mockTestingProposal: SoftwareProposal; + +jest.mock("~/queries/software", () => ({ + usePatterns: () => mockTestingPatterns, + useProposal: () => mockTestingProposal, + useProposalChanges: jest.fn(), +})); + +describe("SoftwareSection", () => { + describe("when the proposal does not have patterns to select", () => { + beforeEach(() => { + mockTestingProposal = { patterns: {}, size: "" }; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("when the proposal has patterns to select", () => { + beforeEach(() => { + mockTestingProposal = testingProposal; + }); + + it("renders the required space and the selected patterns", () => { + installerRender(); + screen.getByText("4.6 GiB"); + screen.getAllByText(/GNOME/); + screen.getByText("YaST Base Utilities"); + screen.getByText("YaST Desktop Utilities"); + screen.getByText("Multimedia"); + screen.getAllByText(/Office Software/); + expect(screen.queryByText("KDE")).toBeNull(); + expect(screen.queryByText("XFCE")).toBeNull(); + expect(screen.queryByText("YaST Server Utilities")).toBeNull(); + }); + }); +}); diff --git a/web/src/components/overview/SoftwareSection.jsx b/web/src/components/overview/SoftwareSection.tsx similarity index 59% rename from web/src/components/overview/SoftwareSection.jsx rename to web/src/components/overview/SoftwareSection.tsx index 3070acd1d2..e96d4544e7 100644 --- a/web/src/components/overview/SoftwareSection.jsx +++ b/web/src/components/overview/SoftwareSection.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -19,40 +19,21 @@ * find current contact information at www.suse.com. */ -import React, { useEffect, useState } from "react"; -import { _ } from "~/i18n"; -import { useInstallerClient } from "~/context/installer"; +import React from "react"; import { List, ListItem, Text, TextContent, TextVariants } from "@patternfly/react-core"; import { Em } from "~/components/core"; +import { SelectedBy } from "~/types/software"; +import { usePatterns, useProposal, useProposalChanges } from "~/queries/software"; +import { isObjectEmpty } from "~/utils"; +import { _ } from "~/i18n"; -export default function SoftwareSection() { - const [proposal, setProposal] = useState({}); - const [patterns, setPatterns] = useState([]); - const [selectedPatterns, setSelectedPatterns] = useState(undefined); - const client = useInstallerClient(); - - useEffect(() => { - client.software.getProposal().then(setProposal); - client.software.getPatterns().then(setPatterns); - }, [client]); - - useEffect(() => { - return client.software.onSelectedPatternsChanged(() => { - client.software.getProposal().then(setProposal); - }); - }, [client, setProposal]); - - useEffect(() => { - if (proposal.patterns === undefined) return; +export default function SoftwareSection(): React.ReactNode { + const proposal = useProposal(); + const patterns = usePatterns(); - const ids = Object.keys(proposal.patterns); - const selected = patterns.filter((p) => ids.includes(p.name)).sort((a, b) => a.order - b.order); - setSelectedPatterns(selected); - }, [client, proposal, patterns]); + useProposalChanges(); - if (selectedPatterns === undefined) { - return; - } + if (isObjectEmpty(proposal.patterns)) return; const TextWithoutList = () => { return ( @@ -65,6 +46,8 @@ export default function SoftwareSection() { const TextWithList = () => { // TRANSLATORS: %s will be replaced with the installation size, example: "5GiB". const [msg1, msg2] = _("The installation will take %s including:").split("%s"); + const selectedPatterns = patterns.filter((p) => p.selectedBy !== SelectedBy.NONE); + return ( <> @@ -84,7 +67,7 @@ export default function SoftwareSection() { return ( {_("Software")} - {selectedPatterns.length ? : } + {patterns.length ? : } ); } diff --git a/web/src/components/software/SoftwarePage.test.jsx b/web/src/components/software/SoftwarePage.test.jsx deleted file mode 100644 index fa920c204a..0000000000 --- a/web/src/components/software/SoftwarePage.test.jsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; - -import { act, screen, within } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; -import { BUSY, IDLE } from "~/client/status"; -import { createClient } from "~/client"; -import test_patterns from "./SoftwarePatternsSelection.test.json"; -import SoftwarePage from "./SoftwarePage"; - -jest.mock("~/client"); - -const getStatusFn = jest.fn(); -const onStatusChangeFn = jest.fn(); -const onSelectedPatternsChangedFn = jest.fn(); -const selectPatternsFn = jest.fn(); -const proposal = { - patterns: { yast2_basis: 1 }, - size: "1.8 GiB", -}; - -beforeEach(() => { - createClient.mockImplementation(() => { - return { - software: { - getStatus: getStatusFn, - onStatusChange: onStatusChangeFn, - onSelectedPatternsChanged: onSelectedPatternsChangedFn, - getPatterns: jest.fn().mockResolvedValue(test_patterns), - getProposal: jest.fn().mockResolvedValue(proposal), - selectPatterns: selectPatternsFn, - }, - }; - }); -}); - -jest.mock("@patternfly/react-core", () => { - const original = jest.requireActual("@patternfly/react-core"); - - return { - ...original, - Skeleton: () => Skeleton Mock, - }; -}); - -describe.skip("SoftwarePage", () => { - it("displays a progress when the backend in busy", async () => { - getStatusFn.mockResolvedValue(BUSY); - await act(async () => installerRender()); - screen.getAllByText("Skeleton Mock"); - }); - - it("clicking in a pattern's checkbox selects the pattern", async () => { - getStatusFn.mockResolvedValue(IDLE); - - const { user } = installerRender(); - const button = await screen.findByRole("button", { name: "Change selection" }); - await user.click(button); - - const basePatterns = await screen.findByRole("region", { - name: "Base Technologies", - }); - const row = await within(basePatterns).findByRole("row", { name: /YaST Base/ }); - const checkbox = await within(row).findByRole("checkbox"); - - expect(checkbox).toBeChecked(); - }); -}); diff --git a/web/src/components/software/SoftwarePage.test.tsx b/web/src/components/software/SoftwarePage.test.tsx new file mode 100644 index 0000000000..5e8810cb7a --- /dev/null +++ b/web/src/components/software/SoftwarePage.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; + +import { screen } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import testingPatterns from "./patterns.test.json"; +import testingProposal from "./proposal.test.json"; +import SoftwarePage from "./SoftwarePage"; + +jest.mock("~/queries/issues", () => ({ + useIssues: () => [], +})); + +jest.mock("~/queries/software", () => ({ + usePatterns: () => testingPatterns, + useProposal: () => testingProposal, + useProposalChanges: jest.fn(), +})); + +describe("SoftwarePage", () => { + it("renders a list of selected patterns", () => { + installerRender(); + screen.getAllByText(/GNOME/); + screen.getByText("YaST Base Utilities"); + screen.getByText("YaST Desktop Utilities"); + screen.getByText("Multimedia"); + screen.getAllByText(/Office software/); + expect(screen.queryByText("KDE")).toBeNull(); + expect(screen.queryByText("XFCE")).toBeNull(); + expect(screen.queryByText("YaST Server Utilities")).toBeNull(); + }); + + it("renders amount of size selected product and patterns will need", () => { + installerRender(); + screen.getByText("Installation will take 4.6 GiB."); + }); + + it("renders a button for navigating to patterns selection", () => { + installerRender(); + screen.getByRole("link", { name: "Change selection" }); + }); +}); diff --git a/web/src/components/software/SoftwarePage.jsx b/web/src/components/software/SoftwarePage.tsx similarity index 50% rename from web/src/components/software/SoftwarePage.jsx rename to web/src/components/software/SoftwarePage.tsx index 001daf5927..fefe2ea381 100644 --- a/web/src/components/software/SoftwarePage.jsx +++ b/web/src/components/software/SoftwarePage.tsx @@ -19,18 +19,7 @@ * find current contact information at www.suse.com. */ -// @ts-check - -import React, { useEffect, useState } from "react"; - -import { useInstallerClient } from "~/context/installer"; -import { useCancellablePromise } from "~/utils"; -import { useIssues } from "~/queries/issues"; -import { BUSY } from "~/client/status"; -import { _ } from "~/i18n"; -import { ButtonLink, CardField, IssuesHint, Page, SectionSkeleton } from "~/components/core"; -import UsedSize from "./UsedSize"; -import { SelectedBy } from "~/client/software"; +import React from "react"; import { CardBody, DescriptionList, @@ -41,44 +30,17 @@ import { GridItem, Stack, } from "@patternfly/react-core"; - -/** - * @typedef {Object} Pattern - * @property {string} name - pattern name (internal ID) - * @property {string} category - pattern category - * @property {string} summary - pattern name (user visible) - * @property {string} description - long description of the pattern - * @property {number} order - display order (string!) - * @property {number} selectedBy - who selected the pattern - */ - -/** - * Builds a list of patterns include its selection status - * - * @param {import("~/client/software").Pattern[]} patterns - Patterns from the HTTP API - * @param {Object.} selection - Patterns selection - * @return {Pattern[]} List of patterns including its selection status - */ -function buildPatterns(patterns, selection) { - return patterns - .map((pattern) => { - const selectedBy = selection[pattern.name] !== undefined ? selection[pattern.name] : 2; - return { - ...pattern, - selectedBy, - }; - }) - .sort((a, b) => a.order - b.order); -} +import { ButtonLink, CardField, IssuesHint, Page } from "~/components/core"; +import UsedSize from "./UsedSize"; +import { useIssues } from "~/queries/issues"; +import { usePatterns, useProposal, useProposalChanges } from "~/queries/software"; +import { Pattern, SelectedBy } from "~/types/software"; +import { _ } from "~/i18n"; /** * List of selected patterns. - * @component - * @param {object} props - * @param {Pattern[]} props.patterns - List of patterns, including selected and unselected ones. - * @return {JSX.Element} */ -const SelectedPatternsList = ({ patterns }) => { +const SelectedPatternsList = ({ patterns }: { patterns: Pattern[] }): React.ReactNode => { const selected = patterns.filter((p) => p.selectedBy !== SelectedBy.NONE); if (selected.length === 0) { @@ -100,7 +62,7 @@ const SelectedPatternsList = ({ patterns }) => { ); }; -const SelectedPatterns = ({ patterns }) => ( +const SelectedPatterns = ({ patterns }): React.ReactNode => ( ( ); -const NoPatterns = () => ( +const NoPatterns = (): React.ReactNode => (

      @@ -126,54 +88,16 @@ const NoPatterns = () => ( ); -// FIXME: move build patterns to utils /** * Software page component - * @component - * @returns {JSX.Element} */ -function SoftwarePage() { +function SoftwarePage(): React.ReactNode { const issues = useIssues("software"); - const [status, setStatus] = useState(BUSY); - const [patterns, setPatterns] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [proposal, setProposal] = useState({ patterns: {}, size: "" }); - const client = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - - useEffect(() => { - cancellablePromise(client.software.getStatus().then(setStatus)); - - return client.software.onStatusChange(setStatus); - }, [client, cancellablePromise]); + const proposal = useProposal(); + const patterns = usePatterns(); - useEffect(() => { - if (!patterns) return; - - return client.software.onSelectedPatternsChanged((selection) => { - client.software.getProposal().then((proposal) => setProposal(proposal)); - setPatterns(buildPatterns(patterns, selection)); - }); - }, [client.software, patterns]); - - useEffect(() => { - if (!isLoading) return; - - const loadPatterns = async () => { - const patterns = await cancellablePromise(client.software.getPatterns()); - const proposal = await cancellablePromise(client.software.getProposal()); - setPatterns(buildPatterns(patterns, proposal.patterns)); - setProposal(proposal); - setIsLoading(false); - }; - - loadPatterns(); - }, [client.software, patterns, cancellablePromise, isLoading]); - - if (status === BUSY || isLoading) { - ; - } + useProposalChanges(); return ( <> diff --git a/web/src/components/software/SoftwarePatternsSelection.jsx b/web/src/components/software/SoftwarePatternsSelection.jsx deleted file mode 100644 index 5f0acdf647..0000000000 --- a/web/src/components/software/SoftwarePatternsSelection.jsx +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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 version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React, { useCallback, useEffect, useState } from "react"; -import { - Card, - CardBody, - Label, - DataList, - DataListCell, - DataListCheck, - DataListItem, - DataListItemCells, - DataListItemRow, - SearchInput, - Stack, -} from "@patternfly/react-core"; - -import { Section, Page } from "~/components/core"; -import { _ } from "~/i18n"; -import { SelectedBy } from "~/client/software"; -import { useInstallerClient } from "~/context/installer"; -import { useCancellablePromise } from "~/utils"; - -/** - * @typedef {Object} Pattern - * @property {string} name pattern name (internal ID) - * @property {string} group pattern group - * @property {string} summary pattern name (user visible) - * @property {string} description long description of the pattern - * @property {string} order display order - * @property {string} icon icon name (not path or file name!) - * @property {number} selected who selected the pattern, undefined - * means it is not selected to install - */ - -/** - * @typedef {Object.} PatternGroups mapping "group name" => - * list of patterns - */ - -/** - * Group the patterns with the same group name - * @param {Array} patterns input - * @return {PatternGroups} - */ -function groupPatterns(patterns) { - const groups = {}; - - patterns.forEach((pattern) => { - if (groups[pattern.category]) { - groups[pattern.category].push(pattern); - } else { - groups[pattern.category] = [pattern]; - } - }); - - // sort patterns by the "order" value - Object.keys(groups).forEach((group) => { - groups[group].sort((p1, p2) => { - if (p1.order === p2.order) { - // there should be no patterns with the same name - return p1.name < p2.name ? -1 : 1; - } else { - return p1.order - p2.order; - } - }); - }); - - return groups; -} - -/** - * Sort pattern group names - * @param {PatternGroups} groups input - * @returns {Array} sorted pattern group names - */ -function sortGroups(groups) { - return Object.keys(groups).sort((g1, g2) => { - const order1 = groups[g1][0].order; - const order2 = groups[g2][0].order; - return order1 - order2; - }); -} - -/** - * Builds a list of patterns include its selection status - * - * @param {import("~/client/software").Pattern[]} patterns - Patterns from the HTTP API - * @param {Object.} selection - Patterns selection - * @return {Pattern[]} List of patterns including its selection status - */ -function buildPatterns(patterns, selection) { - return patterns - .map((pattern) => { - const selectedBy = selection[pattern.name] !== undefined ? selection[pattern.name] : 2; - return { - ...pattern, - selectedBy, - }; - }) - .sort((a, b) => a.order - b.order); -} - -/** - * Pattern selector component - */ -function SoftwarePatternsSelection() { - const client = useInstallerClient(); - const [patterns, setPatterns] = useState([]); - const [proposal, setProposal] = useState({ patterns: {}, size: "" }); - const [isLoading, setIsLoading] = useState(true); - const [visiblePatterns, setVisiblePatterns] = useState(patterns); - const [searchValue, setSearchValue] = useState(""); - const { cancellablePromise } = useCancellablePromise(); - - useEffect(() => { - if (patterns.length !== 0) return; - - const loadPatterns = async () => { - const patterns = await cancellablePromise(client.software.getPatterns()); - const proposal = await cancellablePromise(client.software.getProposal()); - setPatterns(buildPatterns(patterns, proposal.patterns)); - setProposal(proposal); - setIsLoading(false); - }; - - loadPatterns(); - }, [client.software, patterns, cancellablePromise]); - - useEffect(() => { - if (!patterns) return; - - // filtering - search the required text in the name and pattern description - if (searchValue !== "") { - // case insensitive search - const searchData = searchValue.toUpperCase(); - const filtered = patterns.filter( - (p) => - p.name.toUpperCase().indexOf(searchData) !== -1 || - p.description.toUpperCase().indexOf(searchData) !== -1, - ); - setVisiblePatterns(filtered); - } else { - setVisiblePatterns(patterns); - } - - return client.software.onSelectedPatternsChanged((selection) => { - client.software.getProposal().then((proposal) => setProposal(proposal)); - setPatterns(buildPatterns(patterns, selection)); - }); - }, [patterns, searchValue, client.software]); - - const onToggle = useCallback( - (name) => { - const selected = patterns - .filter((p) => p.selectedBy === SelectedBy.USER) - .reduce((all, p) => { - all[p.name] = true; - return all; - }, {}); - const pattern = patterns.find((p) => p.name === name); - selected[name] = pattern.selectedBy === SelectedBy.NONE; - - client.software.selectPatterns(selected); - }, - [patterns, client.software], - ); - - // FIXME: use loading indicator when busy, we cannot know if it will be - // quickly or not in advance. - - // initial empty screen, the patterns are loaded very quickly, no need for any progress - if (visiblePatterns.length === 0 && searchValue === "") return null; - - const groups = groupPatterns(visiblePatterns); - - // FIXME: use a switch instead of a checkbox since these patterns are going to - // be selected/deselected immediately. - // TODO: extract to a DataListSelector component or so. - let selector = sortGroups(groups).map((groupName) => { - const selectedIds = groups[groupName] - .filter((p) => p.selectedBy !== SelectedBy.NONE) - .map((p) => p.name); - return ( -

      - - {groups[groupName].map((option) => ( - - - onToggle(option.name)} - aria-labelledby="check-action-item1" - name="check-action-check1" - isChecked={selectedIds.includes(option.name)} - /> - - -
      - {option.summary}{" "} - {option.selectedBy === SelectedBy.AUTO && ( - - )} -
      -
      {option.description}
      -
      - , - ]} - /> -
      -
      - ))} -
      -
      - ); - }); - - if (selector.length === 0) { - selector = {_("None of the patterns match the filter.")}; - } - - return ( - <> - - -

      {_("Software selection")}

      - setSearchValue(value)} - onClear={() => setSearchValue("")} - resultsCount={visiblePatterns.length} - /> -
      -
      - - - - {selector} - - - - - {_("Close")} - - - ); -} - -export default SoftwarePatternsSelection; diff --git a/web/src/components/software/SoftwarePatternsSelection.test.jsx b/web/src/components/software/SoftwarePatternsSelection.test.jsx deleted file mode 100644 index 1faf1225da..0000000000 --- a/web/src/components/software/SoftwarePatternsSelection.test.jsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; - -import test_patterns from "./SoftwarePatternsSelection.test.json"; -import SoftwarePatternsSelection from "./SoftwarePatternsSelection"; -import { SelectedBy } from "~/client/software"; - -const patterns = test_patterns.map((p) => ({ ...p, selectedBy: SelectedBy.NONE })); - -describe.skip("SoftwarePatternsSelection", () => { - it("displays the pattern groups in the correct order", () => { - plainRender(); - const headings = screen.getAllByRole("heading", { level: 2 }); - const headingsText = headings.map((node) => node.textContent); - expect(headingsText).toEqual([ - "Graphical Environments", - "Base Technologies", - "Desktop Functions", - ]); - }); - - it("displays the patterns in a group in correct order", async () => { - plainRender(); - - // the "Base Technologies" pattern group - const baseGroup = await screen.findByRole("region", { name: "Base Technologies" }); - - // the pattern names - const rows = within(baseGroup).getAllByRole("row"); - - expect(rows[0]).toHaveTextContent(/YaST Base Utilities/); - expect(rows[1]).toHaveTextContent(/YaST Desktop Utilities/); - expect(rows[2]).toHaveTextContent(/YaST Server Utilities/); - }); - - it("displays only the matching patterns when using the search filter", async () => { - const { user } = plainRender(); - - // enter "multimedia" into the search filter - const searchFilter = await screen.findByRole("textbox", { name: "Search" }); - await user.type(searchFilter, "multimedia"); - - const headings = screen.getAllByRole("heading", { level: 2 }); - const headingsText = headings.map((node) => node.textContent); - expect(headingsText).toEqual(["Desktop Functions"]); - - const desktopGroup = screen.getByRole("region", { name: "Desktop Functions" }); - expect(within(desktopGroup).queryByRole("row", { name: /Multimedia/ })).toBeInTheDocument(); - expect( - within(desktopGroup).queryByRole("row", { name: /Office Software/ }), - ).not.toBeInTheDocument(); - }); - - it("displays the checkbox depending whether the patter is selected", async () => { - const pattern = patterns.find((p) => p.name === "yast2_basis"); - pattern.selectedBy = SelectedBy.USER; - - plainRender(); - - // the "Base Technologies" pattern group - const baseGroup = await screen.findByRole("region", { name: "Base Technologies" }); - - const rowBasis = within(baseGroup).getByRole("row", { name: /YaST Base/ }); - const checkboxBasis = await within(rowBasis).findByRole("checkbox"); - expect(checkboxBasis).toBeChecked(); - - const rowDesktop = within(baseGroup).getByRole("row", { name: /YaST Desktop/ }); - const checkboxDesktop = await within(rowDesktop).findByRole("checkbox"); - expect(checkboxDesktop).not.toBeChecked(); - }); -}); diff --git a/web/src/components/software/SoftwarePatternsSelection.test.tsx b/web/src/components/software/SoftwarePatternsSelection.test.tsx new file mode 100644 index 0000000000..002e3a053e --- /dev/null +++ b/web/src/components/software/SoftwarePatternsSelection.test.tsx @@ -0,0 +1,103 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import testingPatterns from "./patterns.test.json"; +import SoftwarePatternsSelection from "./SoftwarePatternsSelection"; + +const onConfigMutationMock = { mutate: jest.fn() }; + +jest.mock("~/queries/software", () => ({ + usePatterns: () => testingPatterns, + useConfigMutation: () => onConfigMutationMock, +})); + +describe("SoftwarePatternsSelection", () => { + it("displays the pattern in the correct order", async () => { + installerRender(); + const headings = screen.getAllByRole("heading", { level: 3 }); + const headingsText = headings.map((node) => node.textContent); + expect(headingsText).toEqual([ + "Graphical Environments", + "Base Technologies", + "Desktop Functions", + ]); + + // the "Base Technologies" pattern group + const baseGroup = await screen.findByRole("list", { name: "Base Technologies" }); + // the "Base Technologies" pattern items + const items = within(baseGroup).getAllByRole("listitem"); + expect(items[0]).toHaveTextContent(/YaST Base Utilities/); + expect(items[1]).toHaveTextContent(/YaST Desktop Utilities/); + expect(items[2]).toHaveTextContent(/YaST Server Utilities/); + }); + + it("displays only the matching patterns when filtering", async () => { + const { user } = installerRender(); + + // enter "multimedia" into the search filter + const searchFilter = await screen.findByRole("textbox", { name: /Filter/ }); + await user.type(searchFilter, "multimedia"); + + const headings = screen.getAllByRole("heading", { level: 3 }); + const headingsText = headings.map((node) => node.textContent); + expect(headingsText).toEqual(["Desktop Functions"]); + + const desktopGroup = screen.getByRole("list", { name: "Desktop Functions" }); + expect( + within(desktopGroup).queryByRole("listitem", { name: /Multimedia/ }), + ).toBeInTheDocument(); + expect( + within(desktopGroup).queryByRole("listitem", { name: /Office Software/ }), + ).not.toBeInTheDocument(); + }); + + it("displays the checkbox reflecting the current pattern selection status", async () => { + installerRender(); + + // the "Base Technologies" pattern group + const baseGroup = await screen.findByRole("list", { name: "Base Technologies" }); + + const basisItem = within(baseGroup).getByRole("listitem", { name: /YaST Base/ }); + const basisCheckbox = await within(basisItem).findByRole("checkbox"); + expect(basisCheckbox).toBeChecked(); + + const serverItem = within(baseGroup).getByRole("listitem", { name: /YaST Server/ }); + const serverCheckbox = await within(serverItem).findByRole("checkbox"); + expect(serverCheckbox).not.toBeChecked(); + }); + + it("allows changing the selection", async () => { + const { user } = installerRender(); + const y2BasisPattern = testingPatterns.find((p) => p.name === "yast2_basis"); + + const basisItem = screen.getByRole("listitem", { name: y2BasisPattern.summary }); + const basisCheckbox = await within(basisItem).findByRole("checkbox"); + expect(basisCheckbox).toBeChecked(); + + await user.click(basisCheckbox); + expect(onConfigMutationMock.mutate).toHaveBeenCalledWith({ + patterns: expect.objectContaining({ yast2_basis: false }), + }); + }); +}); diff --git a/web/src/components/software/SoftwarePatternsSelection.tsx b/web/src/components/software/SoftwarePatternsSelection.tsx new file mode 100644 index 0000000000..4b7648d748 --- /dev/null +++ b/web/src/components/software/SoftwarePatternsSelection.tsx @@ -0,0 +1,215 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; +import { + Card, + CardBody, + Label, + DataList, + DataListCell, + DataListCheck, + DataListItem, + DataListItemCells, + DataListItemRow, + SearchInput, + Stack, +} from "@patternfly/react-core"; +import { Page } from "~/components/core"; +import { useConfigMutation, usePatterns } from "~/queries/software"; +import { Pattern, SelectedBy } from "~/types/software"; +import { _ } from "~/i18n"; +import a11yStyles from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; + +/** + * PatternGroups mapping "group name" => list of patterns + */ +type PatternsGroups = { [key: string]: Pattern[] }; + +/** + * Group the patterns with the same group name + */ +function groupPatterns(patterns: Pattern[]): PatternsGroups { + const groups = {}; + + patterns.forEach((pattern) => { + if (groups[pattern.category]) { + groups[pattern.category].push(pattern); + } else { + groups[pattern.category] = [pattern]; + } + }); + + // sort patterns by the "order" value + Object.keys(groups).forEach((group) => { + groups[group].sort((p1, p2) => { + if (p1.order === p2.order) { + // there should be no patterns with the same name + return p1.name < p2.name ? -1 : 1; + } else { + return p1.order - p2.order; + } + }); + }); + + return groups; +} + +/** + * Sort pattern group names + */ +function sortGroups(groups: PatternsGroups): string[] { + return Object.keys(groups).sort((g1, g2) => { + const order1 = groups[g1][0].order; + const order2 = groups[g2][0].order; + return order1 - order2; + }); +} + +const filterPatterns = (patterns: Pattern[] = [], searchValue = ""): Pattern[] => { + if (searchValue.trim() === "") return patterns; + + // case insensitive search + const searchData = searchValue.toUpperCase(); + return patterns.filter( + (p) => + p.name.toUpperCase().indexOf(searchData) !== -1 || + p.description.toUpperCase().indexOf(searchData) !== -1, + ); +}; + +const NoMatches = (): React.ReactNode => {_("None of the patterns match the filter.")}; + +/** + * Pattern selector component + */ +function SoftwarePatternsSelection(): React.ReactNode { + const patterns = usePatterns(); + const config = useConfigMutation(); + const [searchValue, setSearchValue] = useState(""); + + const onToggle = (name: string) => { + const selected = patterns + .filter((p) => p.selectedBy === SelectedBy.USER) + .reduce((all, p) => { + all[p.name] = true; + return all; + }, {}); + const pattern = patterns.find((p) => p.name === name); + selected[name] = pattern.selectedBy === SelectedBy.NONE; + + config.mutate({ patterns: selected }); + }; + + // FIXME: use loading indicator when busy, we cannot know if it will be + // quickly or not in advance. + + // initial empty screen, the patterns are loaded very quickly, no need for any progress + const visiblePatterns = filterPatterns(patterns, searchValue); + if (visiblePatterns.length === 0 && searchValue === "") return null; + + const groups = groupPatterns(visiblePatterns); + + // FIXME: use a switch instead of a checkbox since these patterns are going to + // be selected/deselected immediately. + // TODO: extract to a DataListSelector component or so. + const selector = sortGroups(groups).map((groupName) => { + const selectedIds = groups[groupName] + .filter((p) => p.selectedBy !== SelectedBy.NONE) + .map((p) => p.name); + return ( +
      +

      {groupName}

      + + {groups[groupName].map((option) => { + const titleId = `${option.name}-title`; + const descId = `${option.name}-desc`; + const selected = selectedIds.includes(option.name); + const nextActionId = `${option.name}-next-action`; + + return ( + + + onToggle(option.name)} + aria-labelledby={[nextActionId, titleId].join(" ")} + isChecked={selected} + /> + + +
      + {option.summary}{" "} + {option.selectedBy === SelectedBy.AUTO && ( + + )} + + {selected ? _("Unselect") : _("Select")} + +
      +
      {option.description}
      +
      + , + ]} + /> +
      +
      + ); + })} +
      +
      + ); + }); + + return ( + <> + + +

      {_("Software selection")}

      + setSearchValue(value)} + onClear={() => setSearchValue("")} + resultsCount={visiblePatterns.length} + /> +
      +
      + + + + {selector.length > 0 ? selector : } + + + + + {_("Close")} + + + ); +} + +export default SoftwarePatternsSelection; diff --git a/web/src/components/software/UsedSize.test.jsx b/web/src/components/software/UsedSize.test.tsx similarity index 99% rename from web/src/components/software/UsedSize.test.jsx rename to web/src/components/software/UsedSize.test.tsx index fae1607f28..12610ec3cd 100644 --- a/web/src/components/software/UsedSize.test.jsx +++ b/web/src/components/software/UsedSize.test.tsx @@ -22,7 +22,6 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; - import UsedSize from "./UsedSize"; describe("UsedSize", () => { diff --git a/web/src/components/software/UsedSize.jsx b/web/src/components/software/UsedSize.tsx similarity index 95% rename from web/src/components/software/UsedSize.jsx rename to web/src/components/software/UsedSize.tsx index 490d8a9684..e4e2d9b40f 100644 --- a/web/src/components/software/UsedSize.jsx +++ b/web/src/components/software/UsedSize.tsx @@ -20,12 +20,11 @@ */ import React from "react"; - import { EmptyState } from "~/components/core"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -export default function UsedSize({ size }) { +export default function UsedSize({ size }: { size?: string }) { if (size === undefined || size === "" || size === "0 B") return null; // TRANSLATORS: %s will be replaced by the estimated installation size, diff --git a/web/src/components/software/SoftwarePatternsSelection.test.json b/web/src/components/software/patterns.test.json similarity index 83% rename from web/src/components/software/SoftwarePatternsSelection.test.json rename to web/src/components/software/patterns.test.json index 58c301ba47..b5c10a3c50 100644 --- a/web/src/components/software/SoftwarePatternsSelection.test.json +++ b/web/src/components/software/patterns.test.json @@ -1,51 +1,57 @@ [ { - "name": "xfce", + "name": "gnome", "category": "Graphical Environments", - "icon": "./pattern-xfce", - "description": "Xfce is a lightweight desktop environment for various *NIX systems.", - "summary": "XFCE Desktop Environment", - "order": "1310" + "icon": "./pattern-gnome-wayland", + "description": "The GNOME desktop environment is an intuitive and attractive desktop for users.\nThis pattern installs components for GNOME to run with Wayland and X11 technologies.", + "summary": "GNOME Desktop Environment (Wayland)", + "order": "1010", + "selectedBy": 0 }, { - "name": "basic_desktop", + "name": "kde", "category": "Graphical Environments", - "icon": "./pattern-x11", - "description": "This pattern installs a rather basic desktop (icewm)", - "summary": "A very basic desktop (previously part of x11 pattern)", - "order": "1802" + "icon": "./pattern-kde", + "description": "Packages providing the Plasma desktop environment and applications from KDE.", + "summary": "KDE Applications and Plasma 5 Desktop", + "order": "1110", + "selectedBy": 2 }, { - "name": "yast2_server", + "name": "yast2_basis", "category": "Base Technologies", "icon": "./yast", - "description": "YaST tools for server system administration.", - "summary": "YaST Server Utilities", - "order": "1224" + "description": "YaST tools for basic system administration.", + "summary": "YaST Base Utilities", + "order": "1220", + "selectedBy": 1 }, { - "name": "office", - "category": "Desktop Functions", - "icon": "./pattern-office", - "description": "Office software for your desktop environment including LibreOffice.", - "summary": "Office Software", - "order": "1640" + "name": "yast2_desktop", + "category": "Base Technologies", + "icon": "./yast", + "description": "YaST tools for desktop system administration.", + "summary": "YaST Desktop Utilities", + "order": "1222", + "selectedBy": 1 }, { - "name": "gnome", - "category": "Graphical Environments", - "icon": "./pattern-gnome-wayland", - "description": "The GNOME desktop environment is an intuitive and attractive desktop for users.\nThis pattern installs components for GNOME to run with Wayland and X11 technologies.", - "summary": "GNOME Desktop Environment (Wayland)", - "order": "1010" + "name": "yast2_server", + "category": "Base Technologies", + "icon": "./yast", + "description": "YaST tools for server system administration.", + "summary": "YaST Server Utilities", + "order": "1224", + "selectedBy": 2 }, { - "name": "kde", + "name": "xfce", "category": "Graphical Environments", - "icon": "./pattern-kde", - "description": "Packages providing the Plasma desktop environment and applications from KDE.", - "summary": "KDE Applications and Plasma Desktop", - "order": "1110" + "icon": "./pattern-xfce", + "description": "Xfce is a lightweight desktop environment for various *NIX systems.", + "summary": "XFCE Desktop Environment", + "order": "1310", + "selectedBy": 2 }, { "name": "multimedia", @@ -53,22 +59,25 @@ "icon": "./pattern-multimedia", "description": "Multimedia players, sound editing tools, video and image manipulation applications.", "summary": "Multimedia", - "order": "1580" + "order": "1580", + "selectedBy": 1 }, { - "name": "yast2_basis", - "category": "Base Technologies", - "icon": "./yast", - "description": "YaST tools for basic system administration.", - "summary": "YaST Base Utilities", - "order": "1220" + "name": "office", + "category": "Desktop Functions", + "icon": "./pattern-office", + "description": "Office software for your desktop environment including LibreOffice.", + "summary": "Office Software", + "order": "1640", + "selectedBy": 1 }, { - "name": "yast2_desktop", - "category": "Base Technologies", - "icon": "./yast", - "description": "YaST tools for desktop system administration.", - "summary": "YaST Desktop Utilities", - "order": "1222" + "name": "basic_desktop", + "category": "Graphical Environments", + "icon": "./pattern-x11", + "description": "This pattern installs a rather basic desktop (icewm)", + "summary": "A very basic desktop (previously part of x11 pattern)", + "order": "1802", + "selectedBy": 2 } ] diff --git a/web/src/components/software/proposal.test.json b/web/src/components/software/proposal.test.json new file mode 100644 index 0000000000..e2166a658d --- /dev/null +++ b/web/src/components/software/proposal.test.json @@ -0,0 +1,35 @@ +{ + "size": "4.6 GiB", + "patterns": { + "fonts": 1, + "gnome_basis_opt": 1, + "minimal_base": 1, + "gnome_imaging": 1, + "fonts_opt": 1, + "x86_64_v3": 1, + "gnome_office": 1, + "x11_yast": 1, + "gnome": 0, + "base": 1, + "sw_management": 1, + "x11": 1, + "gnome_utilities": 1, + "enhanced_base": 1, + "sw_management_gnome": 1, + "gnome_basic": 1, + "x11_enhanced": 1, + "gnome_x11": 1, + "office": 1, + "yast2_desktop": 1, + "gnome_basis": 1, + "basesystem": 1, + "multimedia": 1, + "apparmor": 1, + "yast2_basis": 1, + "gnome_games": 1, + "imaging": 1, + "gnome_multimedia": 1, + "gnome_yast": 1, + "gnome_internet": 1 + } +} diff --git a/web/src/queries/software.js b/web/src/queries/software.js deleted file mode 100644 index 8aa17911b1..0000000000 --- a/web/src/queries/software.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { - QueryClient, - useMutation, - useQueryClient, - useSuspenseQueries, -} from "@tanstack/react-query"; -import { useInstallerClient } from "~/context/installer"; - -const configQuery = () => ({ - queryKey: ["software/config"], - queryFn: () => fetch("/api/software/config").then((res) => res.json()), -}); - -const selectedProductQuery = () => ({ - queryKey: ["software/product"], - queryFn: async () => { - const response = await fetch("/api/software/config"); - const { product } = await response.json(); - return product; - }, -}); - -const productsQuery = () => ({ - queryKey: ["software/products"], - queryFn: () => fetch("/api/software/products").then((res) => res.json()), - staleTime: Infinity, -}); - -/** - * Hook that builds a mutation to update the software configuration - * - * It does not require to call `useMutation`. - */ -const useConfigMutation = () => { - const queryClient = useQueryClient(); - const client = useInstallerClient(); - - const query = { - mutationFn: (newConfig) => - fetch("/api/software/config", { - // FIXME: use "PATCH" instead - method: "PUT", - body: JSON.stringify(newConfig), - headers: { - "Content-Type": "application/json", - }, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["software/config"] }); - queryClient.invalidateQueries({ queryKey: ["software/product"] }); - client.manager.startProbing(); - }, - }; - return useMutation(query); -}; - -/** - * Hook that returns a useEffect to listen for software events - * - * When the configuration changes, it invalidates the config query and forces the router to - * revalidate its data (executing the loaders again). - */ -const useProductChanges = () => { - const client = useInstallerClient(); - - React.useEffect(() => { - if (!client) return; - const queryClient = new QueryClient(); - - return client.ws().onEvent((event) => { - if (event.type === "ProductChanged") { - queryClient.invalidateQueries({ queryKey: ["software/config"] }); - } - }); - }, [client]); -}; - -const useProduct = () => { - const [{ data: selected }, { data: products }] = useSuspenseQueries({ - queries: [selectedProductQuery(), productsQuery()], - }); - - const selectedProduct = products.find((p) => p.id === selected); - return { - products, - selectedProduct, - }; -}; - -export { - configQuery, - selectedProductQuery, - productsQuery, - useConfigMutation, - useProduct, - useProductChanges, -}; diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts new file mode 100644 index 0000000000..9c29d8c73f --- /dev/null +++ b/web/src/queries/software.ts @@ -0,0 +1,217 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { + useMutation, + useQueryClient, + useSuspenseQueries, + useSuspenseQuery, +} from "@tanstack/react-query"; +import { useInstallerClient } from "~/context/installer"; +import { + Pattern, + PatternsSelection, + Product, + SelectedBy, + SoftwareConfig, + SoftwareProposal, +} from "~/types/software"; + +/** + * Query to retrieve software configuration + */ +const configQuery = () => ({ + queryKey: ["software/config"], + queryFn: () => fetch("/api/software/config").then((res) => res.json()), +}); + +/** + * Query to retrieve current software proposal + */ +const proposalQuery = () => ({ + queryKey: ["software/proposal"], + queryFn: () => fetch("/api/software/proposal").then((res) => res.json()), +}); + +/** + * Query to retrieve available products + */ +const productsQuery = () => ({ + queryKey: ["software/products"], + queryFn: () => fetch("/api/software/products").then((res) => res.json()), + staleTime: Infinity, +}); + +/** + * Query to retrieve selected product + */ +const selectedProductQuery = () => ({ + queryKey: ["software/product"], + queryFn: async () => { + const response = await fetch("/api/software/config"); + const { product } = await response.json(); + return product; + }, +}); + +/** + * Query to retrieve available patterns + */ +const patternsQuery = () => ({ + queryKey: ["software/patterns"], + queryFn: () => fetch("/api/software/patterns").then((res) => res.json()), +}); + +/** + * Hook that builds a mutation to update the software configuration + * + * @note it would trigger a general probing as a side-effect when mutation + * includes a product. + */ +const useConfigMutation = () => { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + + const query = { + mutationFn: (newConfig: SoftwareConfig) => + fetch("/api/software/config", { + // FIXME: use "PATCH" instead + method: "PUT", + body: JSON.stringify(newConfig), + headers: { + "Content-Type": "application/json", + }, + }), + onSuccess: (_, config: SoftwareConfig) => { + queryClient.invalidateQueries({ queryKey: ["software/config"] }); + queryClient.invalidateQueries({ queryKey: ["software/proposal"] }); + if (config.product) { + queryClient.invalidateQueries({ queryKey: ["software/product"] }); + client.manager.startProbing(); + } + }, + }; + return useMutation(query); +}; + +/** + * Returns available products and selected one, if any + */ +const useProduct = (): { products: Product[]; selectedProduct: Product | undefined } => { + const [{ data: selected }, { data: products }] = useSuspenseQueries({ + queries: [selectedProductQuery(), productsQuery()], + }); + + const selectedProduct = products.find((p: Product) => p.id === selected); + return { + products, + selectedProduct, + }; +}; + +/** + * Returns a list of patterns with their selectedBy property properly set based on current proposal. + */ +const usePatterns = (): Pattern[] => { + const [{ data: proposal }, { data: patterns }] = useSuspenseQueries({ + queries: [proposalQuery(), patternsQuery()], + }); + + const selection: PatternsSelection = proposal.patterns; + + return patterns + .map((pattern: Pattern): Pattern => { + let selectedBy: SelectedBy; + switch (selection[pattern.name]) { + case 0: + selectedBy = SelectedBy.USER; + break; + case 1: + selectedBy = SelectedBy.AUTO; + break; + default: + selectedBy = SelectedBy.NONE; + } + return { ...pattern, selectedBy }; + }) + .sort((a: Pattern, b: Pattern) => a.order - b.order); +}; + +/** + * Returns current software proposal + */ +const useProposal = (): SoftwareProposal => { + const { data: proposal } = useSuspenseQuery(proposalQuery()); + return proposal; +}; + +/** + * Hook that returns a useEffect to listen for software proposal events + * + * When the configuration changes, it invalidates the config query. + */ +const useProductChanges = () => { + const client = useInstallerClient(); + const queryClient = useQueryClient(); + + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + if (event.type === "") { + queryClient.invalidateQueries({ queryKey: ["software/config"] }); + } + }); + }, [client, queryClient]); +}; + +/** + * Hook that returns a useEffect to listen for software proposal changes + * + * When the selected patterns change, it invalidates the proposal query. + */ +const useProposalChanges = () => { + const client = useInstallerClient(); + const queryClient = useQueryClient(); + + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + if (event.type === "SoftwareProposalChanged") { + queryClient.invalidateQueries({ queryKey: ["software/proposal"] }); + } + }); + }, [client, queryClient]); +}; + +export { + configQuery, + productsQuery, + selectedProductQuery, + useConfigMutation, + usePatterns, + useProduct, + useProductChanges, + useProposal, + useProposalChanges, +}; diff --git a/web/src/types/registration.ts b/web/src/types/registration.ts new file mode 100644 index 0000000000..6d244326a2 --- /dev/null +++ b/web/src/types/registration.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +type Registration = { + /** Registration requirement (i.e., "not-required", "optional", "mandatory") */ + requirement: string; + /** Registration code, if any */ + code?: string; + /** Registration email, if any */ + email?: string; +}; + +type RegistrationFailure = { + /** @property {Number} id - ID of error */ + id: number; + /** Failure message */ + message: string; +}; + +export type { Registration, RegistrationFailure }; diff --git a/web/src/types/software.ts b/web/src/types/software.ts new file mode 100644 index 0000000000..42a9d8edc9 --- /dev/null +++ b/web/src/types/software.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +/** + * Enum for the reasons to select a pattern + */ +enum SelectedBy { + /** Selected by the user */ + USER = 0, + /** Automatically selected as a dependency of another package */ + AUTO = 1, + /** No selected */ + NONE = 2, +} + +type Product = { + /** Product ID (e.g., "Leap") */ + id: string; + /** Product name (e.g., "openSUSE Leap 15.4") */ + name: string; + /** Product description */ + description: string; +}; + +type PatternsSelection = { [key: string]: SelectedBy }; + +type SoftwareProposal = { + /** Used space in human-readable form */ + size: string; + /** Selected patterns and the reason */ + patterns: PatternsSelection; +}; + +type SoftwareConfig = { + /** Product to install */ + product?: string; + /** An object where the keys are the pattern names and the values whether to install them or not */ + patterns: { [key: string]: boolean }; +}; + +type Pattern = { + /** Pattern name (internal ID) */ + name: string; + /** Pattern category */ + category: string; + /** User visible pattern name */ + summary: string; + /** Long description of the pattern */ + description: string; + /** {number} order - Display order (string!) */ + order: number; + /** Icon name (not path or file name!) */ + icon: string; + /** Whether the pattern if selected and by whom */ + selectedBy?: SelectedBy; +}; + +export { SelectedBy }; +export type { Pattern, PatternsSelection, Product, SoftwareConfig, SoftwareProposal }; diff --git a/web/src/utils.js b/web/src/utils.js index dd90e6cbd3..81975e5c69 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -39,6 +39,16 @@ const isObject = (value) => !(value instanceof Set) && !(value instanceof Map); +/** + * 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 + */ +const isObjectEmpty = (value) => { + return Object.keys(value).length === 0; +}; + /** * Returns an empty function useful to be used as a default callback. * @@ -378,6 +388,7 @@ export { noop, identity, isObject, + isObjectEmpty, partition, compact, uniq, diff --git a/web/tsconfig.json b/web/tsconfig.json index c8a6d2fa16..0696f7cbbd 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -5,9 +5,11 @@ "target": "esnext", "moduleResolution": "node", "resolveJsonModule": true, + "esModuleInterop": true, "allowJs": true, "jsx": "react", "allowSyntheticDefaultImports": true, + "types": ["node", "jest", "@testing-library/jest-dom"], "paths": { "~/*": ["src/*"], "~/client": ["src/client/index.js"], From 5cd74da4b72167513841a9c6a8c9b40c935dd3ac Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 23 Jul 2024 18:09:57 +0200 Subject: [PATCH 291/430] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa Co-authored-by: David Díaz <1691872+dgdavid@users.noreply.github.com> --- web/src/components/questions/QuestionWithPassword.test.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/questions/QuestionWithPassword.test.jsx b/web/src/components/questions/QuestionWithPassword.test.jsx index 1bbc50ef51..c9831d72d0 100644 --- a/web/src/components/questions/QuestionWithPassword.test.jsx +++ b/web/src/components/questions/QuestionWithPassword.test.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2024] SUSE LLC * * All Rights Reserved. * @@ -48,8 +48,8 @@ describe("QuestionWithPassword", () => { screen.queryByText(question.text); }); - describe("when the user selects one of the options", () => { - it("calls the callback after setting both, answer and password", async () => { + describe("when the user enters the password", () => { + it("calls the callback", async () => { const { user } = renderQuestion(); const passwordInput = await screen.findByLabelText("Password"); From 91392624bb5f06e381b18f90d28ee23888024ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:34:39 +0100 Subject: [PATCH 292/430] fix(web): drop software client leftovers (#1496) https://github.com/openSUSE/agama/pull/1483 didn't drop dead software client methods that were replaced by queries. Let's do it now. --- web/src/client/software.js | 54 +------------------------------------- 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/web/src/client/software.js b/web/src/client/software.js index a754d8588c..c1ed9b51fd 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -25,21 +25,6 @@ import { WithProgress, WithStatus } from "./mixins"; const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1"; -/** - * Enum for the reasons to select a pattern - * - * @readonly - * @enum { number } - */ -const SelectedBy = Object.freeze({ - /** Selected by the user */ - USER: 0, - /** Automatically selected as a dependency of another package */ - AUTO: 1, - /** No selected */ - NONE: 2, -}); - /** * @typedef {object} Product * @property {string} id - Product ID (e.g., "Leap") @@ -91,43 +76,6 @@ class SoftwareBaseClient { constructor(client) { this.client = client; } - - /** - * Asks the service to reload the repositories metadata - * - * @return {Promise} - */ - probe() { - return this.client.post("/software/probe", {}); - } - - /** - * @return {Promise} - */ - config() { - return this.client.get("/software/config"); - } - - /** - * @param {Object.} patterns - An object where the keys are the pattern names - * and the values whether to install them or not. - * @return {Promise} - */ - selectPatterns(patterns) { - return this.client.put("/software/config", { patterns }); - } - - /** - * Registers a callback to run when the select product changes. - * - * @param {(changes: object) => void} handler - Callback function. - * @return {import ("./http").RemoveFn} Function to remove the callback. - */ - onSelectedPatternsChanged(handler) { - return this.client.onEvent("SoftwareProposalChanged", ({ patterns }) => { - handler(patterns); - }); - } } /** @@ -280,4 +228,4 @@ class ProductClient { } } -export { ProductClient, SelectedBy, SoftwareClient }; +export { ProductClient, SoftwareClient }; From 10d6157391c1d65fe8c8cf46df6e11086bad21eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 24 Jul 2024 17:38:02 +0200 Subject: [PATCH 293/430] feat(puppeteer): Cleanup node_modules (#1497) ## Problem - The Puppeteer `node_modules` directory is huge ## Solution - Delete the not needed content from `node_modules` - Inspired by [node-prune](https://github.com/tj/node-prune/) tool (written in Go, not much suitable to run during RPM build, I rewrote that to shell) ## Testing - Tested manually ## Numbers - The `node_modules` directory size went down from 39MB to 15MB (-62%) - The built RPM size went down from 3.5MB to 1.9MB (-46%) - I expect the Live ISO size reduction similar to the RPM reduction --- puppeteer/agama-integration-tests | 6 + puppeteer/node-prune.sh | 159 ++++++++++++++++++ puppeteer/node-puppeteer-prune.sh | 9 + .../package/agama-integration-tests.changes | 6 + .../package/agama-integration-tests.spec | 6 + 5 files changed, 186 insertions(+) create mode 100755 puppeteer/node-prune.sh create mode 100755 puppeteer/node-puppeteer-prune.sh diff --git a/puppeteer/agama-integration-tests b/puppeteer/agama-integration-tests index 304810c73e..8829b86005 100755 --- a/puppeteer/agama-integration-tests +++ b/puppeteer/agama-integration-tests @@ -19,6 +19,12 @@ MOCHA_OPTIONS=(--bail --slow 10000) if [ -e "$MYDIR/../.git/" ]; then npm install --omit=optional + + # do the same node_modules cleanup as in the RPM package to have the very same + # environment and have consistent results + "$MYDIR/node-prune.sh" + "$MYDIR/node-puppeteer-prune.sh" + npx mocha "${MOCHA_OPTIONS[@]}" "$@" else # set the default load path diff --git a/puppeteer/node-prune.sh b/puppeteer/node-prune.sh new file mode 100755 index 0000000000..7770c806c3 --- /dev/null +++ b/puppeteer/node-prune.sh @@ -0,0 +1,159 @@ +#! /bin/bash + +# This script clean up the node_modules directory which is usually huge and +# contains a lot of not needed files. It was inspired by the node-prune tool +# (https://github.com/tj/node-prune). +# +# Usage: +# +# node-prune.sh [path] +# +# The optional [path] argument is a path to the node_modules directory, if it is +# not specified it uses node_modules in the current directory. +# +# This is a generic tool, you might run it against any node_modules directory, +# not only in Agama Puppeteer tests. + +MODULES_PATH="${1:-./node_modules}" + +# The list of names/patterns comes from +# https://github.com/tj/node-prune/blob/master/internal/prune/prune.go + +# files to delete +FILES=( + Jenkinsfile + Makefile + Gulpfile.js + Gruntfile.js + gulpfile.js + .DS_Store + .tern-project + .gitattributes + .editorconfig + .eslintrc + eslint + .eslintrc.js + .eslintrc.json + .eslintrc.yml + .eslintignore + .stylelintrc + stylelint.config.js + .stylelintrc.json + .stylelintrc.yaml + .stylelintrc.yml + .stylelintrc.js + .htmllintrc + htmllint.js + .lint + .npmrc + .npmignore + .jshintrc + .flowconfig + .documentup.json + .yarn-metadata.json + .travis.yml + appveyor.yml + .gitlab-ci.yml + circle.yml + .coveralls.yml + CHANGES + changelog + # keep the package licenses, it's unclear if we can legally delete them... + # LICENSE.txt + # LICENSE + # LICENSE-MIT + # LICENSE.BSD + # license + # LICENCE.txt + # LICENCE + # LICENCE-MIT + # LICENCE.BSD + # licence + AUTHORS + CONTRIBUTORS + .yarn-integrity + .yarnclean + _config.yml + .babelrc + .yo-rc.json + jest.config.js + karma.conf.js + wallaby.js + wallaby.conf.js + .prettierrc + .prettierrc.yml + .prettierrc.toml + .prettierrc.js + .prettierrc.json + prettier.config.js + .appveyor.yml + tsconfig.json + tslint.json +) + +# directories to delete +DIRECTORIES=( + test + tests + powered-test + docs + doc + .idea + .vscode + website + images + assets + example + examples + coverage + .nyc_output + .circleci + .github +) + +# delete files with specific extensions +EXTENSIONS=( + markdown + md + mkd + ts + jst + coffee + tgz + swp +) + +# delete additional files with specific extensions (not deleted by the original +# node-prune tool) +EXTRA_EXTENSIONS=( + # The map files take almost half of the node_modules content! An they would be + # useful only for reporting bugs in Puppeteer itself or in some dependent + # library. + map +) + +echo -n "Before cleanup: " +du -h -s "$MODULES_PATH" | cut -f1 + +# delete files +for F in "${FILES[@]}"; do + find "$MODULES_PATH" -type f -name "$F" -delete +done + +# delete directories recursively +for D in "${DIRECTORIES[@]}"; do + find "$MODULES_PATH" -type d -name "$D" -prune -exec rm -rf \{\} \; +done + +# delete files with specific extenstions +for E in "${EXTENSIONS[@]}"; do + find "$MODULES_PATH" -type f -name "*.$E" -delete +done + +# delete additional files with extensions +for EE in "${EXTRA_EXTENSIONS[@]}"; do + find "$MODULES_PATH" -type f -name "*.$EE" -delete +done + +echo -n "After cleanup: " +du -h -s "$MODULES_PATH" | cut -f1 diff --git a/puppeteer/node-puppeteer-prune.sh b/puppeteer/node-puppeteer-prune.sh new file mode 100755 index 0000000000..1737f1c92e --- /dev/null +++ b/puppeteer/node-puppeteer-prune.sh @@ -0,0 +1,9 @@ +#! /bin/sh + +# This is a helper script which deletes some not needed files from NPM packages. +# This script is specific for Puppeteer installations. + +MODULES_PATH="${1:-./node_modules}" + +# delete Puppeteer CommonJS modules, we use the ES modules (in lib/esm) +rm -rf "$MODULES_PATH/puppeteer-core/lib/cjs" diff --git a/puppeteer/package/agama-integration-tests.changes b/puppeteer/package/agama-integration-tests.changes index 54d96b1f8e..4476861544 100644 --- a/puppeteer/package/agama-integration-tests.changes +++ b/puppeteer/package/agama-integration-tests.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jul 24 15:03:25 UTC 2024 - Ladislav Slezák + +- Reduce the node_modules size, delete not needed files + (gh#openSUSE/agama#1497) + ------------------------------------------------------------------- Fri Jul 19 10:18:37 UTC 2024 - Ladislav Slezák diff --git a/puppeteer/package/agama-integration-tests.spec b/puppeteer/package/agama-integration-tests.spec index fcd28918dd..87a7d3663a 100644 --- a/puppeteer/package/agama-integration-tests.spec +++ b/puppeteer/package/agama-integration-tests.spec @@ -47,6 +47,12 @@ outside. rm -f package-lock.json local-npm-registry %{_sourcedir} install --omit=optional --with=dev --legacy-peer-deps || ( find ~/.npm/_logs -name '*-debug.log' -print0 | xargs -0 cat; false) +# node_modules cleanup +%{_builddir}/agama/node-prune.sh + +# extra cleanup for the Puppeteer NPM packages +%{_builddir}/agama/node-puppeteer-prune.sh + %install install -D -d -m 0755 %{buildroot}%{_datadir}/agama/integration-tests cp -aR node_modules %{buildroot}%{_datadir}/agama/integration-tests From 96f1da77ab0c35d3e8598b5aab305aba6518c581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 24 Jul 2024 17:04:09 +0100 Subject: [PATCH 294/430] fix(web): drop dead code at product client Which was replaced by its equivalent using Tanstack query. See https://github.com/openSUSE/agama/pull/1454. --- web/src/client/software.js | 50 --------------------------------- web/src/client/software.test.js | 23 --------------- 2 files changed, 73 deletions(-) diff --git a/web/src/client/software.js b/web/src/client/software.js index c1ed9b51fd..5fae2a2002 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -95,56 +95,6 @@ class ProductClient { this.client = client; } - /** - * Returns the list of available products. - * - * @return {Promise>} - */ - async getAll() { - const response = await this.client.get("/software/products"); - if (!response.ok) { - console.log("Failed to get software products: ", response); - } - return response.json(); - } - - /** - * Returns the identifier of the selected product. - * - * @return {Promise} Selected identifier. - */ - async getSelected() { - const response = await this.client.get("/software/config"); - if (!response.ok) { - console.log("Failed to get software config: ", response); - } - const config = await response.json(); - return config.product; - } - - /** - * Selects a product for installation. - * - * @param {string} id - Product ID. - */ - async select(id) { - await this.client.put("/software/config", { product: id }); - } - - /** - * Registers a callback to run when the select product changes. - * - * @param {(id: string) => void} handler - Callback function. - * @return {import ("./http").RemoveFn} Function to remove the callback. - */ - onChange(handler) { - return this.client.onEvent("ProductChanged", ({ id }) => { - if (id) { - handler(id); - } - }); - } - /** * Returns the registration of the selected product. * diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js index 301f596b15..b3e68c2efb 100644 --- a/web/src/client/software.test.js +++ b/web/src/client/software.test.js @@ -74,29 +74,6 @@ const microos = { }; describe("ProductClient", () => { - describe("#getAll", () => { - it("returns the list of available products", async () => { - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - mockJsonFn.mockResolvedValue([tumbleweed, microos]); - const products = await client.getAll(); - expect(products).toEqual([ - { id: "Tumbleweed", name: "openSUSE Tumbleweed", description: "Tumbleweed is..." }, - { id: "MicroOS", name: "openSUSE MicroOS", description: "MicroOS is..." }, - ]); - }); - }); - - describe("#getSelected", () => { - it("returns the selected product", async () => { - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - mockJsonFn.mockResolvedValue({ product: "microos" }); - const selected = await client.getSelected(); - expect(selected).toEqual("microos"); - }); - }); - describe("#getRegistration", () => { describe("if the product is not registered yet", () => { it("returns the expected registration result", async () => { From 72bdc7c44dd9faabaa0f0e993b2873998c0d39d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 24 Jul 2024 17:13:41 +0100 Subject: [PATCH 295/430] fix(web): drop code related to product registration Because it is not in use at this time and it must be reimplemented for using a Tanstack Query approach anyway. --- web/src/client/software.js | 88 --------- web/src/client/software.test.js | 182 ------------------ .../product/ProductRegistrationForm.test.jsx | 95 --------- .../product/ProductRegistrationPage.jsx | 90 --------- web/src/components/product/index.js | 1 - 5 files changed, 456 deletions(-) delete mode 100644 web/src/client/software.test.js delete mode 100644 web/src/components/product/ProductRegistrationForm.test.jsx delete mode 100644 web/src/components/product/ProductRegistrationPage.jsx diff --git a/web/src/client/software.js b/web/src/client/software.js index 5fae2a2002..75da29cbc2 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -32,12 +32,6 @@ const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1"; * @property {string} description - Product description */ -/** - * @typedef {object} ActionResult - * @property {boolean} success - Whether the action was successfully done. - * @property {string} message - Result message. - */ - /** * @typedef {object} SoftwareProposal * @property {string} size - Used space in human-readable form. @@ -94,88 +88,6 @@ class ProductClient { constructor(client) { this.client = client; } - - /** - * Returns the registration of the selected product. - * - * @return {Promise} - */ - async getRegistration() { - const response = await this.client.get("/software/registration"); - if (!response.ok) { - console.log("Failed to get registration config:", response); - return { requirement: "unknown", code: null, email: null }; - } - const config = await response.json(); - - const { requirement, key: code, email } = config; - - const registration = { requirement, code, email }; - if (code.length === 0) registration.code = null; - if (email.length === 0) registration.email = null; - - return registration; - } - - /** - * Tries to register the selected product. - * - * @param {string} code - * @param {string} [email] - * @returns {Promise} - */ - async register(code, email = "") { - const response = await this.client.post("/software/registration", { key: code, email }); - if (response.status === 422) { - /** @type import('~/types/registration').RegistrationFailure */ - const body = await response.json(); - return { - success: false, - message: body.message, - }; - } - - return { - success: response.ok, // still we can fail 400 due to dbus issue or 500 if backend stop working. maybe some message for this case? - message: "", - }; - } - - /** - * Tries to deregister the selected product. - * - * @returns {Promise} - */ - async deregister() { - const response = await this.client.delete("/software/registration"); - - if (response.status === 422) { - /** @type import('~/types/registration').RegistrationFailure */ - const body = await response.json(); - return { - success: false, - message: body.message, - }; - } - - return { - success: response.ok, // still we can fail 400 due to dbus issue or 500 if backend stop working. maybe some message for this case? - message: "", - }; - } - - /** - * Registers a callback to run when the registration changes. - * - * @param {(registration: import('~/types/registration').Registration) => void} handler - Callback function. - */ - onRegistrationChange(handler) { - return this.client.ws().onEvent((event) => { - if (event.type === "RegistrationChanged" || event.type === "RegistrationRequirementChanged") { - this.getRegistration().then(handler); - } - }); - } } export { ProductClient, SoftwareClient }; diff --git a/web/src/client/software.test.js b/web/src/client/software.test.js deleted file mode 100644 index b3e68c2efb..0000000000 --- a/web/src/client/software.test.js +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) [2022-2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -import DBusClient from "./dbus"; -import { HTTPClient } from "./http"; -import { ProductClient, SoftwareClient } from "./software"; - -const mockJsonFn = jest.fn(); - -const mockGetFn = jest.fn().mockImplementation(() => { - return { - ok: true, - json: mockJsonFn, - }; -}); - -const mockPostFn = jest.fn().mockImplementation(() => { - return { - ok: true, - }; -}); - -const mockDeleteFn = jest.fn().mockImplementation(() => { - return { - ok: true, - }; -}); - -jest.mock("./http", () => { - return { - HTTPClient: jest.fn().mockImplementation(() => { - return { - get: mockGetFn, - post: mockPostFn, - delete: mockDeleteFn, - }; - }), - }; -}); - -const PRODUCT_IFACE = "org.opensuse.Agama.Software1.Product"; -const REGISTRATION_IFACE = "org.opensuse.Agama1.Registration"; - -const tumbleweed = { - id: "Tumbleweed", - name: "openSUSE Tumbleweed", - description: "Tumbleweed is...", -}; - -const microos = { - id: "MicroOS", - name: "openSUSE MicroOS", - description: "MicroOS is...", -}; - -describe("ProductClient", () => { - describe("#getRegistration", () => { - describe("if the product is not registered yet", () => { - it("returns the expected registration result", async () => { - mockJsonFn.mockResolvedValue({ - key: "", - email: "", - requirement: "Optional", - }); - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - const registration = await client.getRegistration(); - expect(registration).toStrictEqual({ - code: null, - email: null, - requirement: "Optional", - }); - }); - }); - - describe("if the product is registered", () => { - it("returns the expected registration", async () => { - mockJsonFn.mockResolvedValue({ - key: "111222", - email: "test@test.com", - requirement: "Mandatory", - }); - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - const registration = await client.getRegistration(); - expect(registration).toStrictEqual({ - code: "111222", - email: "test@test.com", - requirement: "Mandatory", - }); - }); - }); - }); - - describe("#register", () => { - it("performs the backend call", async () => { - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - await client.register("111222", "test@test.com"); - expect(mockPostFn).toHaveBeenCalledWith("/software/registration", { - key: "111222", - email: "test@test.com", - }); - }); - - describe("when the action is correctly done", () => { - it("returns a successful result", async () => { - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - const result = await client.register("111222", "test@test.com"); - expect(result).toStrictEqual({ - success: true, - message: "", - }); - }); - }); - - describe("when the action fails", () => { - it("returns an unsuccessful result", async () => { - mockPostFn.mockImplementationOnce(() => { - return { ok: false }; - }); - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - const result = await client.register("111222", "test@test.com"); - expect(result).toStrictEqual({ - success: false, - message: "", - }); - }); - }); - }); - - describe("#deregister", () => { - describe("when the action is correctly done", () => { - it("returns a successful result", async () => { - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - const result = await client.deregister(); - expect(result).toStrictEqual({ - success: true, - message: "", - }); - }); - }); - - describe("when the action fails", () => { - it("returns an unsuccessful result", async () => { - mockDeleteFn.mockImplementationOnce(() => { - return { ok: false }; - }); - const http = new HTTPClient(new URL("http://localhost")); - const client = new ProductClient(http); - const result = await client.deregister(); - expect(result).toStrictEqual({ - success: false, - message: "", - }); - }); - }); - }); -}); diff --git a/web/src/components/product/ProductRegistrationForm.test.jsx b/web/src/components/product/ProductRegistrationForm.test.jsx deleted file mode 100644 index 2c4a9ea44e..0000000000 --- a/web/src/components/product/ProductRegistrationForm.test.jsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React, { useState } from "react"; -import { Button } from "@patternfly/react-core"; -import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import { ProductRegistrationForm } from "~/components/product"; - -it.skip("renders a field for entering the registration code", async () => { - plainRender(); - await screen.findByLabelText(/Registration code/); -}); - -it.skip("renders a field for entering an email", async () => { - plainRender(); - await screen.findByLabelText("Email"); -}); - -const ProductRegistrationFormTest = () => { - const [isSubmitted, setIsSubmitted] = useState(false); - const [isValid, setIsValid] = useState(true); - - return ( - <> - - - {isSubmitted &&

      Form is submitted!

      } - {isValid === false &&

      Form is not valid!

      } - - ); -}; - -it.skip("triggers the onSubmit callback", async () => { - const { user } = plainRender(); - - expect(screen.queryByText("Form is submitted!")).toBeNull(); - - const button = screen.getByRole("button", { name: "Accept" }); - await user.click(button); - await screen.findByText("Form is submitted!"); -}); - -it.skip("sets the form as invalid if there is no code", async () => { - plainRender(); - await screen.findByText("Form is not valid!"); -}); - -it.skip("sets the form as invalid if there is a code and a wrong email", async () => { - const { user } = plainRender(); - const codeInput = await screen.findByLabelText(/Registration code/); - const emailInput = await screen.findByLabelText("Email"); - await user.type(codeInput, "111222"); - await user.type(emailInput, "foo"); - - await screen.findByText("Form is not valid!"); -}); - -it.skip("does not set the form as invalid if there is a code and no email", async () => { - const { user } = plainRender(); - const codeInput = await screen.findByLabelText(/Registration code/); - await user.type(codeInput, "111222"); - - expect(screen.queryByText("Form is not valid!")).toBeNull(); -}); - -it.skip("does not set the form as invalid if there is a code and a correct email", async () => { - const { user } = plainRender(); - const codeInput = await screen.findByLabelText(/Registration code/); - const emailInput = await screen.findByLabelText("Email"); - await user.type(codeInput, "111222"); - await user.type(emailInput, "test@test.com"); - - expect(screen.queryByText("Form is not valid!")).toBeNull(); -}); diff --git a/web/src/components/product/ProductRegistrationPage.jsx b/web/src/components/product/ProductRegistrationPage.jsx deleted file mode 100644 index df3672003b..0000000000 --- a/web/src/components/product/ProductRegistrationPage.jsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React, { useState } from "react"; -import { Alert, Form, FormGroup } from "@patternfly/react-core"; -import { useNavigate } from "react-router-dom"; -import { EmailInput, Page, PasswordInput } from "~/components/core"; -import { useProduct } from "~/queries/software"; -import { useInstallerClient } from "~/context/installer"; -import { _ } from "~/i18n"; -import { sprintf } from "sprintf-js"; - -/** - * Form for registering a product. - * @component - * - * @param {object} props - */ -export default function ProductRegistrationPage() { - const navigate = useNavigate(); - const { software } = useInstallerClient(); - const { selectedProduct } = useProduct(); - const [code, setCode] = useState(""); - const [email, setEmail] = useState(""); - const [error, setError] = useState(); - - // FIXME: re-introduce validations and "isLoading" status - // TODO: see if would be better to use https://reactrouter.com/en/main/components/form - - const onCancel = () => { - setError(null); - navigate(".."); - }; - - const onSubmit = async (e) => { - e.preventDefault(); - const result = await software.product.register(code, email); - if (result.success) { - software.probe(); - } else { - setError(result.message); - } - }; - - return ( - <> - -

      {sprintf(_("Register %s"), selectedProduct.name)}

      - {error && ( - -

      {error}

      -
      - )} - - - setCode(v)} /> - - - setEmail(v)} /> - - -
      - - - - - {_("Accept")} - - - - ); -} diff --git a/web/src/components/product/index.js b/web/src/components/product/index.js index c6153cccaa..c22d43e64a 100644 --- a/web/src/components/product/index.js +++ b/web/src/components/product/index.js @@ -19,6 +19,5 @@ * find current contact information at www.suse.com. */ -export { default as ProductRegistrationPage } from "./ProductRegistrationPage"; export { default as ProductSelectionPage } from "./ProductSelectionPage"; export { default as ProductSelectionProgress } from "./ProductSelectionProgress"; From 93de74817bc3749033c7e27c2f2a798e09624097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 24 Jul 2024 17:21:05 +0100 Subject: [PATCH 296/430] fix(web): drop dead typedef comments Defined types now live at types/software. Related to https://github.com/openSUSE/agama/pull/1483 and https://github.com/openSUSE/agama/pull/1496. --- web/src/client/software.js | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/web/src/client/software.js b/web/src/client/software.js index 75da29cbc2..e5a5fd0ad2 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -25,36 +25,6 @@ import { WithProgress, WithStatus } from "./mixins"; const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1"; -/** - * @typedef {object} Product - * @property {string} id - Product ID (e.g., "Leap") - * @property {string} name - Product name (e.g., "openSUSE Leap 15.4") - * @property {string} description - Product description - */ - -/** - * @typedef {object} SoftwareProposal - * @property {string} size - Used space in human-readable form. - * @property {Object.} patterns - Selected patterns and the reason. - */ - -/** - * @typedef {object} SoftwareConfig - * @propery {Object.} patterns - An object where the keys are the pattern names - * and the values whether to install them or not. - * @property {string|undefined} product - Product to install. - */ - -/** - * @typedef {Object} Pattern - * @property {string} name - Pattern name (internal ID). - * @property {string} category - Pattern category. - * @property {string} summary - User visible pattern name. - * @property {string} description - Long description of the pattern. - * @property {number} order - Display order (string!). - * @property {string} icon - Icon name (not path or file name!). - */ - /** * Software client * From 1555cc4c0144c2043cc4924cb5f34fb4c0a17933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 16 Jul 2024 16:39:23 +0100 Subject: [PATCH 297/430] refactor(web): improve route files By moving them from ~/components/{namespace}/routs.js to ~/routes/{namespace}.tsx It also adds a constant PATHS per namespace and uses them instead of directly typing the route path again when needed. --- web/src/App.jsx | 9 +-- web/src/components/l10n/L10nPage.jsx | 10 +-- web/src/components/overview/routes.js | 35 ---------- .../product/ProductSelectionProgress.jsx | 3 +- .../components/product/{index.js => index.ts} | 1 - web/src/components/software/SoftwarePage.tsx | 3 +- .../components/storage/BootConfigField.jsx | 3 +- .../storage/InstallationDeviceField.jsx | 3 +- .../storage/ProposalActionsSummary.jsx | 3 +- web/src/components/storage/routes.js | 65 ----------------- web/src/components/users/FirstUser.jsx | 5 +- web/src/router.js | 39 +++++++---- web/src/routes/{l10n.js => l10n.tsx} | 26 +++---- web/src/routes/{products.js => products.tsx} | 19 ++--- .../routes.js => routes/software.tsx} | 18 +++-- web/src/routes/storage.tsx | 69 +++++++++++++++++++ .../users/routes.js => routes/users.tsx} | 22 ++++-- 17 files changed, 165 insertions(+), 168 deletions(-) delete mode 100644 web/src/components/overview/routes.js rename web/src/components/product/{index.js => index.ts} (92%) delete mode 100644 web/src/components/storage/routes.js rename web/src/routes/{l10n.js => l10n.tsx} (70%) rename web/src/routes/{products.js => products.tsx} (77%) rename web/src/{components/software/routes.js => routes/software.tsx} (76%) create mode 100644 web/src/routes/storage.tsx rename web/src/{components/users/routes.js => routes/users.tsx} (75%) diff --git a/web/src/App.jsx b/web/src/App.jsx index 26773c8111..c0293f25c6 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -30,7 +30,8 @@ import { useProduct, useProductChanges } from "./queries/software"; import { CONFIG, INSTALL, STARTUP } from "~/client/phase"; import { BUSY } from "~/client/status"; import { useL10nConfigChanges } from "~/queries/l10n"; -import { useIssues, useIssuesChanges } from "./queries/issues"; +import { useIssuesChanges } from "./queries/issues"; +import { PATHS as PRODUCT_PATHS } from "./routes/products"; /** * Main application component. @@ -61,12 +62,12 @@ function App() { return ; } - if (selectedProduct === undefined && location.pathname !== "/products") { + if (selectedProduct === undefined && location.pathname !== PRODUCT_PATHS.root) { return ; } - if (phase === CONFIG && status === BUSY && location.pathname !== "/products/progress") { - return ; + if (phase === CONFIG && status === BUSY && location.pathname !== PRODUCT_PATHS.progress) { + return ; } return ; diff --git a/web/src/components/l10n/L10nPage.jsx b/web/src/components/l10n/L10nPage.jsx index 33e76b3fa5..9d74266212 100644 --- a/web/src/components/l10n/L10nPage.jsx +++ b/web/src/components/l10n/L10nPage.jsx @@ -23,11 +23,7 @@ import React from "react"; import { Gallery, GalleryItem } from "@patternfly/react-core"; import { useLoaderData } from "react-router-dom"; import { ButtonLink, CardField, Page } from "~/components/core"; -import { - LOCALE_SELECTION_PATH, - KEYMAP_SELECTION_PATH, - TIMEZONE_SELECTION_PATH, -} from "~/routes/l10n"; +import { PATHS } from "~/routes/l10n"; import { _ } from "~/i18n"; import { useL10n } from "~/queries/l10n"; @@ -60,7 +56,7 @@ export default function L10nPage() { label={_("Language")} value={locale ? `${locale.name} - ${locale.territory}` : _("Not selected yet")} > - + {locale ? _("Change") : _("Select")}
      @@ -68,7 +64,7 @@ export default function L10nPage() {
      - + {keymap ? _("Change") : _("Select")}
      diff --git a/web/src/components/overview/routes.js b/web/src/components/overview/routes.js deleted file mode 100644 index f9f98041a5..0000000000 --- a/web/src/components/overview/routes.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import OverviewPage from "./OverviewPage"; -import { N_ } from "~/i18n"; - -const routes = { - path: "/overview", - element: , - handle: { - name: N_("Overview"), - icon: "list_alt", - }, -}; - -export default routes; diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.jsx index 944ebe3243..59d64b1491 100644 --- a/web/src/components/product/ProductSelectionProgress.jsx +++ b/web/src/components/product/ProductSelectionProgress.jsx @@ -26,6 +26,7 @@ import { useProduct } from "~/queries/software"; import { ProgressReport } from "~/components/core"; import { IDLE } from "~/client/status"; import { useInstallerClient } from "~/context/installer"; +import { PATHS } from "~/router"; /** * @component @@ -42,7 +43,7 @@ function ProductSelectionProgress() { return manager.onStatusChange(setStatus); }, [manager, setStatus]); - if (status === IDLE) return ; + if (status === IDLE) return ; return ( ( + {_("Change selection")} } diff --git a/web/src/components/storage/BootConfigField.jsx b/web/src/components/storage/BootConfigField.jsx index eb509be202..3ea4558dec 100644 --- a/web/src/components/storage/BootConfigField.jsx +++ b/web/src/components/storage/BootConfigField.jsx @@ -28,6 +28,7 @@ import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { deviceLabel } from "~/components/storage/utils"; import { Icon } from "~/components/layout"; +import { PATHS } from "~/routes/storage"; /** * @typedef {import ("~/client/storage").StorageDevice} StorageDevice @@ -42,7 +43,7 @@ import { Icon } from "~/components/layout"; const Link = ({ isBold = false }) => { const text = _("Change boot options"); - return {isBold ? {text} : text}; + return {isBold ? {text} : text}; }; /** diff --git a/web/src/components/storage/InstallationDeviceField.jsx b/web/src/components/storage/InstallationDeviceField.jsx index 3a817fb9a5..959c0174ed 100644 --- a/web/src/components/storage/InstallationDeviceField.jsx +++ b/web/src/components/storage/InstallationDeviceField.jsx @@ -25,6 +25,7 @@ import React from "react"; import { Skeleton } from "@patternfly/react-core"; import { ButtonLink, CardField } from "~/components/core"; import { deviceLabel } from "~/components/storage/utils"; +import { PATHS } from "~/routes/storage"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; @@ -104,7 +105,7 @@ export default function InstallationDeviceField({ isLoading ? ( ) : ( - + {_("Change")} ) diff --git a/web/src/components/storage/ProposalActionsSummary.jsx b/web/src/components/storage/ProposalActionsSummary.jsx index ce673db4c4..ead8fa3dec 100644 --- a/web/src/components/storage/ProposalActionsSummary.jsx +++ b/web/src/components/storage/ProposalActionsSummary.jsx @@ -28,6 +28,7 @@ import DevicesManager from "~/components/storage/DevicesManager"; import { _, n_ } from "~/i18n"; import { sprintf } from "sprintf-js"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; +import { PATHS } from "~/routes/storage"; /** * @typedef {import ("~/client/storage").Action} Action @@ -235,7 +236,7 @@ export default function ProposalActionsSummary({ isLoading ? ( ) : ( - {_("Change")} + {_("Change")} ) } cardProps={{ isFullHeight: false }} diff --git a/web/src/components/storage/routes.js b/web/src/components/storage/routes.js deleted file mode 100644 index a8be087e3e..0000000000 --- a/web/src/components/storage/routes.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { Page } from "~/components/core"; -import BootSelection from "./BootSelection"; -import DeviceSelection from "./DeviceSelection"; -import SpacePolicySelection from "./SpacePolicySelection"; -import DASDPage from "./DASDPage"; -import ISCSIPage from "./ISCSIPage"; -import ProposalPage from "./ProposalPage"; -import ZFCPPage from "./ZFCPPage"; -import { N_ } from "~/i18n"; - -// FIXME: Choose a better name -const navigation = [ - // FIXME: use index: true - { path: "/storage", element: , handle: { name: N_("Proposal") } }, - { path: "iscsi", element: , handle: { name: N_("iSCSI") } }, -]; - -// if (something) { -// navigation.push({ path: "dasd", element: , handle: { ... } }) -// } -// -// if (somethingElse) { -// navigation.push({ path: "zfcp", element: , handle: { ... } }) -// } - -const selectors = [ - { path: "target-device", element: }, - { path: "booting-partition", element: }, - { path: "space-policy", element: }, -]; - -const routes = { - path: "/storage", - element: , - handle: { - name: N_("Storage"), - icon: "hard_drive", - }, - children: [...navigation, ...selectors], -}; - -export default routes; -export { navigation, selectors }; diff --git a/web/src/components/users/FirstUser.jsx b/web/src/components/users/FirstUser.jsx index 19c5c17f03..f3d41fc5e0 100644 --- a/web/src/components/users/FirstUser.jsx +++ b/web/src/components/users/FirstUser.jsx @@ -26,6 +26,7 @@ import { useNavigate } from "react-router-dom"; import { RowActions, ButtonLink } from "~/components/core"; import { _ } from "~/i18n"; import { useFirstUser, useFirstUserChanges, useRemoveFirstUserMutation } from "~/queries/users"; +import { PATHS } from "~/routes/users"; const UserNotDefined = ({ actionCb }) => { return ( @@ -40,7 +41,7 @@ const UserNotDefined = ({ actionCb }) => {
      - + {_("Define a user now")} @@ -83,7 +84,7 @@ export default function FirstUser() { const actions = [ { title: _("Edit"), - onClick: () => navigate("/users/first/edit"), + onClick: () => navigate(PATHS.firstUser.edit), }, { title: _("Discard"), diff --git a/web/src/router.js b/web/src/router.js index 333f28e83f..d72d0748cc 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -27,27 +27,36 @@ import MainLayout from "~/MainLayout"; import SimpleLayout from "./SimpleLayout"; import { LoginPage } from "~/components/core"; import { OverviewPage } from "~/components/overview"; -import { _ } from "~/i18n"; -import overviewRoutes from "~/components/overview/routes"; +import { _, N_ } from "~/i18n"; import l10nRoutes from "~/routes/l10n"; import networkRoutes from "~/components/network/routes"; import productsRoutes from "~/routes/products"; -import storageRoutes from "~/components/storage/routes"; -import softwareRoutes from "~/components/software/routes"; -import usersRoutes from "~/components/users/routes"; +import storageRoutes from "~/routes/storage"; +import softwareRoutes from "~/routes/software"; +import usersRoutes from "~/routes/users"; + +const PATHS = { + root: "/", + login: "/login", + overview: "/overview", +}; const rootRoutes = [ - overviewRoutes, - l10nRoutes, + { + path: "/overview", + element: , + handle: { name: N_("Overview"), icon: "list_alt" }, + }, + l10nRoutes(), networkRoutes, - storageRoutes, - softwareRoutes, - usersRoutes, + storageRoutes(), + softwareRoutes(), + usersRoutes(), ]; const protectedRoutes = [ { - path: "/", + path: PATHS.root, element: , children: [ { @@ -62,7 +71,7 @@ const protectedRoutes = [ }, { element: , - children: [productsRoutes], + children: [productsRoutes()], }, ], }, @@ -70,7 +79,7 @@ const protectedRoutes = [ const routes = [ { - path: "/login", + path: PATHS.login, exact: true, element: , children: [ @@ -81,7 +90,7 @@ const routes = [ ], }, { - path: "/", + path: PATHS.root, element: , children: [...protectedRoutes], }, @@ -89,4 +98,4 @@ const routes = [ const router = createHashRouter(routes); -export { router, rootRoutes }; +export { router, rootRoutes, PATHS }; diff --git a/web/src/routes/l10n.js b/web/src/routes/l10n.tsx similarity index 70% rename from web/src/routes/l10n.js rename to web/src/routes/l10n.tsx index 17f9a2a919..5d863b94df 100644 --- a/web/src/routes/l10n.js +++ b/web/src/routes/l10n.tsx @@ -22,17 +22,17 @@ import React from "react"; import { Page } from "~/components/core"; import { L10nPage, LocaleSelection, KeymapSelection, TimezoneSelection } from "~/components/l10n"; -import { queryClient } from "~/context/app"; -import { configQuery, localesQuery, keymapsQuery, timezonesQuery } from "~/queries/l10n"; import { N_ } from "~/i18n"; -const L10N_PATH = "/l10n"; -const LOCALE_SELECTION_PATH = "locale/select"; -const KEYMAP_SELECTION_PATH = "keymap/select"; -const TIMEZONE_SELECTION_PATH = "timezone/select"; +const PATHS = { + root: "/l10n", + localeSelection: "/l10n/locale/select", + keymapSelection: "/l10n/keymap/select", + timezoneSelection: "/l10n/timezone/select", +}; -const routes = { - path: L10N_PATH, +const routes = () => ({ + path: PATHS.root, element: , handle: { name: N_("Localization"), @@ -44,19 +44,19 @@ const routes = { element: , }, { - path: LOCALE_SELECTION_PATH, + path: PATHS.localeSelection, element: , }, { - path: KEYMAP_SELECTION_PATH, + path: PATHS.keymapSelection, element: , }, { - path: TIMEZONE_SELECTION_PATH, + path: PATHS.timezoneSelection, element: , }, ], -}; +}); export default routes; -export { L10N_PATH, LOCALE_SELECTION_PATH, KEYMAP_SELECTION_PATH, TIMEZONE_SELECTION_PATH }; +export { PATHS }; diff --git a/web/src/routes/products.js b/web/src/routes/products.tsx similarity index 77% rename from web/src/routes/products.js rename to web/src/routes/products.tsx index 0b30ce962c..f906c28196 100644 --- a/web/src/routes/products.js +++ b/web/src/routes/products.tsx @@ -21,13 +21,15 @@ import React from "react"; import { Page } from "~/components/core"; -import ProductSelectionPage from "~/components/product/ProductSelectionPage"; -import ProductSelectionProgress from "~/components/product/ProductSelectionProgress"; +import { ProductSelectionPage, ProductSelectionProgress } from "~/components/product"; -const PRODUCTS_PATH = "/products"; +const PATHS = { + root: "/products", + progress: "/products/progress", +}; -const productsRoutes = { - path: PRODUCTS_PATH, +const routes = () => ({ + path: PATHS.root, element: , children: [ { @@ -35,10 +37,11 @@ const productsRoutes = { element: , }, { - path: "progress", + path: PATHS.progress, element: , }, ], -}; +}); -export default productsRoutes; +export default routes; +export { PATHS }; diff --git a/web/src/components/software/routes.js b/web/src/routes/software.tsx similarity index 76% rename from web/src/components/software/routes.js rename to web/src/routes/software.tsx index e6214c434f..9c5c56c4ae 100644 --- a/web/src/components/software/routes.js +++ b/web/src/routes/software.tsx @@ -21,12 +21,17 @@ import React from "react"; import { Page } from "~/components/core"; -import SoftwarePage from "./SoftwarePage"; -import SoftwarePatternsSelection from "./SoftwarePatternsSelection"; +import SoftwarePage from "~/components/software/SoftwarePage"; +import SoftwarePatternsSelection from "~/components/software/SoftwarePatternsSelection"; import { N_ } from "~/i18n"; -const routes = { - path: "/software", +const PATHS = { + root: "/software", + patternsSelection: "/software/patterns/select", +}; + +const routes = () => ({ + path: PATHS.root, element: , handle: { name: N_("Software"), @@ -38,10 +43,11 @@ const routes = { element: , }, { - path: "patterns/select", + path: PATHS.patternsSelection, element: , }, ], -}; +}); export default routes; +export { PATHS }; diff --git a/web/src/routes/storage.tsx b/web/src/routes/storage.tsx new file mode 100644 index 0000000000..90b9d55731 --- /dev/null +++ b/web/src/routes/storage.tsx @@ -0,0 +1,69 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { Page } from "~/components/core"; +import BootSelection from "~/components/storage/BootSelection"; +import DeviceSelection from "~/components/storage/DeviceSelection"; +import SpacePolicySelection from "~/components/storage/SpacePolicySelection"; +import ISCSIPage from "~/components/storage/ISCSIPage"; +import ProposalPage from "~/components/storage/ProposalPage"; +import { N_ } from "~/i18n"; + +const PATHS = { + root: "/storage", + targetDevice: "/storage/target-device", + bootingPartition: "/storage/booting-partition", + spacePolicy: "/storage/space-policy", + iscsi: "/storage/iscsi", +}; + +const routes = () => ({ + path: PATHS.root, + element: , + handle: { name: N_("Storage"), icon: "hard_drive" }, + children: [ + { + index: true, + element: , + }, + { + path: PATHS.targetDevice, + element: , + }, + { + path: PATHS.bootingPartition, + element: , + }, + { + path: PATHS.spacePolicy, + element: , + }, + { + path: PATHS.iscsi, + element: , + handle: { name: N_("iSCSI") }, + }, + ], +}); + +export default routes; +export { PATHS }; diff --git a/web/src/components/users/routes.js b/web/src/routes/users.tsx similarity index 75% rename from web/src/components/users/routes.js rename to web/src/routes/users.tsx index 4c108d1f4c..c7c6909325 100644 --- a/web/src/components/users/routes.js +++ b/web/src/routes/users.tsx @@ -21,12 +21,19 @@ import React from "react"; import { Page } from "~/components/core"; -import UsersPage from "./UsersPage"; -import FirstUserForm from "./FirstUserForm"; +import UsersPage from "~/components/users/UsersPage"; +import FirstUserForm from "~/components/users/FirstUserForm"; import { N_ } from "~/i18n"; -const routes = { - path: "/users", +const PATHS = { + root: "/users", + firstUser: { + create: "/users/first", + edit: "/users/first/edit", + }, +}; +const routes = () => ({ + path: PATHS.root, element: , handle: { name: N_("Users"), @@ -35,14 +42,15 @@ const routes = { children: [ { index: true, element: }, { - path: "first", + path: PATHS.firstUser.create, element: , }, { - path: "first/edit", + path: PATHS.firstUser.edit, element: , }, ], -}; +}); export default routes; +export { PATHS }; From ea0cda2bd2a9230407f6d05443e41e0de1b76407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 16 Jul 2024 17:07:34 +0100 Subject: [PATCH 298/430] refactor(web): stop using as route element Using it as a wrapper for PF/PageGroup accepting children instead of a wrapper for a router . The latest is already rendered by the layout. Please, not that this change is just the starting point of a bigger Page component refactor that should help to improve both, the DX experience and the resulting DOM output. --- .../components/core/{Page.jsx => Page.tsx} | 25 +----- web/src/components/l10n/KeyboardSelection.jsx | 4 +- web/src/components/l10n/L10nPage.jsx | 7 +- web/src/components/l10n/LocaleSelection.jsx | 4 +- web/src/components/l10n/TimezoneSelection.jsx | 4 +- web/src/components/network/NetworkPage.jsx | 4 +- web/src/components/network/routes.js | 2 - web/src/components/overview/OverviewPage.jsx | 4 +- .../product/ProductRegistrationPage.jsx | 4 +- .../product/ProductSelectionPage.jsx | 77 ++++++++++--------- .../product/ProductSelectionProgress.jsx | 12 +-- web/src/components/software/SoftwarePage.tsx | 4 +- .../software/SoftwarePatternsSelection.tsx | 4 +- web/src/components/storage/BootSelection.jsx | 4 +- .../components/storage/DeviceSelection.jsx | 4 +- web/src/components/storage/ISCSIPage.jsx | 5 +- web/src/components/storage/ProposalPage.jsx | 4 +- .../storage/SpacePolicySelection.jsx | 4 +- web/src/components/users/FirstUserForm.jsx | 4 +- web/src/components/users/UsersPage.jsx | 7 +- web/src/routes/l10n.tsx | 2 - web/src/routes/products.tsx | 2 - web/src/routes/software.tsx | 2 - web/src/routes/storage.tsx | 2 - web/src/routes/users.tsx | 2 - 25 files changed, 85 insertions(+), 112 deletions(-) rename web/src/components/core/{Page.jsx => Page.tsx} (87%) diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.tsx similarity index 87% rename from web/src/components/core/Page.jsx rename to web/src/components/core/Page.tsx index b71bcdbbcd..4dc62131b2 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.tsx @@ -19,8 +19,6 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { @@ -168,30 +166,15 @@ const CardSection = ({ title, children, ...props }) => { }; /** - * Displays an installation page - * @component - * - * @note Sidebar is mounted as sibling of the page content to make it work - * as expected (e.g., changing the inert attribute of its siblings according to its visibility). + * Wraps children in a PF/PageGroup * * @example Simple usage - * + * * * - * - * @param {object} props - * @param {string} [props.icon] - The icon for the page. - * @param {string} [props.title="Agama"] - The title for the page. By default it - * uses the name of the tool, do not mark it for translation. - * @param {boolean} [props.mountSidebar=true] - Whether include the core/Sidebar component. - * @param {React.ReactNode} [props.children] - The page content. */ -const Page = () => { - return ( - - - - ); +const Page = ({ children }) => { + return {children}; }; Page.CardSection = CardSection; diff --git a/web/src/components/l10n/KeyboardSelection.jsx b/web/src/components/l10n/KeyboardSelection.jsx index f9a45de367..9fd97137af 100644 --- a/web/src/components/l10n/KeyboardSelection.jsx +++ b/web/src/components/l10n/KeyboardSelection.jsx @@ -72,7 +72,7 @@ export default function KeyboardSelection() { } return ( - <> +

      {_("Keyboard selection")}

      @@ -90,6 +90,6 @@ export default function KeyboardSelection() { {_("Select")} - +
      ); } diff --git a/web/src/components/l10n/L10nPage.jsx b/web/src/components/l10n/L10nPage.jsx index 9d74266212..790628a901 100644 --- a/web/src/components/l10n/L10nPage.jsx +++ b/web/src/components/l10n/L10nPage.jsx @@ -21,7 +21,6 @@ import React from "react"; import { Gallery, GalleryItem } from "@patternfly/react-core"; -import { useLoaderData } from "react-router-dom"; import { ButtonLink, CardField, Page } from "~/components/core"; import { PATHS } from "~/routes/l10n"; import { _ } from "~/i18n"; @@ -44,7 +43,7 @@ export default function L10nPage() { const { selectedLocale: locale, selectedTimezone: timezone, selectedKeymap: keymap } = useL10n(); return ( - <> +

      {_("Localization")}

      @@ -75,13 +74,13 @@ export default function L10nPage() { label={_("Time zone")} value={timezone ? (timezone.parts || []).join(" - ") : _("Not selected yet")} > - + {timezone ? _("Change") : _("Select")}
      - + ); } diff --git a/web/src/components/l10n/LocaleSelection.jsx b/web/src/components/l10n/LocaleSelection.jsx index cf6f39f00c..75ed98f6d2 100644 --- a/web/src/components/l10n/LocaleSelection.jsx +++ b/web/src/components/l10n/LocaleSelection.jsx @@ -73,7 +73,7 @@ export default function LocaleSelection() { } return ( - <> +

      {_("Locale selection")}

      @@ -92,6 +92,6 @@ export default function LocaleSelection() { {_("Select")} - +
      ); } diff --git a/web/src/components/l10n/TimezoneSelection.jsx b/web/src/components/l10n/TimezoneSelection.jsx index 385d689a70..56bc6a525d 100644 --- a/web/src/components/l10n/TimezoneSelection.jsx +++ b/web/src/components/l10n/TimezoneSelection.jsx @@ -102,7 +102,7 @@ export default function TimezoneSelection() { } return ( - <> +

      {_(" Timezone selection")}

      - +
      ); } diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index 5f926da4d6..3de10cd297 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -109,7 +109,7 @@ export default function NetworkPage() { }; return ( - <> +

      {_("Network")}

      @@ -124,6 +124,6 @@ export default function NetworkPage() {
      - + ); } diff --git a/web/src/components/network/routes.js b/web/src/components/network/routes.js index 48a8947469..96bc5851d0 100644 --- a/web/src/components/network/routes.js +++ b/web/src/components/network/routes.js @@ -20,7 +20,6 @@ */ import React from "react"; -import { Page } from "~/components/core"; import NetworkPage from "./NetworkPage"; import IpSettingsForm from "./IpSettingsForm"; import WifiSelectorPage from "./WifiSelectorPage"; @@ -28,7 +27,6 @@ import { N_ } from "~/i18n"; const routes = { path: "/network", - element: , handle: { name: N_("Network"), icon: "settings_ethernet", diff --git a/web/src/components/overview/OverviewPage.jsx b/web/src/components/overview/OverviewPage.jsx index 97dcd14c18..7363eb58fd 100644 --- a/web/src/components/overview/OverviewPage.jsx +++ b/web/src/components/overview/OverviewPage.jsx @@ -104,7 +104,7 @@ export default function OverviewPage() { }; return ( - <> + @@ -141,6 +141,6 @@ export default function OverviewPage() { - + ); } diff --git a/web/src/components/product/ProductRegistrationPage.jsx b/web/src/components/product/ProductRegistrationPage.jsx index df3672003b..6e56904764 100644 --- a/web/src/components/product/ProductRegistrationPage.jsx +++ b/web/src/components/product/ProductRegistrationPage.jsx @@ -61,7 +61,7 @@ export default function ProductRegistrationPage() { }; return ( - <> +

      {sprintf(_("Register %s"), selectedProduct.name)}

      {error && ( @@ -85,6 +85,6 @@ export default function ProductRegistrationPage() { {_("Accept")} - +
      ); } diff --git a/web/src/components/product/ProductSelectionPage.jsx b/web/src/components/product/ProductSelectionPage.jsx index bd5f2a8403..d3e08cf97a 100644 --- a/web/src/components/product/ProductSelectionPage.jsx +++ b/web/src/components/product/ProductSelectionPage.jsx @@ -21,12 +21,11 @@ import React, { useState } from "react"; import { Card, CardBody, Flex, Form, Grid, GridItem, Radio } from "@patternfly/react-core"; -import styles from "@patternfly/react-styles/css/utilities/Text/text"; - -import { _ } from "~/i18n"; import { Page } from "~/components/core"; import { Loading, Center } from "~/components/layout"; import { useConfigMutation, useProduct } from "~/queries/software"; +import { _ } from "~/i18n"; +import styles from "@patternfly/react-styles/css/utilities/Text/text"; const Label = ({ children }) => ( {children} @@ -58,42 +57,44 @@ function ProductSelectionPage() { const isSelectionDisabled = !nextProduct || nextProduct === selectedProduct; return ( -
      -
      - - {products.map((product, index) => ( - - - - {product.name}} - body={product.description} - isChecked={nextProduct === product} - onChange={() => setNextProduct(product)} - /> - - + +
      + + + {products.map((product, index) => ( + + + + {product.name}} + body={product.description} + isChecked={nextProduct === product} + onChange={() => setNextProduct(product)} + /> + + + + ))} + + + {selectedProduct && !isLoading && } + + {_("Select")} + + - ))} - - - {selectedProduct && !isLoading && } - - {_("Select")} - - - - - -
      +
      + +
      +
      ); } diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.jsx index 59d64b1491..6036ef1e7f 100644 --- a/web/src/components/product/ProductSelectionProgress.jsx +++ b/web/src/components/product/ProductSelectionProgress.jsx @@ -23,7 +23,7 @@ import React, { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; import { _ } from "~/i18n"; import { useProduct } from "~/queries/software"; -import { ProgressReport } from "~/components/core"; +import { Page, ProgressReport } from "~/components/core"; import { IDLE } from "~/client/status"; import { useInstallerClient } from "~/context/installer"; import { PATHS } from "~/router"; @@ -46,10 +46,12 @@ function ProductSelectionProgress() { if (status === IDLE) return ; return ( - + + + ); } diff --git a/web/src/components/software/SoftwarePage.tsx b/web/src/components/software/SoftwarePage.tsx index ff2397cb15..754286687f 100644 --- a/web/src/components/software/SoftwarePage.tsx +++ b/web/src/components/software/SoftwarePage.tsx @@ -101,7 +101,7 @@ function SoftwarePage(): React.ReactNode { useProposalChanges(); return ( - <> +

      {_("Software")}

      @@ -123,7 +123,7 @@ function SoftwarePage(): React.ReactNode { - +
      ); } diff --git a/web/src/components/software/SoftwarePatternsSelection.tsx b/web/src/components/software/SoftwarePatternsSelection.tsx index 4b7648d748..2f5326d407 100644 --- a/web/src/components/software/SoftwarePatternsSelection.tsx +++ b/web/src/components/software/SoftwarePatternsSelection.tsx @@ -183,7 +183,7 @@ function SoftwarePatternsSelection(): React.ReactNode { }); return ( - <> +

      {_("Software selection")}

      @@ -208,7 +208,7 @@ function SoftwarePatternsSelection(): React.ReactNode { {_("Close")} - +
      ); } diff --git a/web/src/components/storage/BootSelection.jsx b/web/src/components/storage/BootSelection.jsx index 61e68440e6..17bdeac125 100644 --- a/web/src/components/storage/BootSelection.jsx +++ b/web/src/components/storage/BootSelection.jsx @@ -154,7 +154,7 @@ partitions in the appropriate disk.", }; return ( - <> +

      {_("Select booting partition")}

      {description}

      @@ -250,6 +250,6 @@ partitions in the appropriate disk.", {_("Accept")} - +
      ); } diff --git a/web/src/components/storage/DeviceSelection.jsx b/web/src/components/storage/DeviceSelection.jsx index d417bb6c18..92b7fa7688 100644 --- a/web/src/components/storage/DeviceSelection.jsx +++ b/web/src/components/storage/DeviceSelection.jsx @@ -155,7 +155,7 @@ devices.", ).split(/[[\]]/); return ( - <> +

      {_("Select installation device")}

      @@ -254,6 +254,6 @@ devices.", {_("Accept")} - +
      ); } diff --git a/web/src/components/storage/ISCSIPage.jsx b/web/src/components/storage/ISCSIPage.jsx index 4e0a5428c2..f2eba0dc1a 100644 --- a/web/src/components/storage/ISCSIPage.jsx +++ b/web/src/components/storage/ISCSIPage.jsx @@ -20,13 +20,14 @@ */ import React from "react"; +import { Page } from "~/components/core"; import { InitiatorSection, TargetsSection } from "~/components/storage/iscsi"; export default function ISCSIPage() { return ( - <> + - + ); } diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index 4e89fef074..16fd0efbea 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -290,7 +290,7 @@ export default function ProposalPage() { */ return ( - <> +

      {_("Storage")}

      @@ -341,6 +341,6 @@ export default function ProposalPage() { - +
      ); } diff --git a/web/src/components/storage/SpacePolicySelection.jsx b/web/src/components/storage/SpacePolicySelection.jsx index 602ad6d510..9cf0e64e65 100644 --- a/web/src/components/storage/SpacePolicySelection.jsx +++ b/web/src/components/storage/SpacePolicySelection.jsx @@ -167,7 +167,7 @@ export default function SpacePolicySelection() { const xl2Columns = policy.id === "custom" ? 6 : 12; return ( - <> +

      {_("Space policy")}

      @@ -200,6 +200,6 @@ export default function SpacePolicySelection() { {_("Accept")} - +
      ); } diff --git a/web/src/components/users/FirstUserForm.jsx b/web/src/components/users/FirstUserForm.jsx index ad7322baa1..53ce49625f 100644 --- a/web/src/components/users/FirstUserForm.jsx +++ b/web/src/components/users/FirstUserForm.jsx @@ -196,7 +196,7 @@ export default function FirstUserForm() { }; return ( - <> +

      {state.isEditing ? _("Edit user") : _("Create user")}

      @@ -293,6 +293,6 @@ export default function FirstUserForm() { {_("Accept")} - +
      ); } diff --git a/web/src/components/users/UsersPage.jsx b/web/src/components/users/UsersPage.jsx index 5c3a12105a..1913cab2e2 100644 --- a/web/src/components/users/UsersPage.jsx +++ b/web/src/components/users/UsersPage.jsx @@ -20,18 +20,17 @@ */ import React from "react"; - -import { _ } from "~/i18n"; import { CardField, IssuesHint, Page } from "~/components/core"; import { FirstUser, RootAuthMethods } from "~/components/users"; import { CardBody, Grid, GridItem } from "@patternfly/react-core"; import { useIssues } from "~/queries/issues"; +import { _ } from "~/i18n"; export default function UsersPage() { const issues = useIssues("users"); return ( - <> +

      {_("Users")}

      @@ -57,6 +56,6 @@ export default function UsersPage() { - +
      ); } diff --git a/web/src/routes/l10n.tsx b/web/src/routes/l10n.tsx index 5d863b94df..4cd6abb3bd 100644 --- a/web/src/routes/l10n.tsx +++ b/web/src/routes/l10n.tsx @@ -20,7 +20,6 @@ */ import React from "react"; -import { Page } from "~/components/core"; import { L10nPage, LocaleSelection, KeymapSelection, TimezoneSelection } from "~/components/l10n"; import { N_ } from "~/i18n"; @@ -33,7 +32,6 @@ const PATHS = { const routes = () => ({ path: PATHS.root, - element: , handle: { name: N_("Localization"), icon: "globe", diff --git a/web/src/routes/products.tsx b/web/src/routes/products.tsx index f906c28196..3a8bdd51db 100644 --- a/web/src/routes/products.tsx +++ b/web/src/routes/products.tsx @@ -20,7 +20,6 @@ */ import React from "react"; -import { Page } from "~/components/core"; import { ProductSelectionPage, ProductSelectionProgress } from "~/components/product"; const PATHS = { @@ -30,7 +29,6 @@ const PATHS = { const routes = () => ({ path: PATHS.root, - element: , children: [ { index: true, diff --git a/web/src/routes/software.tsx b/web/src/routes/software.tsx index 9c5c56c4ae..ce0b5d1315 100644 --- a/web/src/routes/software.tsx +++ b/web/src/routes/software.tsx @@ -20,7 +20,6 @@ */ import React from "react"; -import { Page } from "~/components/core"; import SoftwarePage from "~/components/software/SoftwarePage"; import SoftwarePatternsSelection from "~/components/software/SoftwarePatternsSelection"; import { N_ } from "~/i18n"; @@ -32,7 +31,6 @@ const PATHS = { const routes = () => ({ path: PATHS.root, - element: , handle: { name: N_("Software"), icon: "apps", diff --git a/web/src/routes/storage.tsx b/web/src/routes/storage.tsx index 90b9d55731..8b77399cd2 100644 --- a/web/src/routes/storage.tsx +++ b/web/src/routes/storage.tsx @@ -20,7 +20,6 @@ */ import React from "react"; -import { Page } from "~/components/core"; import BootSelection from "~/components/storage/BootSelection"; import DeviceSelection from "~/components/storage/DeviceSelection"; import SpacePolicySelection from "~/components/storage/SpacePolicySelection"; @@ -38,7 +37,6 @@ const PATHS = { const routes = () => ({ path: PATHS.root, - element: , handle: { name: N_("Storage"), icon: "hard_drive" }, children: [ { diff --git a/web/src/routes/users.tsx b/web/src/routes/users.tsx index c7c6909325..38d0439311 100644 --- a/web/src/routes/users.tsx +++ b/web/src/routes/users.tsx @@ -20,7 +20,6 @@ */ import React from "react"; -import { Page } from "~/components/core"; import UsersPage from "~/components/users/UsersPage"; import FirstUserForm from "~/components/users/FirstUserForm"; import { N_ } from "~/i18n"; @@ -34,7 +33,6 @@ const PATHS = { }; const routes = () => ({ path: PATHS.root, - element: , handle: { name: N_("Users"), icon: "manage_accounts", From 624e6114d2ad6b2b3fd2b01afb84445f238d0651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 17 Jul 2024 08:51:31 +0100 Subject: [PATCH 299/430] refactor(web): still improving core/Page component --- web/src/components/core/Page.tsx | 73 ++++++++++++-------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/web/src/components/core/Page.tsx b/web/src/components/core/Page.tsx index 4dc62131b2..9ee3bfa528 100644 --- a/web/src/components/core/Page.tsx +++ b/web/src/components/core/Page.tsx @@ -23,6 +23,7 @@ import React from "react"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { Button, + ButtonProps, Card, CardBody, CardHeader, @@ -35,38 +36,15 @@ import { _ } from "~/i18n"; import tabsStyles from "@patternfly/react-styles/css/components/Tabs/tabs"; import flexStyles from "@patternfly/react-styles/css/utilities/Flex/flex"; -/** - * @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps - */ - -/** - * Wrapper component for holding Page actions - * - * Useful and required for placing the components to be used as Page actions, usually a - * Page.Action or PF/Button - * - * @see Page examples. - * - * @param {object} props - Component props. - * @param {React.ReactNode} props.children - Components to be rendered as actions. - */ -const Actions = ({ children }) => <>{children}; +type PageActionProps = { navigateTo?: string } & ButtonProps; +type PageCancelActionProps = { text?: string } & PageActionProps; /** - * A convenient component representing a Page action - * - * Built on top of {@link https://www.patternfly.org/components/button PF/Button} - * - * @see Page examples. + * A convenient component for rendering a page action * - * @typedef {object} ActionProps - * @property {string} [navigateTo] - * - * @typedef {ActionProps & ButtonProps} PageActionProps - * - * @param {PageActionProps} props + * Built on top of {@link https://www.patternfly.org/components/button | PF/Button} */ -const Action = ({ navigateTo, children, ...props }) => { +const Action = ({ navigateTo, children, ...props }: PageActionProps) => { const navigate = useNavigate(); const onClickFn = props.onClick; @@ -76,36 +54,40 @@ const Action = ({ navigateTo, children, ...props }) => { if (navigateTo) navigate(navigateTo); }; - if (!props.size) props.size = "lg"; - - return ; + const buttonProps = { size: "lg" as const, ...props }; + return ; }; /** - * Simple action for navigating back - * @param {ActionProps & { text?: string }} props + * Convenient component for a Cancel / Back action */ -const CancelAction = ({ text = _("Cancel"), navigateTo }) => { - const navigate = useNavigate(); - +const CancelAction = ({ + text = _("Cancel"), + navigateTo = "..", + ...props +}: PageCancelActionProps) => { return ( - navigate(navigateTo || "..")}> + {text} ); }; -// FIXME: would replace Actions -const NextActions = ({ children }) => ( - ( + - - {children} - - + {children} + ); const MainContent = ({ children, ...props }) => ( @@ -178,8 +160,7 @@ const Page = ({ children }) => { }; Page.CardSection = CardSection; -Page.Actions = Actions; -Page.NextActions = NextActions; +Page.NextActions = Actions; Page.Action = Action; Page.MainContent = MainContent; Page.CancelAction = CancelAction; From 6c21d01df0906a77e64c9ca2df144c43fefae23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 23 Jul 2024 16:50:19 +0100 Subject: [PATCH 300/430] refactor(web) move and improve network routes file Related to 1555cc4c0144c2043cc4924cb5f34fb4c0a17933 --- .../components/network/ConnectionsTable.jsx | 8 +++--- web/src/components/network/NetworkPage.jsx | 11 ++++---- .../network/WifiNetworksListPage.jsx | 24 +++++++++-------- web/src/router.js | 4 +-- .../network/routes.js => routes/network.tsx} | 27 +++++++++++-------- 5 files changed, 41 insertions(+), 33 deletions(-) rename web/src/{components/network/routes.js => routes/network.tsx} (73%) diff --git a/web/src/components/network/ConnectionsTable.jsx b/web/src/components/network/ConnectionsTable.jsx index 22616074d7..4dd892758e 100644 --- a/web/src/components/network/ConnectionsTable.jsx +++ b/web/src/components/network/ConnectionsTable.jsx @@ -20,13 +20,13 @@ */ import React from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, generatePath } from "react-router-dom"; import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; -import { sprintf } from "sprintf-js"; - import { RowActions } from "~/components/core"; import { Icon } from "~/components/layout"; import { formatIp } from "~/client/network/utils"; +import { PATHS } from "~/routes/network"; +import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; /** @@ -76,7 +76,7 @@ export default function ConnectionsTable({ connections, devices, onForget }) { role: "link", // TRANSLATORS: %s is replaced by a network connection name "aria-label": sprintf(_("Edit connection %s"), connection.id), - onClick: () => navigate(`connections/${connection.id}/edit`), + onClick: () => navigate(generatePath(PATHS.editConnection, { id: connection.id })), }, typeof onForget === "function" && { title: _("Forget"), diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index 3de10cd297..dd1216d346 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -25,10 +25,11 @@ import React from "react"; import { CardBody, Grid, GridItem } from "@patternfly/react-core"; import { ButtonLink, CardField, EmptyState, Page } from "~/components/core"; import { ConnectionsTable } from "~/components/network"; -import { _ } from "~/i18n"; import { formatIp } from "~/client/network/utils"; -import { sprintf } from "sprintf-js"; import { useNetwork, useNetworkConfigChanges } from "~/queries/network"; +import { PATHS } from "~/routes/network"; +import { sprintf } from "sprintf-js"; +import { _ } from "~/i18n"; /** * Page component holding Network settings @@ -70,7 +71,7 @@ export default function NetworkPage() { + {activeConnection ? _("Change") : _("Connect")} } @@ -101,8 +102,8 @@ export default function NetworkPage() { return ( 0 && _("Wired")}> - {total === 0 && ()} - {total !== 0 && ()} + {total === 0 && } + {total !== 0 && } ); diff --git a/web/src/components/network/WifiNetworksListPage.jsx b/web/src/components/network/WifiNetworksListPage.jsx index d00b4cd2b9..12e362a298 100644 --- a/web/src/components/network/WifiNetworksListPage.jsx +++ b/web/src/components/network/WifiNetworksListPage.jsx @@ -43,15 +43,17 @@ import { Split, Stack, } from "@patternfly/react-core"; +import { generatePath } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; import { Icon } from "~/components/layout"; import { WifiConnectionForm } from "~/components/network"; import { ButtonLink } from "~/components/core"; import { DeviceState } from "~/client/network/model"; -import { useInstallerClient } from "~/context/installer"; -import { _ } from "~/i18n"; import { formatIp } from "~/client/network/utils"; -import { useQueryClient } from "@tanstack/react-query"; +import { useInstallerClient } from "~/context/installer"; import { useSelectedWifi, useSelectedWifiChange } from "~/queries/network"; +import { PATHS } from "~/routes/network"; +import { _ } from "~/i18n"; const HIDDEN_NETWORK = Object.freeze({ hidden: true }); @@ -92,7 +94,7 @@ const WifiDrawerPanelBody = ({ network, onCancel }) => { const { data } = useSelectedWifi(); const forgetNetwork = async () => { await client.network.deleteConnection(network.settings.id); - queryClient.invalidateQueries({ queryKey: ["network", "connections"] }) + queryClient.invalidateQueries({ queryKey: ["network", "connections"] }); }; if (!network) return; @@ -109,7 +111,9 @@ const WifiDrawerPanelBody = ({ network, onCancel }) => { await client.network.connectTo(network.settings)}> {_("Connect")} - {_("Edit")} + + {_("Edit")} + @@ -131,7 +135,7 @@ const WifiDrawerPanelBody = ({ network, onCancel }) => { await client.network.disconnect(network.settings)}> {_("Disconnect")} - + {_("Edit")} From 00fb2daa52ab232304d60dc33e00371e06f0a669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 26 Jul 2024 13:09:00 +0100 Subject: [PATCH 310/430] fix(web): make useProduct hook more robust --- web/src/queries/software.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index af6dfa7572..9834bf8ec9 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -119,17 +119,17 @@ const useConfigMutation = () => { */ const useProduct = ( options?: QueryHookOptions, -): { products: Product[]; selectedProduct: Product | undefined } => { +): { products?: Product[]; selectedProduct?: Product } => { const func = options?.suspense ? useSuspenseQueries : useQueries; - const [{ data: selected }, { data: products }] = func({ + const [ + { data: selected, isPending: isSelectedPending }, + { data: products, isPending: isProductsPending }, + ] = func({ queries: [selectedProductQuery(), productsQuery()], }); - if (!products) { - return { - products: [], - selectedProduct: undefined, - }; + if (isSelectedPending || isProductsPending) { + return {}; } const selectedProduct = products.find((p: Product) => p.id === selected); From 2675ef5775ed264ca5f0a335d6ed52efd0259272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 26 Jul 2024 15:11:05 +0100 Subject: [PATCH 311/430] fix(web): add a Route type Needed to avoid type errors in subsequent changes because not known types of `handle` object values. --- web/src/routes/l10n.tsx | 3 ++- web/src/routes/network.tsx | 3 ++- web/src/routes/products.tsx | 4 +++- web/src/routes/software.tsx | 3 ++- web/src/routes/storage.tsx | 3 ++- web/src/routes/users.tsx | 3 ++- web/src/types/routes.ts | 31 +++++++++++++++++++++++++++++++ 7 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 web/src/types/routes.ts diff --git a/web/src/routes/l10n.tsx b/web/src/routes/l10n.tsx index 4cd6abb3bd..b02c407abc 100644 --- a/web/src/routes/l10n.tsx +++ b/web/src/routes/l10n.tsx @@ -21,6 +21,7 @@ import React from "react"; import { L10nPage, LocaleSelection, KeymapSelection, TimezoneSelection } from "~/components/l10n"; +import { Route } from "~/types/routes"; import { N_ } from "~/i18n"; const PATHS = { @@ -30,7 +31,7 @@ const PATHS = { timezoneSelection: "/l10n/timezone/select", }; -const routes = () => ({ +const routes = (): Route => ({ path: PATHS.root, handle: { name: N_("Localization"), diff --git a/web/src/routes/network.tsx b/web/src/routes/network.tsx index dcbbe97bbd..b1c93140b8 100644 --- a/web/src/routes/network.tsx +++ b/web/src/routes/network.tsx @@ -21,6 +21,7 @@ import React from "react"; import { NetworkPage, IpSettingsForm, WifiSelectorPage } from "~/components/network"; +import { Route } from "~/types/routes"; import { N_ } from "~/i18n"; const PATHS = { @@ -29,7 +30,7 @@ const PATHS = { wifis: "/network/wifis", }; -const routes = () => ({ +const routes = (): Route => ({ path: PATHS.root, handle: { name: N_("Network"), diff --git a/web/src/routes/products.tsx b/web/src/routes/products.tsx index 3a8bdd51db..9e7a55772a 100644 --- a/web/src/routes/products.tsx +++ b/web/src/routes/products.tsx @@ -21,13 +21,15 @@ import React from "react"; import { ProductSelectionPage, ProductSelectionProgress } from "~/components/product"; +import { Route } from "~/types/routes"; const PATHS = { root: "/products", + changeProduct: "/products", progress: "/products/progress", }; -const routes = () => ({ +const routes = (): Route => ({ path: PATHS.root, children: [ { diff --git a/web/src/routes/software.tsx b/web/src/routes/software.tsx index ce0b5d1315..d30b13e44f 100644 --- a/web/src/routes/software.tsx +++ b/web/src/routes/software.tsx @@ -22,6 +22,7 @@ import React from "react"; import SoftwarePage from "~/components/software/SoftwarePage"; import SoftwarePatternsSelection from "~/components/software/SoftwarePatternsSelection"; +import { Route } from "~/types/routes"; import { N_ } from "~/i18n"; const PATHS = { @@ -29,7 +30,7 @@ const PATHS = { patternsSelection: "/software/patterns/select", }; -const routes = () => ({ +const routes = (): Route => ({ path: PATHS.root, handle: { name: N_("Software"), diff --git a/web/src/routes/storage.tsx b/web/src/routes/storage.tsx index 8b77399cd2..4c7fcb5c02 100644 --- a/web/src/routes/storage.tsx +++ b/web/src/routes/storage.tsx @@ -25,6 +25,7 @@ import DeviceSelection from "~/components/storage/DeviceSelection"; import SpacePolicySelection from "~/components/storage/SpacePolicySelection"; import ISCSIPage from "~/components/storage/ISCSIPage"; import ProposalPage from "~/components/storage/ProposalPage"; +import { Route } from "~/types/routes"; import { N_ } from "~/i18n"; const PATHS = { @@ -35,7 +36,7 @@ const PATHS = { iscsi: "/storage/iscsi", }; -const routes = () => ({ +const routes = (): Route => ({ path: PATHS.root, handle: { name: N_("Storage"), icon: "hard_drive" }, children: [ diff --git a/web/src/routes/users.tsx b/web/src/routes/users.tsx index 38d0439311..e16db57c85 100644 --- a/web/src/routes/users.tsx +++ b/web/src/routes/users.tsx @@ -22,6 +22,7 @@ import React from "react"; import UsersPage from "~/components/users/UsersPage"; import FirstUserForm from "~/components/users/FirstUserForm"; +import { Route } from "~/types/routes"; import { N_ } from "~/i18n"; const PATHS = { @@ -31,7 +32,7 @@ const PATHS = { edit: "/users/first/edit", }, }; -const routes = () => ({ +const routes = (): Route => ({ path: PATHS.root, handle: { name: N_("Users"), diff --git a/web/src/types/routes.ts b/web/src/types/routes.ts new file mode 100644 index 0000000000..2c51c087d4 --- /dev/null +++ b/web/src/types/routes.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import { RouteObject } from "react-router-dom"; + +type RouteHandle = { + name: string; + icon?: string; +}; + +type Route = RouteObject & { handle?: RouteHandle }; + +export type { Route }; From bf5ac366bda88bb848dcac05840284c9d5c666fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 26 Jul 2024 11:35:25 +0100 Subject: [PATCH 312/430] refactor(web): use queries to track progress --- web/src/components/core/ProgressReport.jsx | 82 +++++------ .../components/core/ProgressReport.test.jsx | 127 +++++++----------- web/src/queries/progress.ts | 91 +++++++++++++ web/src/types/progress.ts | 58 ++++++++ 4 files changed, 243 insertions(+), 115 deletions(-) create mode 100644 web/src/queries/progress.ts create mode 100644 web/src/types/progress.ts diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index 63b0230231..9791d8dd49 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -35,7 +35,13 @@ import { import { _ } from "~/i18n"; import { Center } from "~/components/layout"; -import { useInstallerClient } from "~/context/installer"; +import { + progressQuery, + useProgress, + useProgressChanges, + useResetProgress, +} from "~/queries/progress"; +import { useQuery } from "@tanstack/react-query"; const Progress = ({ steps, step, firstStep, detail }) => { const stepProperties = (stepNumber) => { @@ -45,9 +51,9 @@ const Progress = ({ steps, step, firstStep, detail }) => { titleId: `step-${stepNumber}-title`, }; - if (stepNumber < step.current) { - properties.variant = "success"; - properties.description =
      {_("Finished")}
      ; + if (stepNumber > step.current) { + properties.variant = "pending"; + properties.description =
      {_("Pending")}
      ; } if (properties.isCurrent) { @@ -69,9 +75,9 @@ const Progress = ({ steps, step, firstStep, detail }) => { } } - if (stepNumber > step.current) { - properties.variant = "pending"; - properties.description =
      {_("Pending")}
      ; + if (stepNumber < step.current || step.finished) { + properties.variant = "success"; + properties.description =
      {_("Finished")}
      ; } return properties; @@ -95,47 +101,43 @@ const Progress = ({ steps, step, firstStep, detail }) => { ); }; +function findDetail(progresses) { + return progresses.find((progress) => { + return progress?.finished === false; + }); +} + /** * @component * * Shows progress steps when a product is selected. */ function ProgressReport({ title, firstStep }) { - const { manager, storage, software } = useInstallerClient(); - const [steps, setSteps] = useState(); - const [step, setStep] = useState(); - const [detail, setDetail] = useState(); - - useEffect(() => software.onProgressChange(setDetail), [software, setDetail]); - useEffect(() => storage.onProgressChange(setDetail), [storage, setDetail]); + const progress = useProgress("manager", { suspense: true }); + const [steps, setSteps] = useState(progress.steps); + const softwareProgress = useProgress("software"); + const storageProgress = useProgress("storage"); + useResetProgress(); + useProgressChanges(); useEffect(() => { - manager.getProgress().then((progress) => { - setSteps(progress.steps); - setStep(progress); - }); - - return manager.onProgressChange(setStep); - }, [manager, setSteps]); - - const Content = () => { - if (!steps) { - return; - } - - return ( - - ); - }; + if (progress.steps.length === 0) return; + + setSteps(progress.steps); + }, [progress, steps]); + const detail = findDetail([softwareProgress, storageProgress]); + + const Content = () => ( + + ); - const progressTitle = !steps ? _("Waiting for progress status...") : title; return (
      @@ -149,7 +151,7 @@ function ProgressReport({ title, firstStep }) { >

      - {progressTitle} + {title}

      diff --git a/web/src/components/core/ProgressReport.test.jsx b/web/src/components/core/ProgressReport.test.jsx index f90b083b5d..1a2384195a 100644 --- a/web/src/components/core/ProgressReport.test.jsx +++ b/web/src/components/core/ProgressReport.test.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -22,98 +22,75 @@ import React from "react"; import { act, screen } from "@testing-library/react"; -import { installerRender, createCallbackMock } from "~/test-utils"; -import { createClient } from "~/client"; +import { plainRender } from "~/test-utils"; import { ProgressReport } from "~/components/core"; -jest.mock("~/client"); +let mockProgress; -let callbacks; -let onManagerProgressChange = jest.fn(); -let onSoftwareProgressChange = jest.fn(); -let onStorageProgressChange = jest.fn(); - -beforeEach(() => { - createClient.mockImplementation(() => { - return { - manager: { - onProgressChange: onManagerProgressChange, - getProgress: jest.fn().mockResolvedValue({ - message: "Partition disks", - current: 1, - total: 10, - steps: ["Partition disks", "Install software"], - }), - }, - software: { - onProgressChange: onSoftwareProgressChange, - }, - storage: { - onProgressChange: onStorageProgressChange, - }, - }; - }); -}); +jest.mock("~/queries/progress", () => ({ + ...jest.requireActual("~/queries/progress"), + useProgress: (service) => mockProgress[service], +})); describe("ProgressReport", () => { - describe("when there is progress information available", () => { + describe("when there are details of the storage service", () => { beforeEach(() => { - const [onManagerProgress, managerCallbacks] = createCallbackMock(); - const [onSoftwareProgress, softwareCallbacks] = createCallbackMock(); - const [onStorageProgress, storageCallbacks] = createCallbackMock(); - onManagerProgressChange = onManagerProgress; - onSoftwareProgressChange = onSoftwareProgress; - onStorageProgressChange = onStorageProgress; - callbacks = { - manager: managerCallbacks, - software: softwareCallbacks, - storage: storageCallbacks, + mockProgress = { + manager: { + message: "Partition disks", + current: 1, + total: 3, + steps: ["Partition disks", "Install software", "Install bootloader"], + }, + storage: { + message: "Doing some partitioning", + current: 1, + total: 1, + finished: false, + }, }; }); - it("shows the progress including the details from the storage service", async () => { - installerRender(); + it("shows the progress including the details", () => { + plainRender(); - await screen.findByText(/Waiting/i); - await screen.findByText(/Partition disks/i); - await screen.findByText(/Install software/i); - - const cb = callbacks.storage[callbacks.storage.length - 1]; - act(() => { - cb({ - message: "Doing some partitioning", - current: 1, - total: 10, - finished: false, - }); - }); + expect(screen.getByText(/Partition disks/)).toBeInTheDocument(); + expect(screen.getByText(/Install software/)).toBeInTheDocument(); // NOTE: not finding the whole text because it is now split in two because of PF/Truncate - await screen.findByText(/Doing some/); - await screen.findByText(/\(1\/10\)/); + expect(screen.getByText(/Doing some/)).toBeInTheDocument(); + expect(screen.getByText(/\(1\/1\)/)).toBeInTheDocument(); }); + }); - it("shows the progress including the details from the software service", async () => { - installerRender(); + describe("when there are details of the software service", () => { + beforeEach(() => { + mockProgress = { + manager: { + message: "Installing software", + current: 2, + total: 3, + steps: ["Partition disks", "Install software", "Install bootloader"], + }, + software: { + message: "Installing vim", + current: 5, + total: 200, + finished: false, + }, + }; + }); - await screen.findByText(/Waiting/i); - await screen.findByText(/Install software/i); + it("shows the progress including the details", () => { + plainRender(); - const cb = callbacks.software[callbacks.software.length - 1]; - act(() => { - cb({ - message: "Installing packages", - current: 495, - total: 500, - finished: false, - }); - }); + expect(screen.getByText(/Partition disks/)).toBeInTheDocument(); + expect(screen.getByText(/Install software/)).toBeInTheDocument(); - // NOTE: not finding the whole "Intalling packages (495/500)" because it - // is now split in two because of PF/Truncate - await screen.findByText(/Installing/); - await screen.findByText(/.*\(495\/500\)/); + // NOTE: not finding the whole text because it is now split in two because of PF/Truncate + expect(screen.getByText(/Installing vim/)).toBeInTheDocument(); + expect(screen.getByText(/\(5\/200\)/)).toBeInTheDocument(); }); }); }); diff --git a/web/src/queries/progress.ts b/web/src/queries/progress.ts new file mode 100644 index 0000000000..d7fbfff5c2 --- /dev/null +++ b/web/src/queries/progress.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { useInstallerClient } from "~/context/installer"; +import { Progress } from "~/types/progress"; + +const servicesMap = { + "org.opensuse.Agama.Manager1": "manager", + "org.opensuse.Agama.Software1": "software", + "org.opensuse.Agama.Storage1": "storage", +}; + +const progressQuery = (service: string) => { + return { + queryKey: ["progress", service], + queryFn: () => + fetch(`/api/${service}/progress`) + .then((res) => res.json()) + .then((body) => Progress.fromApi(body)), + }; +}; + +type UseProgressOptions = { + suspense: boolean; +}; + +const useProgress = (service: string, options?: QueryHookOptions): Progress => { + const query = progressQuery(service); + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func(query); + return data; +}; + +const useProgressChanges = () => { + const client = useInstallerClient(); + const queryClient = useQueryClient(); + + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + if (event.type === "Progress") { + const service = servicesMap[event.service]; + if (!service) { + console.warn("Unknown service", event.service); + return; + } + + const data = queryClient.getQueryData(["progress", service]); + if (data) { + // NOTE: steps are not coming in the updates + const steps = (data as Progress).steps; + const fromEvent = Progress.fromApi(event); + queryClient.setQueryData(["progress", service], { ...fromEvent, steps }); + } + } + }); + }, [client, queryClient]); +}; + +const useResetProgress = () => { + const queryClient = useQueryClient(); + + React.useEffect(() => { + return () => { + queryClient.invalidateQueries({ queryKey: ["progress"] }); + }; + }, []); +}; + +export { useProgress, useProgressChanges, useResetProgress, progressQuery }; diff --git a/web/src/types/progress.ts b/web/src/types/progress.ts new file mode 100644 index 0000000000..d32da63524 --- /dev/null +++ b/web/src/types/progress.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +type ProgressApi = { + currentStep: number; + maxSteps: number; + currentTitle: string; + finished: boolean; + steps?: string[]; + service: string; +}; + +class Progress { + total: number; + current: number; + message: string; + finished: boolean; + steps: string[]; + + constructor(current: number, total: number, message: string, finished: boolean, steps: string[]) { + this.current = current; + this.total = total; + this.message = message; + this.finished = finished; + this.steps = steps; + } + + static fromApi(progress: ProgressApi) { + const { + currentStep: current, + maxSteps: total, + currentTitle: message, + finished, + steps = [], + } = progress; + return new Progress(current, total, message, finished, steps); + } +} + +export { Progress }; From ae08dddeb2cfd2032e4ab494d5ea97ca03dfb7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 26 Jul 2024 15:45:45 +0100 Subject: [PATCH 313/430] refactor(web): drop the WithProgress mixin --- web/src/client/manager.js | 8 +--- web/src/client/mixins.js | 85 +------------------------------------- web/src/client/software.js | 8 +--- web/src/client/storage.js | 8 +--- 4 files changed, 7 insertions(+), 102 deletions(-) diff --git a/web/src/client/manager.js b/web/src/client/manager.js index 365a831b01..2c4a50113b 100644 --- a/web/src/client/manager.js +++ b/web/src/client/manager.js @@ -21,7 +21,7 @@ // @ts-check -import { WithProgress, WithStatus } from "./mixins"; +import { WithStatus } from "./mixins"; const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; @@ -148,10 +148,6 @@ class ManagerBaseClient { /** * Client to interact with the Agama manager service */ -class ManagerClient extends WithProgress( - WithStatus(ManagerBaseClient, "/manager/status", MANAGER_SERVICE), - "/manager/progress", - MANAGER_SERVICE, -) {} +class ManagerClient extends WithStatus(ManagerBaseClient, "/manager/status", MANAGER_SERVICE) {} export { ManagerClient }; diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index 18acdab3ab..1c60923bd0 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -113,89 +113,6 @@ const WithStatus = (superclass, status_path, service_name) => } }; -/** - * @typedef {object} Progress - * @property {number} total - number of steps - * @property {number} current - current step - * @property {string} message - message of the current step - * @property {boolean} finished - whether the progress already finished - */ - -/** - * @typedef {object} ProgressSequence - * @property {string[]} steps - sequence steps if known in advance - * @property {number} total - number of steps - * @property {number} current - current step - * @property {string} message - message of the current step - * @property {boolean} finished - whether the progress already finished - */ - -/** - * @callback ProgressHandler - * @param {Progress} progress - progress status - * @return {void} - */ - -/** - * Extends the given class with methods to get and track the service progress - * - * @template {!WithHTTPClient} T - * @param {T} superclass - superclass to extend - * @param {string} progress_path - status resource path (e.g., "/manager/status"). - * @param {string} service_name - service name (e.g., "org.opensuse.Agama.Manager1"). - */ -const WithProgress = (superclass, progress_path, service_name) => - class extends superclass { - /** - * Returns the service progress - * - * @return {Promise} an object containing the total steps, - * the current step and whether the service finished or not. - */ - async getProgress() { - const response = await this.client.get(progress_path); - if (!response.ok) { - console.log("get progress failed with:", response); - return { - steps: [], - total: 0, - current: 0, - message: "Failed to get progress", - finished: false, - }; - } else { - const { steps, currentStep, maxSteps, currentTitle, finished } = await response.json(); - return { - steps, - total: maxSteps, - current: currentStep, - message: currentTitle, - finished, - }; - } - } - - /** - * Register a callback to run when the progress changes - * - * @param {ProgressHandler} handler - callback function - * @return {import ("./http").RemoveFn} function to disable the callback - */ - onProgressChange(handler) { - return this.client.onEvent("Progress", ({ service, ...progress }) => { - if (service === service_name) { - const { currentStep, maxSteps, currentTitle, finished } = progress; - handler({ - total: maxSteps, - current: currentStep, - message: currentTitle, - finished, - }); - } - }); - } - }; - /** * @typedef {object} ValidationError * @property {string} message - Error message @@ -214,4 +131,4 @@ const createError = (message) => { return { message }; }; -export { WithProgress, WithStatus }; +export { WithStatus }; diff --git a/web/src/client/software.js b/web/src/client/software.js index e5a5fd0ad2..1e886bad56 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -21,7 +21,7 @@ // @ts-check -import { WithProgress, WithStatus } from "./mixins"; +import { WithStatus } from "./mixins"; const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1"; @@ -45,11 +45,7 @@ class SoftwareBaseClient { /** * Manages software and product configuration. */ -class SoftwareClient extends WithProgress( - WithStatus(SoftwareBaseClient, "/software/status", SOFTWARE_SERVICE), - "/software/progress", - SOFTWARE_SERVICE, -) {} +class SoftwareClient extends WithStatus(SoftwareBaseClient, "/software/status", SOFTWARE_SERVICE) {} class ProductClient { /** diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 2734dbd896..3a14b4e318 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -23,7 +23,7 @@ // cspell:ignore ptable import { compact, hex, uniq } from "~/utils"; -import { WithProgress, WithStatus } from "./mixins"; +import { WithStatus } from "./mixins"; import { HTTPClient } from "./http"; const SERVICE_NAME = "org.opensuse.Agama.Storage1"; @@ -1652,10 +1652,6 @@ class StorageBaseClient { /** * Allows interacting with the storage settings */ -class StorageClient extends WithProgress( - WithStatus(StorageBaseClient, "/storage/status", SERVICE_NAME), - "/storage/progress", - SERVICE_NAME, -) {} +class StorageClient extends WithStatus(StorageBaseClient, "/storage/status", SERVICE_NAME) {} export { StorageClient, EncryptionMethods }; From adb1fe2f5dcf0c1d46f8e2aec2a8a52fa33487d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 26 Jul 2024 17:48:35 +0200 Subject: [PATCH 314/430] Live: Fixed building the PXE image --- live/src/agama-installer.kiwi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/live/src/agama-installer.kiwi b/live/src/agama-installer.kiwi index 6adc773307..d1d93c08d0 100644 --- a/live/src/agama-installer.kiwi +++ b/live/src/agama-installer.kiwi @@ -184,7 +184,7 @@ - + From b42fc2b4e6309281a80754926c73f643d1441c0b Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 28 Jul 2024 02:52:57 +0000 Subject: [PATCH 315/430] Update service PO files Agama-weblate commit: e9e0281b40b6dbc2e580ee437bcac18c44e36c0e --- service/po/cs.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/po/cs.po b/service/po/cs.po index aad8bf2983..d0ebd81fa9 100644 --- a/service/po/cs.po +++ b/service/po/cs.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-07-10 02:23+0000\n" -"PO-Revision-Date: 2024-07-12 07:47+0000\n" +"PO-Revision-Date: 2024-07-22 20:47+0000\n" "Last-Translator: Aleš Kastner \n" "Language-Team: Czech \n" @@ -192,7 +192,7 @@ msgstr "Zapisuji konfiguraci boot zavaděče v sysconfig" #. @return [Issue] #: service/lib/agama/storage/proposal.rb:192 msgid "Cannot accommodate the required file systems for installation" -msgstr "Nemohu zajistit systémy souborů pro instalaci" +msgstr "Nelze umístit požadované souborové systémy pro instalaci" #. Issue to communicate a generic Y2Storage error. #. From 373f722ba8b53fdf86b9c3aec59dd29312683737 Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 28 Jul 2024 02:52:58 +0000 Subject: [PATCH 316/430] Update web PO files Agama-weblate commit: e9e0281b40b6dbc2e580ee437bcac18c44e36c0e --- web/po/ca.po | 357 ++++++------- web/po/cs.po | 1148 +++++++++++++++++++++------------------- web/po/de.po | 342 ++++++------ web/po/es.po | 345 ++++++------ web/po/fr.po | 336 ++++++------ web/po/id.po | 326 +++++------- web/po/ja.po | 343 ++++++------ web/po/ka.po | 314 +++++------ web/po/mk.po | 285 ++++------ web/po/nb_NO.po | 340 ++++++------ web/po/nl.po | 347 ++++++------ web/po/pt_BR.po | 340 ++++++------ web/po/ru.po | 354 ++++++------- web/po/sv.po | 351 ++++++------ web/po/tr.po | 294 ++++------ web/po/uk.po | 281 ++++------ web/po/zh_Hans.po | 338 ++++++------ web/src/languages.json | 23 +- 18 files changed, 3006 insertions(+), 3458 deletions(-) diff --git a/web/po/ca.po b/web/po/ca.po index 6b84b4dbc6..7132f194c6 100644 --- a/web/po/ca.po +++ b/web/po/ca.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-18 02:24+0000\n" -"PO-Revision-Date: 2024-07-18 11:47+0000\n" +"POT-Creation-Date: 2024-07-28 02:29+0000\n" +"PO-Revision-Date: 2024-07-25 08:46+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan \n" @@ -56,7 +56,6 @@ msgstr "" "Per obtenir-ne més informació, visiteu el repositori del projecte a %s." #: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 -#: src/components/software/SoftwarePatternsSelection.jsx:268 msgid "Close" msgstr "Tanca" @@ -85,7 +84,7 @@ msgstr "Continua" #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 #: src/components/core/Popup.jsx:132 -#: src/components/network/WifiConnectionForm.jsx:134 +#: src/components/network/WifiConnectionForm.jsx:141 msgid "Cancel" msgstr "Cancel·la" @@ -181,14 +180,13 @@ msgstr "No es pot canviar a la instal·lació remota." #: src/components/core/InstallerOptions.jsx:142 #: src/components/network/IpSettingsForm.jsx:228 -#: src/components/product/ProductRegistrationPage.jsx:85 #: src/components/storage/BootSelection.jsx:250 #: src/components/storage/DeviceSelection.jsx:254 #: src/components/storage/EncryptionSettingsDialog.jsx:155 #: src/components/storage/SpacePolicySelection.jsx:200 #: src/components/storage/VolumeDialog.jsx:794 #: src/components/storage/ZFCPPage.jsx:528 -#: src/components/users/FirstUserForm.jsx:303 +#: src/components/users/FirstUserForm.jsx:293 msgid "Accept" msgstr "Accepta-ho" @@ -270,11 +268,13 @@ msgstr "" msgid "Passwords do not match" msgstr "Les contrasenyes no coincideixen." +#. TRANSLATORS: field label #: src/components/core/PasswordAndConfirmationInput.jsx:79 -#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/network/WifiConnectionForm.jsx:128 +#: src/components/questions/QuestionWithPassword.jsx:48 #: src/components/storage/iscsi/AuthFields.jsx:90 #: src/components/storage/iscsi/AuthFields.jsx:94 -#: src/components/users/RootAuthMethods.jsx:165 +#: src/components/users/RootAuthMethods.jsx:130 msgid "Password" msgstr "Contrasenya" @@ -366,11 +366,11 @@ msgstr "Encara no s'ha seleccionat." #: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 #: src/components/l10n/L10nPage.jsx:83 -#: src/components/network/NetworkPage.jsx:112 +#: src/components/network/NetworkPage.jsx:74 #: src/components/storage/InstallationDeviceField.jsx:108 #: src/components/storage/ProposalActionsSummary.jsx:238 -#: src/components/users/RootAuthMethods.jsx:100 -#: src/components/users/RootAuthMethods.jsx:112 +#: src/components/users/RootAuthMethods.jsx:74 +#: src/components/users/RootAuthMethods.jsx:86 msgid "Change" msgstr "Canvia" @@ -449,7 +449,7 @@ msgstr "Llista de dades d'adreces" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:64 -#: src/components/network/ConnectionsTable.jsx:92 +#: src/components/network/ConnectionsTable.jsx:93 #: src/components/storage/ZFCPPage.jsx:381 #: src/components/storage/iscsi/InitiatorForm.jsx:52 #: src/components/storage/iscsi/InitiatorPresenter.jsx:68 @@ -461,41 +461,46 @@ msgstr "Nom" #. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:66 -#: src/components/network/ConnectionsTable.jsx:93 +#: src/components/network/ConnectionsTable.jsx:94 msgid "IP addresses" msgstr "Adreces IP" -#: src/components/network/ConnectionsTable.jsx:74 -#: src/components/network/WifiNetworksListPage.jsx:107 -#: src/components/network/WifiNetworksListPage.jsx:130 +#. TRANSLATORS: table header aria label +#: src/components/network/ConnectionsTable.jsx:68 +msgid "Connection actions" +msgstr "Accions de connexió" + +#: src/components/network/ConnectionsTable.jsx:75 +#: src/components/network/WifiNetworksListPage.jsx:112 +#: src/components/network/WifiNetworksListPage.jsx:135 #: src/components/storage/PartitionsField.jsx:347 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 -#: src/components/users/FirstUser.jsx:120 +#: src/components/users/FirstUser.jsx:85 msgid "Edit" msgstr "Edita" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/ConnectionsTable.jsx:78 #: src/components/network/IpSettingsForm.jsx:151 #, c-format msgid "Edit connection %s" msgstr "Edita la connexió %s" -#: src/components/network/ConnectionsTable.jsx:81 -#: src/components/network/WifiNetworksListPage.jsx:109 -#: src/components/network/WifiNetworksListPage.jsx:137 +#: src/components/network/ConnectionsTable.jsx:82 +#: src/components/network/WifiNetworksListPage.jsx:114 +#: src/components/network/WifiNetworksListPage.jsx:142 msgid "Forget" msgstr "Oblida-la" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:83 +#: src/components/network/ConnectionsTable.jsx:84 #, c-format msgid "Forget connection %s" msgstr "Oblida la connexió %s" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:98 +#: src/components/network/ConnectionsTable.jsx:99 #, c-format msgid "Actions for connection %s" msgstr "Accions per a la connexió %s" @@ -556,11 +561,11 @@ msgstr "Passarel·la" msgid "Gateway can be defined only in 'Manual' mode" msgstr "La passarel·la només es pot definir en mode manual." -#: src/components/network/NetworkPage.jsx:93 +#: src/components/network/NetworkPage.jsx:55 msgid "No Wi-Fi supported" msgstr "No és compatible amb Wi-Fi." -#: src/components/network/NetworkPage.jsx:95 +#: src/components/network/NetworkPage.jsx:57 msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." @@ -568,125 +573,124 @@ msgstr "" "El sistema no admet connexions de wifi, probablement a causa de maquinari " "que manca o que està inhabilitat." -#: src/components/network/NetworkPage.jsx:109 +#: src/components/network/NetworkPage.jsx:71 msgid "Wi-Fi" msgstr "Wifi" #. TRANSLATORS: button label, connect to a WiFi network -#: src/components/network/NetworkPage.jsx:112 -#: src/components/network/WifiConnectionForm.jsx:130 -#: src/components/network/WifiNetworksListPage.jsx:105 +#: src/components/network/NetworkPage.jsx:74 +#: src/components/network/WifiConnectionForm.jsx:137 +#: src/components/network/WifiNetworksListPage.jsx:110 msgid "Connect" msgstr "Connecta't" -#: src/components/network/NetworkPage.jsx:119 +#: src/components/network/NetworkPage.jsx:81 #, c-format msgid "Conected to %s" msgstr "Connectat amb %s" -#: src/components/network/NetworkPage.jsx:126 +#: src/components/network/NetworkPage.jsx:88 msgid "No connected yet" msgstr "Encara no s'ha connetat." -#: src/components/network/NetworkPage.jsx:127 +#: src/components/network/NetworkPage.jsx:89 msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." msgstr "" "El sistema encara no s'ha configurat per connectar-se a una xarxa de wifi." -#: src/components/network/NetworkPage.jsx:156 +#: src/components/network/NetworkPage.jsx:102 msgid "Wired" msgstr "Amb fil" -#: src/components/network/NetworkPage.jsx:160 +#: src/components/network/NetworkPage.jsx:104 msgid "No wired connections found" msgstr "No s'ha trobat cap connexió amb fil." -#: src/components/network/NetworkPage.jsx:173 -#: src/components/network/routes.js:59 +#: src/components/network/NetworkPage.jsx:114 +#: src/components/network/routes.js:33 msgid "Network" msgstr "Xarxa" #. TRANSLATORS: WiFi authentication mode -#: src/components/network/WifiConnectionForm.jsx:43 +#: src/components/network/WifiConnectionForm.jsx:45 #: src/components/storage/iscsi/InitiatorPresenter.jsx:72 msgid "None" msgstr "Cap" #. TRANSLATORS: WiFi authentication mode -#: src/components/network/WifiConnectionForm.jsx:45 +#: src/components/network/WifiConnectionForm.jsx:47 msgid "WPA & WPA2 Personal" msgstr "WPA i WPA2 personal" -#: src/components/network/WifiConnectionForm.jsx:85 -#: src/components/product/ProductRegistrationPage.jsx:68 +#: src/components/network/WifiConnectionForm.jsx:92 #: src/components/storage/ZFCPDiskForm.jsx:105 #: src/components/storage/iscsi/DiscoverForm.jsx:98 #: src/components/storage/iscsi/LoginForm.jsx:69 -#: src/components/users/FirstUserForm.jsx:217 +#: src/components/users/FirstUserForm.jsx:207 msgid "Something went wrong" msgstr "Alguna cosa ha anat malament." -#: src/components/network/WifiConnectionForm.jsx:86 +#: src/components/network/WifiConnectionForm.jsx:93 msgid "Please, review provided settings and try again." msgstr "" "Si us plau, reviseu la configuració proporcionada i torneu-ho a provar." #. TRANSLATORS: SSID (Wifi network name) configuration -#: src/components/network/WifiConnectionForm.jsx:92 -#: src/components/network/WifiConnectionForm.jsx:96 +#: src/components/network/WifiConnectionForm.jsx:99 +#: src/components/network/WifiConnectionForm.jsx:103 msgid "SSID" msgstr "SSID" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:105 -#: src/components/network/WifiConnectionForm.jsx:108 +#: src/components/network/WifiConnectionForm.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:115 msgid "Security" msgstr "Seguretat" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:117 +#: src/components/network/WifiConnectionForm.jsx:124 msgid "WPA Password" msgstr "Contrasenya de WPA" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:63 -#: src/components/network/WifiNetworksListPage.jsx:117 +#: src/components/network/WifiNetworksListPage.jsx:64 +#: src/components/network/WifiNetworksListPage.jsx:122 msgid "Connecting" msgstr "Connectant" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:66 -#: src/components/network/WifiNetworksListPage.jsx:121 -#: src/components/network/WifiNetworksListPage.jsx:164 +#: src/components/network/WifiNetworksListPage.jsx:67 +#: src/components/network/WifiNetworksListPage.jsx:126 +#: src/components/network/WifiNetworksListPage.jsx:169 msgid "Connected" msgstr "Connectat" #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:71 -#: src/components/network/WifiNetworksListPage.jsx:119 +#: src/components/network/WifiNetworksListPage.jsx:72 +#: src/components/network/WifiNetworksListPage.jsx:124 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" msgstr "Desconnectat" -#: src/components/network/WifiNetworksListPage.jsx:127 +#: src/components/network/WifiNetworksListPage.jsx:132 msgid "Disconnect" msgstr "Desconnecta" -#: src/components/network/WifiNetworksListPage.jsx:150 +#: src/components/network/WifiNetworksListPage.jsx:155 msgid "Connect to a hidden network" msgstr "Connecta't a una xarxa oculta" -#: src/components/network/WifiNetworksListPage.jsx:161 +#: src/components/network/WifiNetworksListPage.jsx:166 msgid "configured" msgstr "configurat" -#: src/components/network/WifiNetworksListPage.jsx:265 +#: src/components/network/WifiNetworksListPage.jsx:268 msgid "Connect to hidden network" msgstr "Connecta't a una xarxa oculta" -#: src/components/network/WifiSelectorPage.jsx:136 +#: src/components/network/WifiSelectorPage.jsx:40 msgid "Connect to a Wi-Fi network" msgstr "Connecteu-vos a una xarxa Wi-Fi" @@ -711,8 +715,6 @@ msgid "Storage" msgstr "Emmagatzematge" #: src/components/overview/OverviewPage.jsx:51 -#: src/components/overview/SoftwareSection.jsx:86 -#: src/components/software/SoftwarePage.jsx:181 #: src/components/software/routes.js:32 msgid "Software" msgstr "Programari" @@ -745,16 +747,6 @@ msgstr "" "Aquests són els paràmetres d'instal·lació més rellevants. No dubteu a " "navegar per les seccions del menú per a més detalls." -#: src/components/overview/SoftwareSection.jsx:60 -msgid "The installation will take" -msgstr "La instal·lació necessitarà" - -#. TRANSLATORS: %s will be replaced with the installation size, example: "5GiB". -#: src/components/overview/SoftwareSection.jsx:67 -#, c-format -msgid "The installation will take %s including:" -msgstr "La instal·lació necessitarà %s, incloent-hi el següent:" - #: src/components/overview/StorageSection.jsx:53 msgid "" "Install in a new Logical Volume Manager (LVM) volume group shrinking " @@ -865,19 +857,6 @@ msgstr "" msgid "Overview" msgstr "Resum" -#: src/components/product/ProductRegistrationPage.jsx:66 -#, c-format -msgid "Register %s" -msgstr "Registra %s" - -#: src/components/product/ProductRegistrationPage.jsx:73 -msgid "Registration code" -msgstr "Codi de registre" - -#: src/components/product/ProductRegistrationPage.jsx:76 -msgid "Email" -msgstr "Adreça electrònica" - #: src/components/product/ProductSelectionProgress.jsx:49 msgid "Configuring the product, please wait ..." msgstr "Configurant el producte. Espereu, si us plau..." @@ -901,65 +880,9 @@ msgstr "Dispositiu encriptat" msgid "Encryption Password" msgstr "Contrasenya d'encriptació" -#: src/components/software/SoftwarePage.jsx:85 -msgid "No additional software was selected." -msgstr "No s'ha seleccionat cap programari addicional." - -#: src/components/software/SoftwarePage.jsx:90 -msgid "The following software patterns are selected for installation:" -msgstr "" -"S'han seleccionat els patrons de programari següents per a la instal·lació:" - -#: src/components/software/SoftwarePage.jsx:105 -#: src/components/software/SoftwarePage.jsx:119 -msgid "Selected patterns" -msgstr "Patrons seleccionats" - -#: src/components/software/SoftwarePage.jsx:108 -msgid "Change selection" -msgstr "Canvia la selecció" - -#: src/components/software/SoftwarePage.jsx:123 -msgid "" -"This product does not allow to select software patterns during installation. " -"However, you can add additional software once the installation is finished." -msgstr "" -"Aquest producte no permet seleccionar patrons de programari durant la instal·" -"lació. Tanmateix, hi podeu afegir programari addicional un cop acabada la " -"instal·lació." - -#: src/components/software/SoftwarePatternsSelection.jsx:223 -msgid "auto selected" -msgstr "seleccionat automàticament" - -#: src/components/software/SoftwarePatternsSelection.jsx:241 -msgid "None of the patterns match the filter." -msgstr "Cap dels patrons coincideix amb el filtre." - -#: src/components/software/SoftwarePatternsSelection.jsx:248 -msgid "Software selection" -msgstr "Selecció de programari" - -#. TRANSLATORS: search field placeholder text -#: src/components/software/SoftwarePatternsSelection.jsx:251 -#: src/components/software/SoftwarePatternsSelection.jsx:252 -msgid "Filter by pattern title or description" -msgstr "Filtra per títol o descripció del patró" - -#. TRANSLATORS: %s will be replaced by the estimated installation size, -#. example: "728.8 MiB" -#: src/components/software/UsedSize.jsx:33 -#, c-format -msgid "Installation will take %s." -msgstr "La instal·lació necessitarà %s." - -#: src/components/software/UsedSize.jsx:37 -msgid "" -"This space includes the base system and the selected software patterns, if " -"any." -msgstr "" -"Aquest espai inclou el sistema de base i els patrons de programari " -"seleccionats, si n'hi ha." +#: src/components/questions/QuestionWithPassword.jsx:42 +msgid "Password Required" +msgstr "Cal una contrasenya." #: src/components/storage/BootConfigField.jsx:43 msgid "Change boot options" @@ -1066,7 +989,7 @@ msgstr "Identificador del canal" #: src/components/storage/ZFCPPage.jsx:325 #: src/components/storage/iscsi/NodesPresenter.jsx:102 #: src/components/storage/iscsi/NodesPresenter.jsx:123 -#: src/components/users/RootAuthMethods.jsx:159 +#: src/components/users/RootAuthMethods.jsx:124 msgid "Status" msgstr "Estat" @@ -2378,11 +2301,11 @@ msgstr "Seleccioneu què voleu fer amb cada partició." msgid "with custom actions" msgstr "amb accions personalitzades." -#: src/components/users/FirstUser.jsx:35 +#: src/components/users/FirstUser.jsx:34 msgid "No user defined yet." msgstr "Encara no s'ha definit cap usuari." -#: src/components/users/FirstUser.jsx:39 +#: src/components/users/FirstUser.jsx:38 msgid "" "Please, be aware that a user must be defined before installing the system to " "be able to log into it." @@ -2390,76 +2313,72 @@ msgstr "" "Si us plau, tingueu en compte que cal definir un usuari abans d'instal·lar " "el sistema per poder-hi iniciar sessió." -#: src/components/users/FirstUser.jsx:45 +#: src/components/users/FirstUser.jsx:44 msgid "Define a user now" msgstr "Definiu un usuari ara" -#: src/components/users/FirstUser.jsx:58 -#: src/components/users/FirstUserForm.jsx:227 +#: src/components/users/FirstUser.jsx:57 +#: src/components/users/FirstUserForm.jsx:217 msgid "Full name" msgstr "Nom complet" -#: src/components/users/FirstUser.jsx:59 -#: src/components/users/FirstUserForm.jsx:241 -#: src/components/users/FirstUserForm.jsx:246 -#: src/components/users/FirstUserForm.jsx:249 +#: src/components/users/FirstUser.jsx:58 +#: src/components/users/FirstUserForm.jsx:231 +#: src/components/users/FirstUserForm.jsx:236 +#: src/components/users/FirstUserForm.jsx:239 msgid "Username" msgstr "Nom d'usuari" -#: src/components/users/FirstUser.jsx:124 -#: src/components/users/RootAuthMethods.jsx:104 -#: src/components/users/RootAuthMethods.jsx:116 +#: src/components/users/FirstUser.jsx:89 +#: src/components/users/RootAuthMethods.jsx:78 +#: src/components/users/RootAuthMethods.jsx:90 msgid "Discard" msgstr "Descarta'l" -#: src/components/users/FirstUserForm.jsx:57 +#: src/components/users/FirstUserForm.jsx:58 msgid "Username suggestion dropdown" msgstr "Menú desplegable de suggeriments de nom d'usuari" #. TRANSLATORS: dropdown username suggestions -#: src/components/users/FirstUserForm.jsx:72 +#: src/components/users/FirstUserForm.jsx:73 msgid "Use suggested username" msgstr "Usa el nom d'usuari suggerit" -#: src/components/users/FirstUserForm.jsx:151 +#: src/components/users/FirstUserForm.jsx:144 msgid "All fields are required" msgstr "Tots els camps són obligatoris." -#: src/components/users/FirstUserForm.jsx:158 -msgid "Please, try again." -msgstr "Si us plau, torneu-ho a provar." - -#: src/components/users/FirstUserForm.jsx:211 +#: src/components/users/FirstUserForm.jsx:201 msgid "Create user" msgstr "Crea un usuari" -#: src/components/users/FirstUserForm.jsx:211 +#: src/components/users/FirstUserForm.jsx:201 msgid "Edit user" msgstr "Edita l'usuari" -#: src/components/users/FirstUserForm.jsx:231 -#: src/components/users/FirstUserForm.jsx:233 +#: src/components/users/FirstUserForm.jsx:221 +#: src/components/users/FirstUserForm.jsx:223 msgid "User full name" msgstr "Nom complet de l'usuari" -#: src/components/users/FirstUserForm.jsx:271 +#: src/components/users/FirstUserForm.jsx:261 msgid "Edit password too" msgstr "Edita també la contrasenya" -#: src/components/users/FirstUserForm.jsx:287 +#: src/components/users/FirstUserForm.jsx:277 msgid "user autologin" msgstr "entrada de sessió automàtica de l'usuari" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:291 +#: src/components/users/FirstUserForm.jsx:281 msgid "Auto-login" msgstr "Entrada automàtica" -#: src/components/users/RootAuthMethods.jsx:35 +#: src/components/users/RootAuthMethods.jsx:36 msgid "No root authentication method defined yet." msgstr "Encara no s'ha definit cap mètode d'autenticació d'arrel." -#: src/components/users/RootAuthMethods.jsx:39 +#: src/components/users/RootAuthMethods.jsx:40 msgid "" "Please, define at least one authentication method for logging into the " "system as root." @@ -2467,54 +2386,54 @@ msgstr "" "Si us plau, definiu almenys un mètode d'autenticació per iniciar sessió al " "sistema com a arrel." -#: src/components/users/RootAuthMethods.jsx:46 +#: src/components/users/RootAuthMethods.jsx:47 msgid "Set a password" msgstr "Establiu una contrasenya" -#: src/components/users/RootAuthMethods.jsx:50 +#: src/components/users/RootAuthMethods.jsx:51 msgid "Upload a SSH Public Key" msgstr "Carrega una clau pública SSH" -#: src/components/users/RootAuthMethods.jsx:100 -#: src/components/users/RootAuthMethods.jsx:112 +#: src/components/users/RootAuthMethods.jsx:74 +#: src/components/users/RootAuthMethods.jsx:86 msgid "Set" msgstr "Estableix" -#: src/components/users/RootAuthMethods.jsx:132 +#: src/components/users/RootAuthMethods.jsx:97 msgid "Already set" msgstr "Ja s'ha establert" -#: src/components/users/RootAuthMethods.jsx:132 -#: src/components/users/RootAuthMethods.jsx:136 +#: src/components/users/RootAuthMethods.jsx:97 +#: src/components/users/RootAuthMethods.jsx:101 msgid "Not set" msgstr "No s'ha establert" #. TRANSLATORS: table header, user authentication method -#: src/components/users/RootAuthMethods.jsx:157 +#: src/components/users/RootAuthMethods.jsx:122 msgid "Method" msgstr "Mètode" -#: src/components/users/RootAuthMethods.jsx:174 +#: src/components/users/RootAuthMethods.jsx:139 msgid "SSH Key" msgstr "Clau SSH" -#: src/components/users/RootAuthMethods.jsx:193 +#: src/components/users/RootAuthMethods.jsx:158 msgid "Change the root password" msgstr "Canvia la contrasenya d'arrel" -#: src/components/users/RootAuthMethods.jsx:193 +#: src/components/users/RootAuthMethods.jsx:158 msgid "Set a root password" msgstr "Establiu una contrasenya d'arrel" -#: src/components/users/RootAuthMethods.jsx:203 +#: src/components/users/RootAuthMethods.jsx:168 msgid "Edit the SSH Public Key for root" msgstr "Edita la clau pública SSH per a l'arrel" -#: src/components/users/RootAuthMethods.jsx:204 +#: src/components/users/RootAuthMethods.jsx:169 msgid "Add a SSH Public Key for root" msgstr "Afegiu una clau pública SSH per a l'arrel" -#: src/components/users/RootPasswordPopup.jsx:43 +#: src/components/users/RootPasswordPopup.jsx:44 msgid "Root password" msgstr "Contrasenya d'arrel" @@ -2548,6 +2467,72 @@ msgstr "Usuari primer" msgid "Root authentication" msgstr "Autenticació d'arrel" +#, c-format +#~ msgid "Register %s" +#~ msgstr "Registra %s" + +#~ msgid "Registration code" +#~ msgstr "Codi de registre" + +#~ msgid "Email" +#~ msgstr "Adreça electrònica" + +#~ msgid "The installation will take" +#~ msgstr "La instal·lació necessitarà" + +#, c-format +#~ msgid "The installation will take %s including:" +#~ msgstr "La instal·lació necessitarà %s, incloent-hi el següent:" + +#~ msgid "No additional software was selected." +#~ msgstr "No s'ha seleccionat cap programari addicional." + +#~ msgid "The following software patterns are selected for installation:" +#~ msgstr "" +#~ "S'han seleccionat els patrons de programari següents per a la " +#~ "instal·lació:" + +#~ msgid "Selected patterns" +#~ msgstr "Patrons seleccionats" + +#~ msgid "Change selection" +#~ msgstr "Canvia la selecció" + +#~ msgid "" +#~ "This product does not allow to select software patterns during " +#~ "installation. However, you can add additional software once the " +#~ "installation is finished." +#~ msgstr "" +#~ "Aquest producte no permet seleccionar patrons de programari durant la " +#~ "instal·lació. Tanmateix, hi podeu afegir programari addicional un cop " +#~ "acabada la instal·lació." + +#~ msgid "auto selected" +#~ msgstr "seleccionat automàticament" + +#~ msgid "None of the patterns match the filter." +#~ msgstr "Cap dels patrons coincideix amb el filtre." + +#~ msgid "Software selection" +#~ msgstr "Selecció de programari" + +#~ msgid "Filter by pattern title or description" +#~ msgstr "Filtra per títol o descripció del patró" + +#, c-format +#~ msgid "Installation will take %s." +#~ msgstr "La instal·lació necessitarà %s." + +#~ msgid "" +#~ "This space includes the base system and the selected software patterns, " +#~ "if any." +#~ msgstr "" +#~ "Aquest espai inclou el sistema de base i els patrons de programari " +#~ "seleccionats, si n'hi ha." + +#~ msgid "Please, try again." +#~ msgstr "Si us plau, torneu-ho a provar." + #~ msgid "Reading file..." #~ msgstr "Llegint el fitxer..." diff --git a/web/po/cs.po b/web/po/cs.po index 836ba34bab..b89e698f18 100644 --- a/web/po/cs.po +++ b/web/po/cs.po @@ -8,11 +8,11 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-18 02:24+0000\n" -"PO-Revision-Date: 2024-07-20 17:47+0000\n" +"POT-Creation-Date: 2024-07-28 02:29+0000\n" +"PO-Revision-Date: 2024-07-25 08:46+0000\n" "Last-Translator: Aleš Kastner \n" -"Language-Team: Czech " -"\n" +"Language-Team: Czech \n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -56,7 +56,6 @@ msgid "For more information, please visit the project's repository at %s." msgstr "Další informace najdete v úložišti projektu na %s." #: src/components/core/About.jsx:92 src/components/core/LogsButton.jsx:126 -#: src/components/software/SoftwarePatternsSelection.jsx:268 msgid "Close" msgstr "Zavřít" @@ -84,7 +83,7 @@ msgstr "Pokračovat" #. TRANSLATORS: button label #: src/components/core/InstallButton.jsx:49 src/components/core/Page.jsx:90 #: src/components/core/Popup.jsx:132 -#: src/components/network/WifiConnectionForm.jsx:134 +#: src/components/network/WifiConnectionForm.jsx:141 msgid "Cancel" msgstr "Zrušit/Storno" @@ -179,14 +178,13 @@ msgstr "U instalace na dálku nelze změnit" #: src/components/core/InstallerOptions.jsx:142 #: src/components/network/IpSettingsForm.jsx:228 -#: src/components/product/ProductRegistrationPage.jsx:85 #: src/components/storage/BootSelection.jsx:250 #: src/components/storage/DeviceSelection.jsx:254 #: src/components/storage/EncryptionSettingsDialog.jsx:155 #: src/components/storage/SpacePolicySelection.jsx:200 #: src/components/storage/VolumeDialog.jsx:794 #: src/components/storage/ZFCPPage.jsx:528 -#: src/components/users/FirstUserForm.jsx:303 +#: src/components/users/FirstUserForm.jsx:293 msgid "Accept" msgstr "Přijmout" @@ -261,11 +259,13 @@ msgstr "Stahování záznamů se nezdařilo. Zkuste to znovu." msgid "Passwords do not match" msgstr "Hesla se neshodují" +#. TRANSLATORS: field label #: src/components/core/PasswordAndConfirmationInput.jsx:79 -#: src/components/network/WifiConnectionForm.jsx:121 +#: src/components/network/WifiConnectionForm.jsx:128 +#: src/components/questions/QuestionWithPassword.jsx:48 #: src/components/storage/iscsi/AuthFields.jsx:90 #: src/components/storage/iscsi/AuthFields.jsx:94 -#: src/components/users/RootAuthMethods.jsx:165 +#: src/components/users/RootAuthMethods.jsx:130 msgid "Password" msgstr "Heslo" @@ -343,51 +343,51 @@ msgstr "Výběr klávesnice" #: src/components/l10n/TimezoneSelection.jsx:125 #: src/components/product/ProductSelectionPage.jsx:90 msgid "Select" -msgstr "" +msgstr "Zvolit" #: src/components/l10n/L10nPage.jsx:53 #: src/components/overview/L10nSection.jsx:37 src/routes/l10n.js:38 msgid "Localization" -msgstr "" +msgstr "Lokalizace" #: src/components/l10n/L10nPage.jsx:61 src/components/l10n/L10nPage.jsx:70 #: src/components/l10n/L10nPage.jsx:80 msgid "Not selected yet" -msgstr "" +msgstr "Dosud nevybráno" #: src/components/l10n/L10nPage.jsx:64 src/components/l10n/L10nPage.jsx:72 #: src/components/l10n/L10nPage.jsx:83 -#: src/components/network/NetworkPage.jsx:112 +#: src/components/network/NetworkPage.jsx:74 #: src/components/storage/InstallationDeviceField.jsx:108 #: src/components/storage/ProposalActionsSummary.jsx:238 -#: src/components/users/RootAuthMethods.jsx:100 -#: src/components/users/RootAuthMethods.jsx:112 +#: src/components/users/RootAuthMethods.jsx:74 +#: src/components/users/RootAuthMethods.jsx:86 msgid "Change" -msgstr "" +msgstr "Změnit" #: src/components/l10n/L10nPage.jsx:70 msgid "Keyboard" -msgstr "" +msgstr "Klávesnice" #: src/components/l10n/L10nPage.jsx:79 msgid "Time zone" -msgstr "" +msgstr "Časové pásmo" #: src/components/l10n/LocaleSelection.jsx:39 msgid "Filter by language, territory or locale code" -msgstr "" +msgstr "Filtrování podle jazyka, území nebo kódu lokality" #: src/components/l10n/LocaleSelection.jsx:72 msgid "None of the locales match the filter." -msgstr "" +msgstr "Žádné umístění neodpovídá filtru." #: src/components/l10n/LocaleSelection.jsx:78 msgid "Locale selection" -msgstr "" +msgstr "Výběr lokality" #: src/components/l10n/TimezoneSelection.jsx:64 msgid "Filter by territory, time zone code or UTC offset" -msgstr "" +msgstr "Filtrování podle území, kódu časového pásma nebo posunu od UTC" #: src/components/l10n/TimezoneSelection.jsx:101 msgid "None of the time zones match the filter." @@ -440,7 +440,7 @@ msgstr "Seznam údajů o adresách" #. TRANSLATORS: input field for the iSCSI initiator name #. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:64 -#: src/components/network/ConnectionsTable.jsx:92 +#: src/components/network/ConnectionsTable.jsx:93 #: src/components/storage/ZFCPPage.jsx:381 #: src/components/storage/iscsi/InitiatorForm.jsx:52 #: src/components/storage/iscsi/InitiatorPresenter.jsx:68 @@ -452,242 +452,248 @@ msgstr "Název" #. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:66 -#: src/components/network/ConnectionsTable.jsx:93 +#: src/components/network/ConnectionsTable.jsx:94 msgid "IP addresses" msgstr "IP adresy" -#: src/components/network/ConnectionsTable.jsx:74 -#: src/components/network/WifiNetworksListPage.jsx:107 -#: src/components/network/WifiNetworksListPage.jsx:130 +#. TRANSLATORS: table header aria label +#: src/components/network/ConnectionsTable.jsx:68 +msgid "Connection actions" +msgstr "Akce připojení" + +#: src/components/network/ConnectionsTable.jsx:75 +#: src/components/network/WifiNetworksListPage.jsx:112 +#: src/components/network/WifiNetworksListPage.jsx:135 #: src/components/storage/PartitionsField.jsx:347 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 -#: src/components/users/FirstUser.jsx:120 +#: src/components/users/FirstUser.jsx:85 msgid "Edit" msgstr "Upravit" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:77 +#: src/components/network/ConnectionsTable.jsx:78 #: src/components/network/IpSettingsForm.jsx:151 #, c-format msgid "Edit connection %s" -msgstr "" +msgstr "Upravit připojení %s" -#: src/components/network/ConnectionsTable.jsx:81 -#: src/components/network/WifiNetworksListPage.jsx:109 -#: src/components/network/WifiNetworksListPage.jsx:137 +#: src/components/network/ConnectionsTable.jsx:82 +#: src/components/network/WifiNetworksListPage.jsx:114 +#: src/components/network/WifiNetworksListPage.jsx:142 msgid "Forget" -msgstr "" +msgstr "Zapomenout" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:83 +#: src/components/network/ConnectionsTable.jsx:84 #, c-format msgid "Forget connection %s" -msgstr "" +msgstr "Zapomenout připojení %s" #. TRANSLATORS: %s is replaced by a network connection name -#: src/components/network/ConnectionsTable.jsx:98 +#: src/components/network/ConnectionsTable.jsx:99 #, c-format msgid "Actions for connection %s" -msgstr "" +msgstr "Akce pro připojení %s" #. TRANSLATORS: input field name #: src/components/network/DnsDataList.jsx:81 #: src/components/network/DnsDataList.jsx:82 msgid "Server IP" -msgstr "" +msgstr "IP serveru" #: src/components/network/DnsDataList.jsx:104 msgid "Add DNS" -msgstr "" +msgstr "Přidat DNS" #. TRANSLATORS: button label #: src/components/network/DnsDataList.jsx:104 msgid "Add another DNS" -msgstr "" +msgstr "Přidat další DNS" #: src/components/network/DnsDataList.jsx:109 msgid "DNS" -msgstr "" +msgstr "DNS" #. TRANSLATORS: input field name #: src/components/network/IpPrefixInput.jsx:33 msgid "IP prefix or netmask" -msgstr "" +msgstr "Předpona IP nebo maska sítě" #. TRANSLATORS: error message #: src/components/network/IpSettingsForm.jsx:104 msgid "At least one address must be provided for selected mode" -msgstr "" +msgstr "Pro zvolený režim musí být uvedena alespoň jedna adresa" #. TRANSLATORS: network connection mode (automatic via DHCP or manual with static IP) #: src/components/network/IpSettingsForm.jsx:160 #: src/components/network/IpSettingsForm.jsx:165 #: src/components/network/IpSettingsForm.jsx:167 msgid "Mode" -msgstr "" +msgstr "Režim" #: src/components/network/IpSettingsForm.jsx:174 msgid "Automatic (DHCP)" -msgstr "" +msgstr "Automatický (DHCP)" #. TRANSLATORS: manual network configuration mode with a static IP address #: src/components/network/IpSettingsForm.jsx:177 #: src/components/storage/iscsi/NodeStartupOptions.js:25 msgid "Manual" -msgstr "" +msgstr "Manuální" #. TRANSLATORS: network gateway configuration #: src/components/network/IpSettingsForm.jsx:185 #: src/components/network/IpSettingsForm.jsx:188 msgid "Gateway" -msgstr "" +msgstr "Brána" #: src/components/network/IpSettingsForm.jsx:196 msgid "Gateway can be defined only in 'Manual' mode" -msgstr "" +msgstr "Bránu lze definovat pouze v režimu 'Manual'" -#: src/components/network/NetworkPage.jsx:93 +#: src/components/network/NetworkPage.jsx:55 msgid "No Wi-Fi supported" -msgstr "" +msgstr "Wi-Fi není podporováno" -#: src/components/network/NetworkPage.jsx:95 +#: src/components/network/NetworkPage.jsx:57 msgid "" "The system does not support Wi-Fi connections, probably because of missing " "or disabled hardware." msgstr "" +"Systém nepodporuje připojení Wi-Fi, pravděpodobně chybí hardware nebo je " +"zakázán." -#: src/components/network/NetworkPage.jsx:109 +#: src/components/network/NetworkPage.jsx:71 msgid "Wi-Fi" -msgstr "" +msgstr "Wi-Fi" #. TRANSLATORS: button label, connect to a WiFi network -#: src/components/network/NetworkPage.jsx:112 -#: src/components/network/WifiConnectionForm.jsx:130 -#: src/components/network/WifiNetworksListPage.jsx:105 +#: src/components/network/NetworkPage.jsx:74 +#: src/components/network/WifiConnectionForm.jsx:137 +#: src/components/network/WifiNetworksListPage.jsx:110 msgid "Connect" -msgstr "" +msgstr "Připojit" -#: src/components/network/NetworkPage.jsx:119 +#: src/components/network/NetworkPage.jsx:81 #, c-format msgid "Conected to %s" -msgstr "" +msgstr "Připojit k %s" -#: src/components/network/NetworkPage.jsx:126 +#: src/components/network/NetworkPage.jsx:88 msgid "No connected yet" -msgstr "" +msgstr "Dosud nepřipojeno" -#: src/components/network/NetworkPage.jsx:127 +#: src/components/network/NetworkPage.jsx:89 msgid "" "The system has not been configured for connecting to a Wi-Fi network yet." -msgstr "" +msgstr "Systém zatím nebyl konfigurován pro připojení k síti Wi-Fi." -#: src/components/network/NetworkPage.jsx:156 +#: src/components/network/NetworkPage.jsx:102 msgid "Wired" -msgstr "" +msgstr "Připojení kabelem (Ethernet)" -#: src/components/network/NetworkPage.jsx:160 +#: src/components/network/NetworkPage.jsx:104 msgid "No wired connections found" -msgstr "" +msgstr "Nebyla nalezena žádná kabelová připojení" -#: src/components/network/NetworkPage.jsx:173 -#: src/components/network/routes.js:59 +#: src/components/network/NetworkPage.jsx:114 +#: src/components/network/routes.js:33 msgid "Network" -msgstr "" +msgstr "Síť" #. TRANSLATORS: WiFi authentication mode -#: src/components/network/WifiConnectionForm.jsx:43 +#: src/components/network/WifiConnectionForm.jsx:45 #: src/components/storage/iscsi/InitiatorPresenter.jsx:72 msgid "None" -msgstr "" +msgstr "Žádná" #. TRANSLATORS: WiFi authentication mode -#: src/components/network/WifiConnectionForm.jsx:45 +#: src/components/network/WifiConnectionForm.jsx:47 msgid "WPA & WPA2 Personal" -msgstr "" +msgstr "WPA & WPA2 Osobní" -#: src/components/network/WifiConnectionForm.jsx:85 -#: src/components/product/ProductRegistrationPage.jsx:68 +#: src/components/network/WifiConnectionForm.jsx:92 #: src/components/storage/ZFCPDiskForm.jsx:105 #: src/components/storage/iscsi/DiscoverForm.jsx:98 #: src/components/storage/iscsi/LoginForm.jsx:69 -#: src/components/users/FirstUserForm.jsx:217 +#: src/components/users/FirstUserForm.jsx:207 msgid "Something went wrong" -msgstr "" +msgstr "Něco se nezdařilo" -#: src/components/network/WifiConnectionForm.jsx:86 +#: src/components/network/WifiConnectionForm.jsx:93 msgid "Please, review provided settings and try again." -msgstr "" +msgstr "Zkontrolujte poskytnutá nastavení a zkuste to znovu." #. TRANSLATORS: SSID (Wifi network name) configuration -#: src/components/network/WifiConnectionForm.jsx:92 -#: src/components/network/WifiConnectionForm.jsx:96 +#: src/components/network/WifiConnectionForm.jsx:99 +#: src/components/network/WifiConnectionForm.jsx:103 msgid "SSID" -msgstr "" +msgstr "SSID" #. TRANSLATORS: Wifi security configuration (password protected or not) -#: src/components/network/WifiConnectionForm.jsx:105 -#: src/components/network/WifiConnectionForm.jsx:108 +#: src/components/network/WifiConnectionForm.jsx:112 +#: src/components/network/WifiConnectionForm.jsx:115 msgid "Security" -msgstr "" +msgstr "Zabezpečení" #. TRANSLATORS: WiFi password -#: src/components/network/WifiConnectionForm.jsx:117 +#: src/components/network/WifiConnectionForm.jsx:124 msgid "WPA Password" -msgstr "" +msgstr "Heslo k WPA" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:63 -#: src/components/network/WifiNetworksListPage.jsx:117 +#: src/components/network/WifiNetworksListPage.jsx:64 +#: src/components/network/WifiNetworksListPage.jsx:122 msgid "Connecting" -msgstr "" +msgstr "Připojuji" #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:66 -#: src/components/network/WifiNetworksListPage.jsx:121 -#: src/components/network/WifiNetworksListPage.jsx:164 +#: src/components/network/WifiNetworksListPage.jsx:67 +#: src/components/network/WifiNetworksListPage.jsx:126 +#: src/components/network/WifiNetworksListPage.jsx:169 msgid "Connected" -msgstr "" +msgstr "Připojeno" #. TRANSLATORS: iSCSI connection status #. TRANSLATORS: Wifi network status -#: src/components/network/WifiNetworksListPage.jsx:71 -#: src/components/network/WifiNetworksListPage.jsx:119 +#: src/components/network/WifiNetworksListPage.jsx:72 +#: src/components/network/WifiNetworksListPage.jsx:124 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" -msgstr "" +msgstr "Odpojeno" -#: src/components/network/WifiNetworksListPage.jsx:127 +#: src/components/network/WifiNetworksListPage.jsx:132 msgid "Disconnect" -msgstr "" +msgstr "Odpojit" -#: src/components/network/WifiNetworksListPage.jsx:150 +#: src/components/network/WifiNetworksListPage.jsx:155 msgid "Connect to a hidden network" -msgstr "" +msgstr "Připojení ke skryté síti" -#: src/components/network/WifiNetworksListPage.jsx:161 +#: src/components/network/WifiNetworksListPage.jsx:166 msgid "configured" -msgstr "" +msgstr "konfigurováno" -#: src/components/network/WifiNetworksListPage.jsx:265 +#: src/components/network/WifiNetworksListPage.jsx:268 msgid "Connect to hidden network" -msgstr "" +msgstr "Připojit ke skryté síti" -#: src/components/network/WifiSelectorPage.jsx:136 +#: src/components/network/WifiSelectorPage.jsx:40 msgid "Connect to a Wi-Fi network" -msgstr "" +msgstr "Připojení k síti Wi-Fi" #. TRANSLATORS: %s will be replaced by a language name and territory, example: #. "English (United States)". #: src/components/overview/L10nSection.jsx:33 #, c-format msgid "The system will use %s as its default language." -msgstr "" +msgstr "Systém použije jako výchozí jazyk %s." #: src/components/overview/OverviewPage.jsx:49 #: src/components/users/UsersPage.jsx:36 src/components/users/routes.js:32 msgid "Users" -msgstr "" +msgstr "Uživatelé" #: src/components/overview/OverviewPage.jsx:50 #: src/components/overview/StorageSection.jsx:111 @@ -695,72 +701,70 @@ msgstr "" #: src/components/storage/StoragePage.jsx:30 #: src/components/storage/routes.js:58 msgid "Storage" -msgstr "" +msgstr "Paměť" #: src/components/overview/OverviewPage.jsx:51 -#: src/components/overview/SoftwareSection.jsx:86 -#: src/components/software/SoftwarePage.jsx:181 #: src/components/software/routes.js:32 msgid "Software" -msgstr "" +msgstr "Software" #: src/components/overview/OverviewPage.jsx:56 msgid "Ready for installation" -msgstr "" +msgstr "Připraveno k instalaci" #: src/components/overview/OverviewPage.jsx:102 msgid "Installation" -msgstr "" +msgstr "Instalace" #: src/components/overview/OverviewPage.jsx:103 msgid "Before installing, please check the following problems." -msgstr "" +msgstr "Před instalací zkontrolujte tyto problémy." #: src/components/overview/OverviewPage.jsx:114 msgid "" "Take your time to check your configuration before starting the installation " "process." -msgstr "" +msgstr "Před zahájením instalace zkontrolujte konfiguraci." #: src/components/overview/OverviewPage.jsx:123 msgid "" "These are the most relevant installation settings. Feel free to browse the " "sections in the menu for further details." msgstr "" - -#: src/components/overview/SoftwareSection.jsx:60 -msgid "The installation will take" -msgstr "" - -#. TRANSLATORS: %s will be replaced with the installation size, example: "5GiB". -#: src/components/overview/SoftwareSection.jsx:67 -#, c-format -msgid "The installation will take %s including:" -msgstr "" +"Toto je nejdůležitější nastavení instalace. Další podrobnosti najdete v " +"sekcích v nabídce." #: src/components/overview/StorageSection.jsx:53 msgid "" "Install in a new Logical Volume Manager (LVM) volume group shrinking " "existing partitions at the underlying devices as needed" msgstr "" +"Instalace do nové skupiny svazků LVM (Logical Volume Manager), která podle " +"potřeby zmenší existující oddíly na základních zařízeních" #: src/components/overview/StorageSection.jsx:58 msgid "" "Install in a new Logical Volume Manager (LVM) volume group without modifying " "the partitions at the underlying devices" msgstr "" +"Instalace do nové skupiny svazků Správce logických svazků (LVM) bez úpravy " +"oddílů v základních zařízeních" #: src/components/overview/StorageSection.jsx:63 msgid "" "Install in a new Logical Volume Manager (LVM) volume group deleting all the " "content of the underlying devices" msgstr "" +"Instalace do nové skupiny svazků LVM (Logical Volume Manager), která " +"odstraní veškerý obsah základních zařízení" #: src/components/overview/StorageSection.jsx:68 msgid "" "Install in a new Logical Volume Manager (LVM) volume group using a custom " "strategy to find the needed space at the underlying devices" msgstr "" +"Instalace do nové skupiny svazků LVM (Logical Volume Manager) pomocí vlastní " +"strategie pro nalezení potřebného místa v základních zařízeních" #: src/components/overview/StorageSection.jsx:86 #, c-format @@ -768,6 +772,8 @@ msgid "" "Install in a new Logical Volume Manager (LVM) volume group on %s shrinking " "existing partitions as needed" msgstr "" +"Instalace do nové skupiny svazků LVM (Logical Volume Manager) na %s se " +"zmenšením stávajících oddílů podle potřeby" #: src/components/overview/StorageSection.jsx:92 #, c-format @@ -775,6 +781,8 @@ msgid "" "Install in a new Logical Volume Manager (LVM) volume group on %s without " "modifying existing partitions" msgstr "" +"Instalace do nové skupiny svazků LVM (Logical Volume Manager) na %s bez " +"úpravy existujících oddílů" #: src/components/overview/StorageSection.jsx:98 #, c-format @@ -782,6 +790,8 @@ msgid "" "Install in a new Logical Volume Manager (LVM) volume group on %s deleting " "all its content" msgstr "" +"Instalace do nové skupiny svazků LVM (Logical Volume Manager) na %s s " +"odstraněním veškerého jejich obsahu" #: src/components/overview/StorageSection.jsx:104 #, c-format @@ -789,11 +799,13 @@ msgid "" "Install in a new Logical Volume Manager (LVM) volume group on %s using a " "custom strategy to find the needed space" msgstr "" +"Instalace do nové skupiny svazků LVM (Logical Volume Manager) na %s s " +"použitím vlastní strategie pro nalezení potřebného místa" #: src/components/overview/StorageSection.jsx:179 #: src/components/storage/InstallationDeviceField.jsx:66 msgid "No device selected yet" -msgstr "" +msgstr "Zatím nebylo vybráno žádné zařízení" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" @@ -801,20 +813,21 @@ msgstr "" #, c-format msgid "Install using device %s shrinking existing partitions as needed" msgstr "" +"Instalace pomocí zařízení %s se zmenšením stávajících oddílů podle potřeby" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" #: src/components/overview/StorageSection.jsx:190 #, c-format msgid "Install using device %s without modifying existing partitions" -msgstr "" +msgstr "Instalace pomocí zařízení %s bez úpravy stávajících oddílů" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" #: src/components/overview/StorageSection.jsx:194 #, c-format msgid "Install using device %s and deleting all its content" -msgstr "" +msgstr "Instalace pomocí zařízení %s a odstranění veškerého jeho obsahu" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" @@ -822,202 +835,145 @@ msgstr "" #, c-format msgid "Install using device %s with a custom strategy to find the needed space" msgstr "" +"Instalace pomocí zařízení %s s vlastní strategií pro vyhledání potřebného " +"místa" #: src/components/overview/routes.js:30 msgid "Overview" -msgstr "" - -#: src/components/product/ProductRegistrationPage.jsx:66 -#, c-format -msgid "Register %s" -msgstr "" - -#: src/components/product/ProductRegistrationPage.jsx:73 -msgid "Registration code" -msgstr "" - -#: src/components/product/ProductRegistrationPage.jsx:76 -msgid "Email" -msgstr "" +msgstr "Přehled" #: src/components/product/ProductSelectionProgress.jsx:49 msgid "Configuring the product, please wait ..." -msgstr "" +msgstr "Konfigurace produktu, počkejte prosím..." #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" -msgstr "" +msgstr "Dotaz" #. TRANSLATORS: error message, user entered a wrong password #: src/components/questions/LuksActivationQuestion.jsx:34 msgid "Given encryption password didn't work" -msgstr "" +msgstr "Zadané šifrovací heslo nefungovalo" #: src/components/questions/LuksActivationQuestion.jsx:59 msgid "Encrypted Device" -msgstr "" +msgstr "Šifrované zařízení" #. TRANSLATORS: field label #: src/components/questions/LuksActivationQuestion.jsx:67 msgid "Encryption Password" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:85 -msgid "No additional software was selected." -msgstr "" - -#: src/components/software/SoftwarePage.jsx:90 -msgid "The following software patterns are selected for installation:" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:105 -#: src/components/software/SoftwarePage.jsx:119 -msgid "Selected patterns" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:108 -msgid "Change selection" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:123 -msgid "" -"This product does not allow to select software patterns during installation. " -"However, you can add additional software once the installation is finished." -msgstr "" - -#: src/components/software/SoftwarePatternsSelection.jsx:223 -msgid "auto selected" -msgstr "" - -#: src/components/software/SoftwarePatternsSelection.jsx:241 -msgid "None of the patterns match the filter." -msgstr "" - -#: src/components/software/SoftwarePatternsSelection.jsx:248 -msgid "Software selection" -msgstr "" - -#. TRANSLATORS: search field placeholder text -#: src/components/software/SoftwarePatternsSelection.jsx:251 -#: src/components/software/SoftwarePatternsSelection.jsx:252 -msgid "Filter by pattern title or description" -msgstr "" - -#. TRANSLATORS: %s will be replaced by the estimated installation size, -#. example: "728.8 MiB" -#: src/components/software/UsedSize.jsx:33 -#, c-format -msgid "Installation will take %s." -msgstr "" +msgstr "Heslo pro šifrování" -#: src/components/software/UsedSize.jsx:37 -msgid "" -"This space includes the base system and the selected software patterns, if " -"any." -msgstr "" +#: src/components/questions/QuestionWithPassword.jsx:42 +msgid "Password Required" +msgstr "Vyžadováno heslo" #: src/components/storage/BootConfigField.jsx:43 msgid "Change boot options" -msgstr "" +msgstr "Změna možností spouštění systému" #: src/components/storage/BootConfigField.jsx:81 msgid "Installation will not configure partitions for booting." -msgstr "" +msgstr "Instalace nenakonfiguruje oddíly pro zavádění systému." #: src/components/storage/BootConfigField.jsx:85 msgid "" "Installation will configure partitions for booting at the installation disk." -msgstr "" +msgstr "Instalace nakonfiguruje oddíly pro zavádění na instalačním disku." #: src/components/storage/BootConfigField.jsx:89 #, c-format msgid "Installation will configure partitions for booting at %s." -msgstr "" +msgstr "Instalace nakonfiguruje oddíly pro zavádění v %s." #: src/components/storage/BootSelection.jsx:132 msgid "" "To ensure the new system is able to boot, the installer may need to create " "or configure some partitions in the appropriate disk." msgstr "" +"Aby bylo možné nový systém spustit, může být nutné, aby instalační program " +"vytvořil nebo nakonfiguroval některé oddíly na příslušném disku." #: src/components/storage/BootSelection.jsx:138 msgid "Partitions to boot will be allocated at the installation disk." -msgstr "" +msgstr "Oddíly pro zavádění budou přiděleny na instalačním disku." #. TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB") #: src/components/storage/BootSelection.jsx:143 #, c-format msgid "Partitions to boot will be allocated at the installation disk (%s)." -msgstr "" +msgstr "Oddíly pro zavádění budou přiděleny na instalačním disku (%s)." #: src/components/storage/BootSelection.jsx:159 msgid "Select booting partition" -msgstr "" +msgstr "Výběr zaváděcího oddílu" #: src/components/storage/BootSelection.jsx:180 #: src/components/storage/iscsi/NodeStartupOptions.js:27 msgid "Automatic" -msgstr "" +msgstr "Automatický" #: src/components/storage/BootSelection.jsx:198 msgid "Select a disk" -msgstr "" +msgstr "Výběr disku" #: src/components/storage/BootSelection.jsx:204 msgid "Partitions to boot will be allocated at the following device." -msgstr "" +msgstr "Oddíly pro zavádění budou přiděleny na tomto zařízení." #: src/components/storage/BootSelection.jsx:207 msgid "Choose a disk for placing the boot loader" -msgstr "" +msgstr "Výběr disku pro umístění zavaděče" #: src/components/storage/BootSelection.jsx:230 msgid "Do not configure" -msgstr "" +msgstr "Nekonfigurujte" #: src/components/storage/BootSelection.jsx:236 msgid "" "No partitions will be automatically configured for booting. Use with caution." msgstr "" +"Žádné oddíly nebudou automaticky konfigurovány pro zavádění systému. " +"Používejte opatrně." #: src/components/storage/DASDFormatProgress.jsx:60 msgid "Waiting for progress report" -msgstr "" +msgstr "Čekám na zprávu o postupu" #: src/components/storage/DASDFormatProgress.jsx:67 msgid "Formatting DASD devices" -msgstr "" +msgstr "Formátuji zařízení DASD" #: src/components/storage/DASDTable.jsx:62 #: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 #: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "No" -msgstr "" +msgstr "Ne" #: src/components/storage/DASDTable.jsx:62 #: src/components/storage/ZFCPPage.jsx:340 #: src/components/storage/iscsi/InitiatorPresenter.jsx:71 #: src/components/storage/iscsi/NodesPresenter.jsx:101 msgid "Yes" -msgstr "" +msgstr "Ano" #: src/components/storage/DASDTable.jsx:69 #: src/components/storage/ZFCPDiskForm.jsx:110 #: src/components/storage/ZFCPPage.jsx:324 #: src/components/storage/ZFCPPage.jsx:382 msgid "Channel ID" -msgstr "" +msgstr "ID kanálu" #. TRANSLATORS: table header #: src/components/storage/DASDTable.jsx:70 #: src/components/storage/ZFCPPage.jsx:325 #: src/components/storage/iscsi/NodesPresenter.jsx:102 #: src/components/storage/iscsi/NodesPresenter.jsx:123 -#: src/components/users/RootAuthMethods.jsx:159 +#: src/components/users/RootAuthMethods.jsx:124 msgid "Status" -msgstr "" +msgstr "Stav" #: src/components/storage/DASDTable.jsx:71 #: src/components/storage/DeviceSelectorTable.jsx:197 @@ -1025,81 +981,83 @@ msgstr "" #: src/components/storage/SpaceActionsTable.jsx:200 #: src/components/storage/VolumeLocationSelectorTable.jsx:107 msgid "Device" -msgstr "" +msgstr "Zařízení" #: src/components/storage/DASDTable.jsx:72 msgid "Type" -msgstr "" +msgstr "Typ" #. TRANSLATORS: table header, the column contains "Yes"/"No" values #. for the DIAG access mode (special disk access mode on IBM mainframes), #. usually keep untranslated #: src/components/storage/DASDTable.jsx:76 msgid "DIAG" -msgstr "" +msgstr "DIAG" #: src/components/storage/DASDTable.jsx:77 msgid "Formatted" -msgstr "" +msgstr "Formátován" #: src/components/storage/DASDTable.jsx:78 msgid "Partition Info" -msgstr "" +msgstr "Údaje o oddílech" #. TRANSLATORS: drop down menu label #: src/components/storage/DASDTable.jsx:115 msgid "Perform an action" -msgstr "" +msgstr "Provést akci" #: src/components/storage/DASDTable.jsx:122 #: src/components/storage/ZFCPPage.jsx:353 msgid "Activate" -msgstr "" +msgstr "Aktivace" #: src/components/storage/DASDTable.jsx:126 #: src/components/storage/ZFCPPage.jsx:395 msgid "Deactivate" -msgstr "" +msgstr "Deaktivace" #: src/components/storage/DASDTable.jsx:131 msgid "Set DIAG On" -msgstr "" +msgstr "Zapnout DIAG" #: src/components/storage/DASDTable.jsx:135 msgid "Set DIAG Off" -msgstr "" +msgstr "Vypnout DIAG" #: src/components/storage/DASDTable.jsx:140 msgid "Format" -msgstr "" +msgstr "Formát" #: src/components/storage/DASDTable.jsx:261 #: src/components/storage/DASDTable.jsx:262 msgid "Filter by min channel" -msgstr "" +msgstr "Filtrování podle min. kanálu" #: src/components/storage/DASDTable.jsx:269 msgid "Remove min channel filter" -msgstr "" +msgstr "Odstranění filtru min. kanálu" #: src/components/storage/DASDTable.jsx:283 #: src/components/storage/DASDTable.jsx:284 msgid "Filter by max channel" -msgstr "" +msgstr "Filtrování podle max. kanálu" #: src/components/storage/DASDTable.jsx:291 msgid "Remove max channel filter" -msgstr "" +msgstr "Odstranění filtru max. kanálu" #: src/components/storage/DeviceSelection.jsx:108 msgid "Loading data, please wait a second..." -msgstr "" +msgstr "Načítání dat chvíli trvá..." #: src/components/storage/DeviceSelection.jsx:144 msgid "" "The file systems will be allocated by default as [new partitions in the " "selected device]." msgstr "" +"Souborové systémy budou ve výchozím nastavení přiděleny jako [nové oddíly ve " +"vybraném zařízení]." #: src/components/storage/DeviceSelection.jsx:151 msgid "" @@ -1107,101 +1065,105 @@ msgid "" "LVM Volume Group]. The corresponding physical volumes will be created on " "demand as new partitions at the selected devices." msgstr "" +"Souborové systémy budou ve výchozím nastavení přiděleny jako [logické svazky " +"nové skupiny svazků LVM]. Odpovídající fyzické svazky budou na vyžádání " +"vytvořeny jako nové oddíly na vybraných zařízeních." #: src/components/storage/DeviceSelection.jsx:160 msgid "Select installation device" -msgstr "" +msgstr "Výběr instalačního zařízení" #: src/components/storage/DeviceSelection.jsx:166 msgid "Install new system on" -msgstr "" +msgstr "Instalace nového systému na" #: src/components/storage/DeviceSelection.jsx:169 msgid "An existing disk" -msgstr "" +msgstr "Existující disk" #: src/components/storage/DeviceSelection.jsx:178 msgid "A new LVM Volume Group" -msgstr "" +msgstr "Nová skupina svazků LVM" #: src/components/storage/DeviceSelection.jsx:203 msgid "Device selector for target disk" -msgstr "" +msgstr "Výběr zařízení pro cílový disk" #: src/components/storage/DeviceSelection.jsx:226 msgid "Device selector for new LVM volume group" -msgstr "" +msgstr "Výběr zařízení pro novou skupinu svazků LVM" #: src/components/storage/DeviceSelection.jsx:242 msgid "Prepare more devices by configuring advanced" -msgstr "" +msgstr "Připravte další zařízení pomocí pokročilé konfigurace" #: src/components/storage/DeviceSelection.jsx:243 +#, fuzzy msgid "storage techs" -msgstr "" +msgstr "technologie úložiště" #. TRANSLATORS: multipath device type #: src/components/storage/DeviceSelectorTable.jsx:61 msgid "Multipath" -msgstr "" +msgstr "Vícecestný" #. TRANSLATORS: %s is replaced by the device bus ID #: src/components/storage/DeviceSelectorTable.jsx:66 #, c-format msgid "DASD %s" -msgstr "" +msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 #: src/components/storage/DeviceSelectorTable.jsx:71 #, c-format msgid "Software %s" -msgstr "" +msgstr "Software %s" #: src/components/storage/DeviceSelectorTable.jsx:76 msgid "SD Card" -msgstr "" +msgstr "Karta SD" #. TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA" #: src/components/storage/DeviceSelectorTable.jsx:81 #, c-format msgid "%s disk" -msgstr "" +msgstr "%s disk" #: src/components/storage/DeviceSelectorTable.jsx:82 msgid "Disk" -msgstr "" +msgstr "Disk" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array #: src/components/storage/DeviceSelectorTable.jsx:102 #, c-format msgid "Members: %s" -msgstr "" +msgstr "Členové: %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array #: src/components/storage/DeviceSelectorTable.jsx:111 #, c-format msgid "Devices: %s" -msgstr "" +msgstr "Zařízení: %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device #: src/components/storage/DeviceSelectorTable.jsx:120 #, c-format msgid "Wires: %s" -msgstr "" +msgstr "Kabely: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions #: src/components/storage/DeviceSelectorTable.jsx:155 #, c-format msgid "%s with %d partitions" -msgstr "" +msgstr "%s s %d oddíly" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty #: src/components/storage/DeviceSelectorTable.jsx:161 #: src/components/storage/SpaceActionsTable.jsx:175 msgid "No content found" -msgstr "" +msgstr "Nebyl nalezen žádný obsah" #: src/components/storage/DeviceSelectorTable.jsx:198 #: src/components/storage/PartitionsField.jsx:487 @@ -1209,7 +1171,7 @@ msgstr "" #: src/components/storage/SpaceActionsTable.jsx:201 #: src/components/storage/VolumeLocationSelectorTable.jsx:108 msgid "Details" -msgstr "" +msgstr "Podrobnosti" #: src/components/storage/DeviceSelectorTable.jsx:199 #: src/components/storage/PartitionsField.jsx:488 @@ -1218,71 +1180,77 @@ msgstr "" #: src/components/storage/VolumeFields.jsx:488 #: src/components/storage/VolumeLocationSelectorTable.jsx:113 msgid "Size" -msgstr "" +msgstr "Velikost" #: src/components/storage/DevicesTechMenu.jsx:38 msgid "Manage and format" -msgstr "" +msgstr "Správa a formátování" #: src/components/storage/DevicesTechMenu.jsx:52 msgid "Activate disks" -msgstr "" +msgstr "Aktivace disků" #: src/components/storage/DevicesTechMenu.jsx:53 msgid "zFCP" -msgstr "" +msgstr "zFCP" #: src/components/storage/DevicesTechMenu.jsx:66 msgid "Connect to iSCSI targets" -msgstr "" +msgstr "Připojení k cílům iSCSI" #: src/components/storage/DevicesTechMenu.jsx:67 #: src/components/storage/routes.js:37 msgid "iSCSI" -msgstr "" +msgstr "iSCSI" #: src/components/storage/EncryptionField.jsx:38 #: src/components/storage/EncryptionSettingsDialog.jsx:36 msgid "Encryption" -msgstr "" +msgstr "Šifrování" #: src/components/storage/EncryptionField.jsx:40 msgid "" "Protection for the information stored at the device, including data, " "programs, and system files." msgstr "" +"Ochrana informací uložených v zařízení, včetně dat, programů a systémových " +"souborů." #: src/components/storage/EncryptionField.jsx:44 msgid "disabled" -msgstr "" +msgstr "odpojeno" #: src/components/storage/EncryptionField.jsx:45 msgid "enabled" -msgstr "" +msgstr "zapojeno" #: src/components/storage/EncryptionField.jsx:46 msgid "using TPM unlocking" -msgstr "" +msgstr "odemykání čipem TPM" #: src/components/storage/EncryptionField.jsx:60 msgid "Enable" -msgstr "" +msgstr "Zapojit" #: src/components/storage/EncryptionField.jsx:60 msgid "Modify" -msgstr "" +msgstr "Upravit" #: src/components/storage/EncryptionSettingsDialog.jsx:38 msgid "" "Full Disk Encryption (FDE) allows to protect the information stored at the " "device, including data, programs, and system files." msgstr "" +"Šifrování celého disku (FDE) umožňuje chránit informace uložené v zařízení, " +"včetně dat, programů a systémových souborů." #. TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation #: src/components/storage/EncryptionSettingsDialog.jsx:42 msgid "" "Use the Trusted Platform Module (TPM) to decrypt automatically on each boot" msgstr "" +"Použití modulu TPM (Trusted Platform Module) k automatickému dešifrování při " +"každém spuštění systému" #: src/components/storage/EncryptionSettingsDialog.jsx:46 msgid "" @@ -1290,98 +1258,101 @@ msgid "" "verify the integrity of the system. TPM sealing requires the new system to " "be booted directly on its first run." msgstr "" +"Dokáže-li čip TPM ověřit integritu systému, nebude heslo pro spuštění " +"systému a přístup k datům potřebné. Zapečetění TPM vyžaduje, aby byl nový " +"systém spuštěn hned při prvním použití." #: src/components/storage/EncryptionSettingsDialog.jsx:129 msgid "Encrypt the system" -msgstr "" +msgstr "Šifrování systému" #: src/components/storage/InstallationDeviceField.jsx:36 #: src/components/storage/VolumeLocationSelectorTable.jsx:61 msgid "Installation device" -msgstr "" +msgstr "Instalační zařízení" #. TRANSLATORS: The storage "Installation device" field's description. #: src/components/storage/InstallationDeviceField.jsx:38 msgid "Main disk or LVM Volume Group for installation." -msgstr "" +msgstr "Hlavní disk nebo skupina svazků LVM pro instalaci." #. TRANSLATORS: %s is the installation disk (eg. "/dev/sda, 80 GiB) #: src/components/storage/InstallationDeviceField.jsx:52 #, c-format msgid "File systems created as new partitions at %s" -msgstr "" +msgstr "Souborové systémy vytvořené jako nové oddíly v %s" #: src/components/storage/InstallationDeviceField.jsx:55 msgid "File systems created at a new LVM volume group" -msgstr "" +msgstr "Souborové systémy vytvořené v nové skupině svazků LVM" #: src/components/storage/InstallationDeviceField.jsx:60 #, c-format msgid "File systems created at a new LVM volume group on %s" -msgstr "" +msgstr "Souborové systémy vytvořené v nové skupině svazků LVM na %s" #. TRANSLATORS: minimum device size, %s is replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:84 #, c-format msgid "at least %s" -msgstr "" +msgstr "alespoň %s" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:108 #, c-format msgid "Transactional Btrfs root volume (%s)" -msgstr "" +msgstr "Transakční kořenový svazek Btrfs (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:110 #, c-format msgid "Transactional Btrfs root partition (%s)" -msgstr "" +msgstr "Transakční kořenový oddíl Btrfs (%s)" #. TRANSLATORS: "/" is in an LVM logical volume. %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:115 #, c-format msgid "Btrfs root volume with snapshots (%s)" -msgstr "" +msgstr "Kořenový svazek Btrfs se snímky (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "17.5 GiB" #: src/components/storage/PartitionsField.jsx:117 #, c-format msgid "Btrfs root partition with snapshots (%s)" -msgstr "" +msgstr "Kořenový oddíl Btrfs se snímky (%s)" #. TRANSLATORS: This results in something like "Mount /dev/sda3 at /home (25 GiB)" since #. %1$s is replaced by the device name, %2$s by the mount point and %3$s by the size #: src/components/storage/PartitionsField.jsx:126 #, c-format msgid "Mount %1$s at %2$s (%3$s)" -msgstr "" +msgstr "Připojit %1$s at %2$s (%3$s)" #. TRANSLATORS: This results in something like "Swap at /dev/sda3 (2 GiB)" since #. %1$s is replaced by the device name, and %2$s by the size #: src/components/storage/PartitionsField.jsx:132 #, c-format msgid "Swap at %1$s (%2$s)" -msgstr "" +msgstr "Přepnout na %1$s (%2$s)" #. TRANSLATORS: Swap is in an LVM logical volume. %s replaced by size string, e.g. "8 GiB" #: src/components/storage/PartitionsField.jsx:136 #, c-format msgid "Swap volume (%s)" -msgstr "" +msgstr "Přepnout svazek (%s)" #. TRANSLATORS: %s replaced by size string, e.g. "8 GiB" #: src/components/storage/PartitionsField.jsx:138 #, c-format msgid "Swap partition (%s)" -msgstr "" +msgstr "Přepnout oddíl (%s)" #. TRANSLATORS: This results in something like "Btrfs root at /dev/sda3 (20 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the device name, and %3$s by the size #: src/components/storage/PartitionsField.jsx:147 #, c-format msgid "%1$s root at %2$s (%3$s)" -msgstr "" +msgstr "%1$s kořen na %2$s (%3$s)" #. TRANSLATORS: "/" is in an LVM logical volume. #. Results in something like "Btrfs root volume (at least 20 GiB)" since @@ -1389,21 +1360,21 @@ msgstr "" #: src/components/storage/PartitionsField.jsx:153 #, c-format msgid "%1$s root volume (%2$s)" -msgstr "" +msgstr "%1$s kořenový svazek (%2$s)" #. TRANSLATORS: Results in something like "Btrfs root partition (at least 20 GiB)" since #. $1$s is replaced by filesystem type and %2$s by size description #: src/components/storage/PartitionsField.jsx:156 #, c-format msgid "%1$s root partition (%2$s)" -msgstr "" +msgstr "%1$s kořenový oddíl (%2$s)" #. TRANSLATORS: This results in something like "Ext4 /home at /dev/sda3 (20 GiB)" since #. %1$s is replaced by filesystem type, %2$s by mount point, %3$s by device name and %4$s by size #: src/components/storage/PartitionsField.jsx:162 #, c-format msgid "%1$s %2$s at %3$s (%4$s)" -msgstr "" +msgstr "%1$s %2$s na %3$s (%4$s)" #. TRANSLATORS: The filesystem is in an LVM logical volume. #. Results in something like "Ext4 /home volume (at least 10 GiB)" since @@ -1411,248 +1382,250 @@ msgstr "" #: src/components/storage/PartitionsField.jsx:168 #, c-format msgid "%1$s %2$s volume (%3$s)" -msgstr "" +msgstr "%1$s %2$s svazek (%3$s)" #. TRANSLATORS: This results in something like "Ext4 /home partition (at least 10 GiB)" since #. %1$s is replaced by the filesystem type, %2$s by the mount point and %3$s by the size description #: src/components/storage/PartitionsField.jsx:171 #, c-format msgid "%1$s %2$s partition (%3$s)" -msgstr "" +msgstr "%1$s %2$s oddíl (%3$s)" #: src/components/storage/PartitionsField.jsx:182 msgid "Do not configure partitions for booting" -msgstr "" +msgstr "Nekonfigurujte oddíly pro zavádění systému" #: src/components/storage/PartitionsField.jsx:184 msgid "Boot partitions at installation disk" -msgstr "" +msgstr "Oddíly zavádějící systém na instalačním disku" #. TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB) #: src/components/storage/PartitionsField.jsx:187 #, c-format msgid "Boot partitions at %s" -msgstr "" +msgstr "Zaváděcí oddíly na %s" #. TRANSLATORS: header for a list of items referring to size limits for file systems #: src/components/storage/PartitionsField.jsx:209 msgid "These limits are affected by:" -msgstr "" +msgstr "Tyto limity jsou ovlivněny (čím):" #. TRANSLATORS: list item, this affects the computed partition size limits #: src/components/storage/PartitionsField.jsx:213 msgid "The configuration of snapshots" -msgstr "" +msgstr "Konfigurace snímků" #: src/components/storage/PartitionsField.jsx:219 #, c-format msgid "Presence of other volumes (%s)" -msgstr "" +msgstr "Přítomnost dalších svazků (%s)" #. TRANSLATORS: list item, describes a factor that affects the computed size of a #. file system; eg. adjusting the size of the swap #: src/components/storage/PartitionsField.jsx:225 msgid "The amount of RAM in the system" -msgstr "" +msgstr "Množství paměti RAM v systému" #: src/components/storage/PartitionsField.jsx:292 msgid "auto" -msgstr "" +msgstr "auto" #. TRANSLATORS: %s will be replaced by a file-system type like "Btrfs" or "Ext4" #: src/components/storage/PartitionsField.jsx:309 #, c-format msgid "Reused %s" -msgstr "" +msgstr "Opětovné použití %s" #: src/components/storage/PartitionsField.jsx:310 msgid "Transactional Btrfs" -msgstr "" +msgstr "Transakční systém Btrfs" #: src/components/storage/PartitionsField.jsx:311 msgid "Btrfs with snapshots" -msgstr "" +msgstr "Btrfs se snímky" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") #: src/components/storage/PartitionsField.jsx:325 #, c-format msgid "Partition at %s" -msgstr "" +msgstr "Oddíl na %s" #. TRANSLATORS: %s will be replaced by a disk name (eg. "/dev/sda") #: src/components/storage/PartitionsField.jsx:328 #, c-format msgid "Separate LVM at %s" -msgstr "" +msgstr "Oddělené LVM na %s" #: src/components/storage/PartitionsField.jsx:331 msgid "Logical volume at system LVM" -msgstr "" +msgstr "Logický svazek na systému LVM" #: src/components/storage/PartitionsField.jsx:333 msgid "Partition at installation disk" -msgstr "" +msgstr "Oddíl na instalačním disku" #: src/components/storage/PartitionsField.jsx:348 msgid "Reset location" -msgstr "" +msgstr "Výmaz umístění" #: src/components/storage/PartitionsField.jsx:349 msgid "Change location" -msgstr "" +msgstr "Změna umístění" #: src/components/storage/PartitionsField.jsx:350 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" -msgstr "" +msgstr "Smazat" #: src/components/storage/PartitionsField.jsx:486 #: src/components/storage/VolumeFields.jsx:66 #: src/components/storage/VolumeFields.jsx:75 #: src/components/storage/VolumeFields.jsx:80 msgid "Mount point" -msgstr "" +msgstr "Přípojný bod" #. TRANSLATORS: where (and how) the file-system is going to be created #: src/components/storage/PartitionsField.jsx:490 msgid "Location" -msgstr "" +msgstr "Umístění" #: src/components/storage/PartitionsField.jsx:532 msgid "Table with mount points" -msgstr "" +msgstr "Tabulka s přípojnými body" #: src/components/storage/PartitionsField.jsx:604 #: src/components/storage/PartitionsField.jsx:624 #: src/components/storage/VolumeDialog.jsx:86 msgid "Add file system" -msgstr "" +msgstr "Přidat souborový systém" #: src/components/storage/PartitionsField.jsx:636 msgid "Other" -msgstr "" +msgstr "Ostatní/jiné" #: src/components/storage/PartitionsField.jsx:777 msgid "Reset to defaults" -msgstr "" +msgstr "Návrat k standardním hodnotám" #: src/components/storage/PartitionsField.jsx:849 msgid "Partitions and file systems" -msgstr "" +msgstr "Oddíly a souborové systémy" #: src/components/storage/PartitionsField.jsx:851 msgid "" "Structure of the new system, including any additional partition needed for " "booting" msgstr "" +"Struktura nového systému, včetně případných dalších oddílů potřebných pro " +"zavádění systému" #: src/components/storage/PartitionsField.jsx:858 msgid "Show partitions and file-systems actions" -msgstr "" +msgstr "Zobrazení oddílů a akcí souborových systémů" #: src/components/storage/ProposalActionsDialog.jsx:65 #, c-format msgid "Hide %d subvolume action" msgid_plural "Hide %d subvolume actions" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" +msgstr[0] "Skrýt %d akci podsvazku" +msgstr[1] "Skrýt %d akce podsvazku" +msgstr[2] "Skrýt %d akcí podsvazku" #: src/components/storage/ProposalActionsDialog.jsx:70 #, c-format msgid "Show %d subvolume action" msgid_plural "Show %d subvolume actions" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" +msgstr[0] "Zobrazit %d akci podsvazku" +msgstr[1] "Zobrazit %d akce podsvazku" +msgstr[2] "Zobrazit %d akcí podsvazku" #: src/components/storage/ProposalActionsSummary.jsx:57 msgid "Destructive actions are not allowed" -msgstr "" +msgstr "Destruktivní akce nejsou povoleny" #: src/components/storage/ProposalActionsSummary.jsx:59 msgid "Destructive actions are allowed" -msgstr "" +msgstr "Destruktivní akce jsou povoleny" #: src/components/storage/ProposalActionsSummary.jsx:82 #: src/components/storage/ProposalActionsSummary.jsx:132 msgid "affecting" -msgstr "" +msgstr "ovlivňující" #: src/components/storage/ProposalActionsSummary.jsx:112 msgid "Shrinking partitions is not allowed" -msgstr "" +msgstr "Zmenšování oddílů není povoleno" #: src/components/storage/ProposalActionsSummary.jsx:116 msgid "Shrinking partitions is allowed" -msgstr "" +msgstr "Zmenšování oddílů je povoleno" #: src/components/storage/ProposalActionsSummary.jsx:118 msgid "Shrinking some partitions is allowed but not needed" -msgstr "" +msgstr "Zmenšení některých oddílů je povoleno, ale není nutné" #: src/components/storage/ProposalActionsSummary.jsx:121 #, c-format msgid "%d partition will be shrunk" msgid_plural "%d partitions will be shrunk" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" +msgstr[0] "%d oddíl bude zmenšen" +msgstr[1] "%d oddíly budou zmenšeny" +msgstr[2] "%d oddílů bude zmenšeno" #: src/components/storage/ProposalActionsSummary.jsx:159 msgid "Cannot accommodate the required file systems for installation" -msgstr "" +msgstr "Nelze umístit požadované souborové systémy pro instalaci" #: src/components/storage/ProposalActionsSummary.jsx:167 #, c-format msgid "Check the planned action" msgid_plural "Check the %d planned actions" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" +msgstr[0] "Zkontrolujte plánovanou akci" +msgstr[1] "Zkontrolujte %d plánované akce" +msgstr[2] "Zkontrolujte %d plánovaných akcí" #: src/components/storage/ProposalActionsSummary.jsx:182 msgid "Waiting for actions information..." -msgstr "" +msgstr "Čekáme na informace o akcích..." #: src/components/storage/ProposalPage.jsx:317 msgid "Planned Actions" -msgstr "" +msgstr "Plánované akce" #: src/components/storage/ProposalResultSection.jsx:43 msgid "Waiting for information about storage configuration" -msgstr "" +msgstr "Čekání na informace o konfiguraci úložiště" #: src/components/storage/ProposalResultSection.jsx:73 msgid "Final layout" -msgstr "" +msgstr "Konečné rozvržení" #: src/components/storage/ProposalResultSection.jsx:74 msgid "The systems will be configured as displayed below." -msgstr "" +msgstr "Systémy budou konfigurovány tak, jak je zobrazeno níže." #: src/components/storage/ProposalResultSection.jsx:83 msgid "Storage proposal not possible" -msgstr "" +msgstr "Návrh úložiště není možný" #: src/components/storage/ProposalResultTable.jsx:79 msgid "New" -msgstr "" +msgstr "Nový" #. TRANSLATORS: Label to indicate the device size before resizing, where %s is #. replaced by the original size (e.g., 3.00 GiB). #: src/components/storage/ProposalResultTable.jsx:105 #, c-format msgid "Before %s" -msgstr "" +msgstr "Před %s" #: src/components/storage/ProposalResultTable.jsx:131 msgid "Mount Point" -msgstr "" +msgstr "Přípojný bod" #: src/components/storage/ProposalTransactionalInfo.jsx:45 msgid "Transactional root file system" -msgstr "" +msgstr "Transakční kořenový souborový systém" #: src/components/storage/ProposalTransactionalInfo.jsx:49 #, c-format @@ -1660,182 +1633,186 @@ msgid "" "%s is an immutable system with atomic updates. It uses a read-only Btrfs " "file system updated via snapshots." msgstr "" +"%s je neměnný systém s atomickými aktualizacemi. Používá souborový systém " +"Btrfs pouze pro čtení aktualizovaný pomocí snímků." #: src/components/storage/SnapshotsField.jsx:36 msgid "Use Btrfs snapshots for the root file system" -msgstr "" +msgstr "Použití snímků Btrfs pro kořenový souborový systém" #: src/components/storage/SnapshotsField.jsx:38 msgid "" "Allows to boot to a previous version of the system after configuration " "changes or software upgrades." msgstr "" +"Umožňuje zavést předchozí verzi systému po změně konfigurace nebo " +"aktualizaci softwaru." #: src/components/storage/SpaceActionsTable.jsx:68 #, c-format msgid "Up to %s can be recovered by shrinking the device." -msgstr "" +msgstr "Zmenšením zařízení lze obnovit až %s." #: src/components/storage/SpaceActionsTable.jsx:77 msgid "The device cannot be shrunk:" -msgstr "" +msgstr "Zařízení nelze zmenšit:" #: src/components/storage/SpaceActionsTable.jsx:98 #, c-format msgid "Show information about %s" -msgstr "" +msgstr "Zobrazit informace o %s" #: src/components/storage/SpaceActionsTable.jsx:172 msgid "The content may be deleted" -msgstr "" +msgstr "Obsah může být smazán" #: src/components/storage/SpaceActionsTable.jsx:204 msgid "Action" -msgstr "" +msgstr "Akce" #: src/components/storage/SpaceActionsTable.jsx:215 msgid "Actions to find space" -msgstr "" +msgstr "Akce k nalezení prostoru" #: src/components/storage/SpacePolicySelection.jsx:172 msgid "Space policy" -msgstr "" +msgstr "Zásady pro volné místo" #: src/components/storage/VolumeDialog.jsx:83 #, c-format msgid "Add %s file system" -msgstr "" +msgstr "Přidání souborového systému %s" #: src/components/storage/VolumeDialog.jsx:84 #, c-format msgid "Edit %s file system" -msgstr "" +msgstr "Upravit souborový systém %s" #: src/components/storage/VolumeDialog.jsx:86 msgid "Edit file system" -msgstr "" +msgstr "Úprava souborového systému" #. TRANSLATORS: Warning when editing a file system. #: src/components/storage/VolumeDialog.jsx:101 msgid "The type and size of the file system cannot be edited." -msgstr "" +msgstr "Typ a velikost souborového systému nelze upravovat." #: src/components/storage/VolumeDialog.jsx:105 #, c-format msgid "The current file system on %s is selected to be mounted at %s." -msgstr "" +msgstr "Aktuální souborový systém na %s je vybrán k připojení k %s." #. TRANSLATORS: Warning when editing a file system. #: src/components/storage/VolumeDialog.jsx:113 msgid "The size of the file system cannot be edited" -msgstr "" +msgstr "Velikost souborového systému nelze měnit" #. TRANSLATORS: Description of a warning. %s is replaced by a device name (e.g., /dev/vda). #: src/components/storage/VolumeDialog.jsx:115 #, c-format msgid "The file system is allocated at the device %s." -msgstr "" +msgstr "Souborový systém je přidělen na zařízení %s." #: src/components/storage/VolumeDialog.jsx:163 msgid "A mount point is required" -msgstr "" +msgstr "Je vyžadován přípojný bod" #: src/components/storage/VolumeDialog.jsx:190 msgid "The mount point is invalid" -msgstr "" +msgstr "Přípojný bod je neplatný" #: src/components/storage/VolumeDialog.jsx:218 msgid "A size value is required" -msgstr "" +msgstr "Je vyžadována hodnota velikosti" #: src/components/storage/VolumeDialog.jsx:246 msgid "Minimum size is required" -msgstr "" +msgstr "Je vyžadována minimální velikost" #: src/components/storage/VolumeDialog.jsx:278 msgid "Maximum must be greater than minimum" -msgstr "" +msgstr "Maximum musí být větší než minimum" #: src/components/storage/VolumeDialog.jsx:320 #, c-format msgid "There is already a file system for %s." -msgstr "" +msgstr "Pro %s již existuje souborový systém." #: src/components/storage/VolumeDialog.jsx:322 msgid "Do you want to edit it?" -msgstr "" +msgstr "Chcete to upravit?" #: src/components/storage/VolumeDialog.jsx:367 #, c-format msgid "There is a predefined file system for %s." -msgstr "" +msgstr "Pro %s existuje předdefinovaný souborový systém." #: src/components/storage/VolumeDialog.jsx:369 msgid "Do you want to add it?" -msgstr "" +msgstr "Chcete ho přidat?" #: src/components/storage/VolumeFields.jsx:225 msgid "" "The options for the file system type depends on the product and the mount " "point." -msgstr "" +msgstr "Možnosti typu souborového systému závisí na produktu a přípojném bodu." #: src/components/storage/VolumeFields.jsx:232 msgid "More info for file system types" -msgstr "" +msgstr "Další informace o typech souborových systémů" #. TRANSLATORS: label for the file system selector. #: src/components/storage/VolumeFields.jsx:243 msgid "File system type" -msgstr "" +msgstr "Typ systému souborů" #. TRANSLATORS: item which affects the final computed partition size #: src/components/storage/VolumeFields.jsx:274 msgid "the configuration of snapshots" -msgstr "" +msgstr "konfigurace snímků" #: src/components/storage/VolumeFields.jsx:281 #, c-format msgid "the presence of the file system for %s" -msgstr "" +msgstr "přítomnost souborového systému pro %s" #. TRANSLATORS: conjunction for merging two list items #: src/components/storage/VolumeFields.jsx:283 msgid ", " -msgstr "" +msgstr ", " #. TRANSLATORS: item which affects the final computed partition size #: src/components/storage/VolumeFields.jsx:289 msgid "the amount of RAM in the system" -msgstr "" +msgstr "velikost paměti RAM v systému" #: src/components/storage/VolumeFields.jsx:293 #, c-format msgid "The final size depends on %s." -msgstr "" +msgstr "Konečná velikost závisí na %s." #. TRANSLATORS: conjunction for merging two texts #: src/components/storage/VolumeFields.jsx:295 msgid " and " -msgstr "" +msgstr " a " #: src/components/storage/VolumeFields.jsx:302 msgid "Automatically calculated size according to the selected product." -msgstr "" +msgstr "Automatický výpočet velikosti podle vybraného produktu." #: src/components/storage/VolumeFields.jsx:321 msgid "Exact size for the file system." -msgstr "" +msgstr "Přesná velikost souborového systému." #. TRANSLATORS: requested partition size #: src/components/storage/VolumeFields.jsx:330 msgid "Exact size" -msgstr "" +msgstr "Přesná velikost" #. TRANSLATORS: units selector (like KiB, MiB, GiB...) #: src/components/storage/VolumeFields.jsx:347 msgid "Size unit" -msgstr "" +msgstr "Jednotka velikosti" #: src/components/storage/VolumeFields.jsx:376 msgid "" @@ -1843,209 +1820,223 @@ msgid "" "given minimum and maximum. If no maximum is given then the file system will " "be as big as possible." msgstr "" +"Omezení velikosti souborového systému. Konečná velikost bude hodnota mezi " +"zadaným minimem a maximem. Pokud není zadáno žádné maximum, bude souborový " +"systém co největší." #. TRANSLATORS: the minimal partition size #: src/components/storage/VolumeFields.jsx:384 msgid "Minimum" -msgstr "" +msgstr "Minimum" #. TRANSLATORS: the minium partition size #: src/components/storage/VolumeFields.jsx:395 msgid "Minimum desired size" -msgstr "" +msgstr "Minimální požadovaná velikost" #: src/components/storage/VolumeFields.jsx:406 msgid "Unit for the minimum size" -msgstr "" +msgstr "Jednotka pro minimální velikost" #. TRANSLATORS: the maximum partition size #: src/components/storage/VolumeFields.jsx:418 msgid "Maximum" -msgstr "" +msgstr "Maximum" #. TRANSLATORS: the maximum partition size #: src/components/storage/VolumeFields.jsx:430 msgid "Maximum desired size" -msgstr "" +msgstr "Maximální požadovaná velikost" #: src/components/storage/VolumeFields.jsx:440 msgid "Unit for the maximum size" -msgstr "" +msgstr "Jednotka pro maximální velikost" #. TRANSLATORS: radio button label, fully automatically computed partition size, no user input #: src/components/storage/VolumeFields.jsx:458 msgid "Auto" -msgstr "" +msgstr "Auto" #. TRANSLATORS: radio button label, exact partition size requested by user #: src/components/storage/VolumeFields.jsx:460 msgid "Fixed" -msgstr "" +msgstr "Opraveno" #. TRANSLATORS: radio button label, automatically computed partition size within the user provided min and max limits #: src/components/storage/VolumeFields.jsx:462 msgid "Range" -msgstr "" +msgstr "Rozsah" #: src/components/storage/VolumeLocationDialog.jsx:41 msgid "" "The file systems are allocated at the installation device by default. " "Indicate a custom location to create the file system at a specific device." msgstr "" +"Souborové systémy jsou ve výchozím nastavení vytvořeny v instalačním " +"zařízení. Chcete-li vytvořit souborový systém na konkrétním zařízení, " +"zadejte vlastní umístění." #. TRANSLATORS: Title of the dialog for changing the location of a file system. %s is replaced #. by a mount path (e.g., /home). #: src/components/storage/VolumeLocationDialog.jsx:137 #, c-format msgid "Location for %s file system" -msgstr "" +msgstr "Umístění souborového systému %s" #: src/components/storage/VolumeLocationDialog.jsx:147 msgid "Select in which device to allocate the file system" -msgstr "" +msgstr "Vyberte, ve kterém zařízení se má vytvořit systém souborů" #: src/components/storage/VolumeLocationDialog.jsx:150 msgid "Select a location" -msgstr "" +msgstr "Vyberte umístění" #: src/components/storage/VolumeLocationDialog.jsx:162 msgid "Select how to allocate the file system" -msgstr "" +msgstr "Zvolte způsob vytvoření souborového systému" #: src/components/storage/VolumeLocationDialog.jsx:167 msgid "Create a new partition" -msgstr "" +msgstr "Vytvořit nový oddíl" #: src/components/storage/VolumeLocationDialog.jsx:169 msgid "" "The file system will be allocated as a new partition at the selected disk." -msgstr "" +msgstr "Souborový systém bude přidělen jako nový oddíl na vybraném disku." #: src/components/storage/VolumeLocationDialog.jsx:179 msgid "Create a dedicated LVM volume group" -msgstr "" +msgstr "Vytvoření vyhrazené skupiny svazků LVM" #: src/components/storage/VolumeLocationDialog.jsx:181 msgid "" "A new volume group will be allocated in the selected disk and the file " "system will be created as a logical volume." msgstr "" +"Na vybraném disku bude vytvořena nová skupina svazků a systém souborů bude " +"vytvořen jako logický svazek." #: src/components/storage/VolumeLocationDialog.jsx:191 msgid "Format the device" -msgstr "" +msgstr "Formátovat zařízení" #: src/components/storage/VolumeLocationDialog.jsx:195 #, c-format msgid "The selected device will be formatted as %s file system." -msgstr "" +msgstr "Vybrané zařízení bude formátováno jako souborový systém %s." #: src/components/storage/VolumeLocationDialog.jsx:206 msgid "Mount the file system" -msgstr "" +msgstr "Připojit souborový systém" #: src/components/storage/VolumeLocationDialog.jsx:208 msgid "" "The current file system on the selected device will be mounted without " "formatting the device." msgstr "" +"Aktuální souborový systém na vybraném zařízení bude připojen bez " +"formátování zařízení." #: src/components/storage/VolumeLocationSelectorTable.jsx:110 msgid "Usage" -msgstr "" +msgstr "Použití" #: src/components/storage/ZFCPDiskForm.jsx:106 msgid "The zFCP disk was not activated." -msgstr "" +msgstr "Disk zFCP nebyl aktivován." #. TRANSLATORS: abbrev. World Wide Port Name #: src/components/storage/ZFCPDiskForm.jsx:123 #: src/components/storage/ZFCPPage.jsx:383 msgid "WWPN" -msgstr "" +msgstr "WWPN" #. TRANSLATORS: abbrev. Logical Unit Number #: src/components/storage/ZFCPDiskForm.jsx:131 #: src/components/storage/ZFCPPage.jsx:384 msgid "LUN" -msgstr "" +msgstr "LUN" #: src/components/storage/ZFCPPage.jsx:326 msgid "Auto LUNs Scan" -msgstr "" +msgstr "Automatické skenování jednotek LUN" #: src/components/storage/ZFCPPage.jsx:337 msgid "Activated" -msgstr "" +msgstr "Aktivováno" #: src/components/storage/ZFCPPage.jsx:337 msgid "Deactivated" -msgstr "" +msgstr "Deaktivováno" #: src/components/storage/ZFCPPage.jsx:437 msgid "No zFCP controllers found." -msgstr "" +msgstr "Nebyly nalezeny žádné řadiče zFCP." #: src/components/storage/ZFCPPage.jsx:438 msgid "Please, try to read the zFCP devices again." -msgstr "" +msgstr "Zkuste znovu načíst zařízení zFCP." #: src/components/storage/ZFCPPage.jsx:441 msgid "Read zFCP devices" -msgstr "" +msgstr "Načtení zařízení zFCP" #: src/components/storage/ZFCPPage.jsx:452 msgid "" "Automatic LUN scan is [enabled]. Activating a controller which is running in " "NPIV mode will automatically configures all its LUNs." msgstr "" +"Automatické skenování LUN je [povoleno]. Aktivací řadiče, který běží v " +"režimu NPIV, se automaticky nakonfigurují všechny jeho LUN." #: src/components/storage/ZFCPPage.jsx:457 msgid "" "Automatic LUN scan is [disabled]. LUNs have to be manually configured after " "activating a controller." msgstr "" +"Automatické skenování LUN je [zakázáno]. Po aktivaci řadiče je třeba LUNy " +"nakonfigurovat ručně." #: src/components/storage/ZFCPPage.jsx:519 msgid "Activate a zFCP disk" -msgstr "" +msgstr "Aktivovat disk zFCP" #: src/components/storage/ZFCPPage.jsx:553 msgid "Please, try to activate a zFCP controller." -msgstr "" +msgstr "Zkuste aktivovat řadič zFCP." #: src/components/storage/ZFCPPage.jsx:559 msgid "Please, try to activate a zFCP disk." -msgstr "" +msgstr "Zkuste aktivovat disk zFCP." #: src/components/storage/ZFCPPage.jsx:562 msgid "Activate zFCP disk" -msgstr "" +msgstr "Aktivovat disk zFCP" #: src/components/storage/ZFCPPage.jsx:570 msgid "No zFCP disks found." -msgstr "" +msgstr "Nebyly nalezeny žádné disky zFCP." #: src/components/storage/ZFCPPage.jsx:586 msgid "Activate new disk" -msgstr "" +msgstr "Aktivace nového disku" #. TRANSLATORS: section title #: src/components/storage/ZFCPPage.jsx:599 msgid "Disks" -msgstr "" +msgstr "Disky" #: src/components/storage/device-utils.jsx:92 msgid "Unused space" -msgstr "" +msgstr "Nevyužitý prostor" #: src/components/storage/iscsi/AuthFields.jsx:70 msgid "Only available if authentication by target is provided" -msgstr "" +msgstr "K dispozici, jen když je zadáno ověřování cílem" #: src/components/storage/iscsi/AuthFields.jsx:77 msgid "Authentication by target" -msgstr "" +msgstr "Ověřování cílem" #: src/components/storage/iscsi/AuthFields.jsx:78 #: src/components/storage/iscsi/AuthFields.jsx:82 @@ -2054,75 +2045,75 @@ msgstr "" #: src/components/storage/iscsi/AuthFields.jsx:108 #: src/components/storage/iscsi/AuthFields.jsx:110 msgid "User name" -msgstr "" +msgstr "Uživatelské jméno" #: src/components/storage/iscsi/AuthFields.jsx:88 #: src/components/storage/iscsi/AuthFields.jsx:116 msgid "Incorrect user name" -msgstr "" +msgstr "Nesprávné uživatelské jméno" #: src/components/storage/iscsi/AuthFields.jsx:99 #: src/components/storage/iscsi/AuthFields.jsx:130 msgid "Incorrect password" -msgstr "" +msgstr "Nesprávné heslo" #: src/components/storage/iscsi/AuthFields.jsx:102 msgid "Authentication by initiator" -msgstr "" +msgstr "Ověření iniciátorem" #: src/components/storage/iscsi/AuthFields.jsx:123 msgid "Target Password" -msgstr "" +msgstr "Cílové heslo" #. TRANSLATORS: popup title #: src/components/storage/iscsi/DiscoverForm.jsx:94 msgid "Discover iSCSI Targets" -msgstr "" +msgstr "Najít cílové stanice iSCSI" #: src/components/storage/iscsi/DiscoverForm.jsx:99 #: src/components/storage/iscsi/LoginForm.jsx:70 msgid "Make sure you provide the correct values" -msgstr "" +msgstr "Ujistěte se, že jste zadali správné hodnoty" #: src/components/storage/iscsi/DiscoverForm.jsx:103 msgid "IP address" -msgstr "" +msgstr "adresa IP" #. TRANSLATORS: network address #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/DiscoverForm.jsx:110 msgid "Address" -msgstr "" +msgstr "Adresa" #: src/components/storage/iscsi/DiscoverForm.jsx:115 msgid "Incorrect IP address" -msgstr "" +msgstr "Nesprávná IP adresa" #. TRANSLATORS: network port number #: src/components/storage/iscsi/DiscoverForm.jsx:117 #: src/components/storage/iscsi/DiscoverForm.jsx:122 #: src/components/storage/iscsi/DiscoverForm.jsx:124 msgid "Port" -msgstr "" +msgstr "Port" #: src/components/storage/iscsi/DiscoverForm.jsx:129 msgid "Incorrect port" -msgstr "" +msgstr "Nesprávný port" #. TRANSLATORS: %s is replaced by the iSCSI target node name #: src/components/storage/iscsi/EditNodeForm.jsx:48 #, c-format msgid "Edit %s" -msgstr "" +msgstr "Upravit %s" #: src/components/storage/iscsi/InitiatorForm.jsx:42 msgid "Edit iSCSI Initiator" -msgstr "" +msgstr "Upravit iniciátor iSCSI" #. TRANSLATORS: iSCSI initiator name #: src/components/storage/iscsi/InitiatorForm.jsx:49 msgid "Initiator name" -msgstr "" +msgstr "Název iniciátora" #. TRANSLATORS: usually just keep the original text #. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI @@ -2131,327 +2122,392 @@ msgstr "" #: src/components/storage/iscsi/NodesPresenter.jsx:101 #: src/components/storage/iscsi/NodesPresenter.jsx:122 msgid "iBFT" -msgstr "" +msgstr "iBFT" #: src/components/storage/iscsi/InitiatorPresenter.jsx:72 #: src/components/storage/iscsi/InitiatorPresenter.jsx:87 +#, fuzzy msgid "Offload card" -msgstr "" +msgstr "Vyložení karty" #. TRANSLATORS: iSCSI initiator section name #: src/components/storage/iscsi/InitiatorSection.jsx:49 msgid "Initiator" -msgstr "" +msgstr "Iniciátor" #. TRANSLATORS: %s is replaced by the iSCSI target name #: src/components/storage/iscsi/LoginForm.jsx:66 #, c-format msgid "Login %s" -msgstr "" +msgstr "Přihlášení %s" #. TRANSLATORS: iSCSI start up mode (on boot/manual/automatic) #: src/components/storage/iscsi/LoginForm.jsx:74 #: src/components/storage/iscsi/LoginForm.jsx:77 msgid "Startup" -msgstr "" +msgstr "Typ startu iSCSI" #: src/components/storage/iscsi/NodeStartupOptions.js:26 msgid "On boot" -msgstr "" +msgstr "Při spuštění systému" #. TRANSLATORS: iSCSI connection status, %s is replaced by node label #: src/components/storage/iscsi/NodesPresenter.jsx:67 #, c-format msgid "Connected (%s)" -msgstr "" +msgstr "Připojeno (%s)" #: src/components/storage/iscsi/NodesPresenter.jsx:82 msgid "Login" -msgstr "" +msgstr "Přihlášení" #: src/components/storage/iscsi/NodesPresenter.jsx:86 msgid "Logout" -msgstr "" +msgstr "Odhlášení" #: src/components/storage/iscsi/NodesPresenter.jsx:99 #: src/components/storage/iscsi/NodesPresenter.jsx:120 msgid "Portal" -msgstr "" +msgstr "Portál" #: src/components/storage/iscsi/NodesPresenter.jsx:100 #: src/components/storage/iscsi/NodesPresenter.jsx:121 msgid "Interface" -msgstr "" +msgstr "Rozhraní" #: src/components/storage/iscsi/TargetsSection.jsx:138 msgid "No iSCSI targets found." -msgstr "" +msgstr "Nebyly nalezeny žádné cíle iSCSI." #: src/components/storage/iscsi/TargetsSection.jsx:140 msgid "" "Please, perform an iSCSI discovery in order to find available iSCSI targets." -msgstr "" +msgstr "Spusťte vyhledávání iSCSI a tím najděte dostupné cíle iSCSI." #: src/components/storage/iscsi/TargetsSection.jsx:144 msgid "Discover iSCSI targets" -msgstr "" +msgstr "Zjištění cílů iSCSI" #. TRANSLATORS: button label, starts iSCSI discovery #: src/components/storage/iscsi/TargetsSection.jsx:156 msgid "Discover" -msgstr "" +msgstr "Objevit" #. TRANSLATORS: iSCSI targets section title #: src/components/storage/iscsi/TargetsSection.jsx:167 msgid "Targets" -msgstr "" +msgstr "Cíle" #: src/components/storage/routes.js:36 msgid "Proposal" -msgstr "" +msgstr "Návrh" #: src/components/storage/utils.js:64 msgid "KiB" -msgstr "" +msgstr "KiB" #: src/components/storage/utils.js:65 msgid "MiB" -msgstr "" +msgstr "MiB" #: src/components/storage/utils.js:66 msgid "GiB" -msgstr "" +msgstr "GiB" #: src/components/storage/utils.js:67 msgid "TiB" -msgstr "" +msgstr "TiB" #: src/components/storage/utils.js:68 msgid "PiB" -msgstr "" +msgstr "PiB" #: src/components/storage/utils.js:77 msgid "Delete current content" -msgstr "" +msgstr "Odstranit aktuální obsah" #: src/components/storage/utils.js:78 msgid "All partitions will be removed and any data in the disks will be lost." msgstr "" +"Všechny oddíly budou odstraněny a veškerá data na discích budou ztracena." #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space deleting current content". Keep it short #: src/components/storage/utils.js:82 msgid "deleting current content" -msgstr "" +msgstr "odstranění aktuálního obsahu" #: src/components/storage/utils.js:87 msgid "Shrink existing partitions" -msgstr "" +msgstr "Zmenšit stávající oddíly" #: src/components/storage/utils.js:88 msgid "The data is kept, but the current partitions will be resized as needed." msgstr "" +"Data zůstanou zachována, ale velikost aktuálních oddílů se podle potřeby " +"změní." #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space shrinking partitions". Keep it short. #: src/components/storage/utils.js:92 msgid "shrinking partitions" -msgstr "" +msgstr "zmenšování oddílů" #: src/components/storage/utils.js:97 msgid "Use available space" -msgstr "" +msgstr "Využít dostupný prostor" #: src/components/storage/utils.js:98 msgid "" "The data is kept. Only the space not assigned to any partition will be used." msgstr "" +"Data jsou uchována. Využije se pouze prostor, který není přiřazen žádnému " +"oddílu." #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space without modifying any partition". Keep it short. #: src/components/storage/utils.js:102 msgid "without modifying any partition" -msgstr "" +msgstr "bez úpravy jakéhokoli oddílu" #: src/components/storage/utils.js:107 msgid "Custom" -msgstr "" +msgstr "Vlastní" #: src/components/storage/utils.js:108 msgid "Select what to do with each partition." -msgstr "" +msgstr "Vyberte, co se má s jednotlivými oddíly dělat." #. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence #. would read as "Find space with custom actions". Keep it short. #: src/components/storage/utils.js:112 msgid "with custom actions" -msgstr "" +msgstr "s vlastními akcemi" -#: src/components/users/FirstUser.jsx:35 +#: src/components/users/FirstUser.jsx:34 msgid "No user defined yet." -msgstr "" +msgstr "Zatím není definován žádný uživatel." -#: src/components/users/FirstUser.jsx:39 +#: src/components/users/FirstUser.jsx:38 msgid "" "Please, be aware that a user must be defined before installing the system to " "be able to log into it." msgstr "" +"Pozor, před instalací systému musí být definován uživatel, aby se pak do " +"systému dalo přihlásit." -#: src/components/users/FirstUser.jsx:45 +#: src/components/users/FirstUser.jsx:44 msgid "Define a user now" -msgstr "" +msgstr "Nyní definujte uživatele" -#: src/components/users/FirstUser.jsx:58 -#: src/components/users/FirstUserForm.jsx:227 +#: src/components/users/FirstUser.jsx:57 +#: src/components/users/FirstUserForm.jsx:217 msgid "Full name" -msgstr "" +msgstr "Celé jméno" -#: src/components/users/FirstUser.jsx:59 -#: src/components/users/FirstUserForm.jsx:241 -#: src/components/users/FirstUserForm.jsx:246 -#: src/components/users/FirstUserForm.jsx:249 +#: src/components/users/FirstUser.jsx:58 +#: src/components/users/FirstUserForm.jsx:231 +#: src/components/users/FirstUserForm.jsx:236 +#: src/components/users/FirstUserForm.jsx:239 msgid "Username" -msgstr "" +msgstr "Uživatelské jméno" -#: src/components/users/FirstUser.jsx:124 -#: src/components/users/RootAuthMethods.jsx:104 -#: src/components/users/RootAuthMethods.jsx:116 +#: src/components/users/FirstUser.jsx:89 +#: src/components/users/RootAuthMethods.jsx:78 +#: src/components/users/RootAuthMethods.jsx:90 msgid "Discard" -msgstr "" +msgstr "Vyřadit" -#: src/components/users/FirstUserForm.jsx:57 +#: src/components/users/FirstUserForm.jsx:58 msgid "Username suggestion dropdown" -msgstr "" +msgstr "Rozbalovací nabídka uživatelských jmen" #. TRANSLATORS: dropdown username suggestions -#: src/components/users/FirstUserForm.jsx:72 +#: src/components/users/FirstUserForm.jsx:73 msgid "Use suggested username" -msgstr "" +msgstr "Použijte navrhované uživatelské jméno" -#: src/components/users/FirstUserForm.jsx:151 +#: src/components/users/FirstUserForm.jsx:144 msgid "All fields are required" -msgstr "" - -#: src/components/users/FirstUserForm.jsx:158 -msgid "Please, try again." -msgstr "" +msgstr "Všechna pole jsou povinná" -#: src/components/users/FirstUserForm.jsx:211 +#: src/components/users/FirstUserForm.jsx:201 msgid "Create user" -msgstr "" +msgstr "Vytvořit uživatele" -#: src/components/users/FirstUserForm.jsx:211 +#: src/components/users/FirstUserForm.jsx:201 msgid "Edit user" -msgstr "" +msgstr "Upravit uživatele" -#: src/components/users/FirstUserForm.jsx:231 -#: src/components/users/FirstUserForm.jsx:233 +#: src/components/users/FirstUserForm.jsx:221 +#: src/components/users/FirstUserForm.jsx:223 msgid "User full name" -msgstr "" +msgstr "Celé jméno uživatele" -#: src/components/users/FirstUserForm.jsx:271 +#: src/components/users/FirstUserForm.jsx:261 msgid "Edit password too" -msgstr "" +msgstr "Upravit také heslo" -#: src/components/users/FirstUserForm.jsx:287 +#: src/components/users/FirstUserForm.jsx:277 msgid "user autologin" -msgstr "" +msgstr "automatické přihlášení uživatele" #. TRANSLATORS: check box label -#: src/components/users/FirstUserForm.jsx:291 +#: src/components/users/FirstUserForm.jsx:281 msgid "Auto-login" -msgstr "" +msgstr "Automatické přihlášení" -#: src/components/users/RootAuthMethods.jsx:35 +#: src/components/users/RootAuthMethods.jsx:36 msgid "No root authentication method defined yet." -msgstr "" +msgstr "Zatím není definována žádná metoda ověřování superuživatele root." -#: src/components/users/RootAuthMethods.jsx:39 +#: src/components/users/RootAuthMethods.jsx:40 msgid "" "Please, define at least one authentication method for logging into the " "system as root." msgstr "" +"Definujte alespoň jednu metodu ověřování pro přihlášení do systému jako root." -#: src/components/users/RootAuthMethods.jsx:46 +#: src/components/users/RootAuthMethods.jsx:47 msgid "Set a password" -msgstr "" +msgstr "Nastavte heslo" -#: src/components/users/RootAuthMethods.jsx:50 +#: src/components/users/RootAuthMethods.jsx:51 msgid "Upload a SSH Public Key" -msgstr "" +msgstr "Nahrátí veřejného klíče SSH" -#: src/components/users/RootAuthMethods.jsx:100 -#: src/components/users/RootAuthMethods.jsx:112 +#: src/components/users/RootAuthMethods.jsx:74 +#: src/components/users/RootAuthMethods.jsx:86 msgid "Set" -msgstr "" +msgstr "Nastavit" -#: src/components/users/RootAuthMethods.jsx:132 +#: src/components/users/RootAuthMethods.jsx:97 msgid "Already set" -msgstr "" +msgstr "Již nastaveno" -#: src/components/users/RootAuthMethods.jsx:132 -#: src/components/users/RootAuthMethods.jsx:136 +#: src/components/users/RootAuthMethods.jsx:97 +#: src/components/users/RootAuthMethods.jsx:101 msgid "Not set" -msgstr "" +msgstr "Nenastaveno" #. TRANSLATORS: table header, user authentication method -#: src/components/users/RootAuthMethods.jsx:157 +#: src/components/users/RootAuthMethods.jsx:122 msgid "Method" -msgstr "" +msgstr "Metoda" -#: src/components/users/RootAuthMethods.jsx:174 +#: src/components/users/RootAuthMethods.jsx:139 msgid "SSH Key" -msgstr "" +msgstr "Klíč SSH" -#: src/components/users/RootAuthMethods.jsx:193 +#: src/components/users/RootAuthMethods.jsx:158 msgid "Change the root password" -msgstr "" +msgstr "Změna hesla roota" -#: src/components/users/RootAuthMethods.jsx:193 +#: src/components/users/RootAuthMethods.jsx:158 msgid "Set a root password" -msgstr "" +msgstr "Nastavte heslo roota" -#: src/components/users/RootAuthMethods.jsx:203 +#: src/components/users/RootAuthMethods.jsx:168 msgid "Edit the SSH Public Key for root" -msgstr "" +msgstr "Úprava veřejného klíče SSH pro uživatele root" -#: src/components/users/RootAuthMethods.jsx:204 +#: src/components/users/RootAuthMethods.jsx:169 msgid "Add a SSH Public Key for root" -msgstr "" +msgstr "Přidat veřejný klíč SSH pro uživatele root" -#: src/components/users/RootPasswordPopup.jsx:43 +#: src/components/users/RootPasswordPopup.jsx:44 msgid "Root password" -msgstr "" +msgstr "Heslo roota" #: src/components/users/RootSSHKeyPopup.jsx:43 msgid "Set root SSH public key" -msgstr "" +msgstr "Nastavte veřejný klíč SSH pro roota" #: src/components/users/RootSSHKeyPopup.jsx:71 msgid "Root SSH public key" -msgstr "" +msgstr "Veřejný klíč SSH pro roota" #: src/components/users/RootSSHKeyPopup.jsx:76 msgid "Upload, paste, or drop an SSH public key" -msgstr "" +msgstr "Nahrání, vložení nebo přetažení veřejného klíče SSH" #. TRANSLATORS: push button label #: src/components/users/RootSSHKeyPopup.jsx:78 msgid "Upload" -msgstr "" +msgstr "Nahrát" #. TRANSLATORS: push button label, clears the related input field #: src/components/users/RootSSHKeyPopup.jsx:80 msgid "Clear" -msgstr "" +msgstr "Smazat" #: src/components/users/UsersPage.jsx:45 msgid "First user" -msgstr "" +msgstr "První uživatel" #: src/components/users/UsersPage.jsx:52 msgid "Root authentication" -msgstr "" +msgstr "Ověření superuživatele root" + +#, c-format +#~ msgid "Register %s" +#~ msgstr "Registrovat %s" + +#~ msgid "Registration code" +#~ msgstr "Registrační kód" + +#~ msgid "Email" +#~ msgstr "E-mail" + +#~ msgid "The installation will take" +#~ msgstr "Instalace zabere" + +#, c-format +#~ msgid "The installation will take %s including:" +#~ msgstr "Instalace bude trvat %s včetně:" + +#~ msgid "No additional software was selected." +#~ msgstr "Nebyl vybrán žádný další software." + +#~ msgid "The following software patterns are selected for installation:" +#~ msgstr "Pro instalaci jsou vybrány tyto softwarové vzory:" + +#~ msgid "Selected patterns" +#~ msgstr "Vybrané vzory" + +#~ msgid "Change selection" +#~ msgstr "Změnit výběr" + +#~ msgid "" +#~ "This product does not allow to select software patterns during " +#~ "installation. However, you can add additional software once the " +#~ "installation is finished." +#~ msgstr "" +#~ "Tento produkt neumožňuje výběr softwarových vzorů během instalace. Po " +#~ "dokončení instalace však můžete přidat další software." + +#~ msgid "auto selected" +#~ msgstr "automaticky vybráno" + +#~ msgid "None of the patterns match the filter." +#~ msgstr "Žádný ze vzorů neodpovídá filtru." + +#~ msgid "Software selection" +#~ msgstr "Výběr softwaru" + +#~ msgid "Filter by pattern title or description" +#~ msgstr "Filtrování podle názvu nebo popisu vzoru" + +#, c-format +#~ msgid "Installation will take %s." +#~ msgstr "Instalace bude trvat %s." + +#~ msgid "" +#~ "This space includes the base system and the selected software patterns, " +#~ "if any." +#~ msgstr "" +#~ "Tento prostor zahrnuje základní systém a vybrané softwarové vzory, pokud " +#~ "existují." #~ msgid "Reading file..." #~ msgstr "Soubor se načítá…" diff --git a/web/po/de.po b/web/po/de.po index 9b304decba..49ab3c6ad6 100644 --- a/web/po/de.po +++ b/web/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-18 02:24+0000\n" +"POT-Creation-Date: 2024-07-28 02:29+0000\n" "PO-Revision-Date: 2024-07-13 19:47+0000\n" "Last-Translator: Ettore Atalan \n" "Language-Team: German - + From 03d9c4b72794d8bfedb5490e050f529bf1d1a694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 29 Jul 2024 09:48:04 +0100 Subject: [PATCH 319/430] refactor(web): extract change product link to a component --- .../core/ChangeProductLink.test.tsx | 69 +++++++++++++++++++ web/src/components/core/ChangeProductLink.tsx | 41 +++++++++++ web/src/components/core/index.js | 1 + 3 files changed, 111 insertions(+) create mode 100644 web/src/components/core/ChangeProductLink.test.tsx create mode 100644 web/src/components/core/ChangeProductLink.tsx diff --git a/web/src/components/core/ChangeProductLink.test.tsx b/web/src/components/core/ChangeProductLink.test.tsx new file mode 100644 index 0000000000..df69181a80 --- /dev/null +++ b/web/src/components/core/ChangeProductLink.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import { PATHS } from "~/routes/products"; +import { Product } from "~/types/software"; +import ChangeProductLink from "./ChangeProductLink"; + +const tumbleweedProduct = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + description: "Tumbleweed description...", +}; +const microosProduct = { + id: "MicroOS", + name: "openSUSE MicroOS", + description: "MicroOS description", +}; + +let mockUseProduct: { products: Product[]; selectedProduct?: Product }; + +jest.mock("~/queries/software", () => ({ + useProduct: () => mockUseProduct, +})); + +describe("ChangeProductLink", () => { + describe("when there is more than one product available", () => { + beforeEach(() => { + mockUseProduct = { products: [tumbleweedProduct, microosProduct] }; + }); + + it("renders a link for navigating to product selection page", () => { + installerRender(); + const link = screen.getByRole("link", { name: "Change product" }); + expect(link).toHaveAttribute("href", PATHS.changeProduct); + }); + }); + + describe("when there is only one product available", () => { + beforeEach(() => { + mockUseProduct = { products: [tumbleweedProduct] }; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/web/src/components/core/ChangeProductLink.tsx b/web/src/components/core/ChangeProductLink.tsx new file mode 100644 index 0000000000..bc965f9f64 --- /dev/null +++ b/web/src/components/core/ChangeProductLink.tsx @@ -0,0 +1,41 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { Link, LinkProps } from "react-router-dom"; +import { useProduct } from "~/queries/software"; +import { PATHS } from "~/routes/products"; +import { _ } from "~/i18n"; + +/** + * Link for navigating to the selection product. + */ +export default function ChangeProductLink({ children, ...props }: Omit) { + const { products } = useProduct(); + + if (products.length <= 1) return null; + + return ( + + {children || _("Change product")} + + ); +} diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index 54ad24b00d..ada3c45abf 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -20,6 +20,7 @@ */ export { default as About } from "./About"; +export { default as ChangeProductLink } from "./ChangeProductLink"; export { default as Description } from "./Description"; export { default as Section } from "./Section"; export { default as FormLabel } from "./FormLabel"; From 4212f0195664b3707b1b7d6b4595f1009c79c220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 29 Jul 2024 09:56:13 +0100 Subject: [PATCH 320/430] doc(web): document progress queries --- web/src/queries/progress.ts | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/web/src/queries/progress.ts b/web/src/queries/progress.ts index d7fbfff5c2..1904c4a6a5 100644 --- a/web/src/queries/progress.ts +++ b/web/src/queries/progress.ts @@ -30,6 +30,14 @@ const servicesMap = { "org.opensuse.Agama.Storage1": "storage", }; +/** + * Returns a query for retrieving the progress information for a given service + * + * At this point, the services that implement the progress API are + * "manager", "software" and "storage". + * + * @param service - Service to retrieve the progress from (e.g., "manager") + */ const progressQuery = (service: string) => { return { queryKey: ["progress", service], @@ -40,17 +48,26 @@ const progressQuery = (service: string) => { }; }; -type UseProgressOptions = { - suspense: boolean; -}; - -const useProgress = (service: string, options?: QueryHookOptions): Progress => { +/** + * Hook that returns the progress for a given service + * + * @param service - Service to retrieve the progress from + * @param options - Query options + * @returns Progress information or undefined if unknown + */ +const useProgress = (service: string, options?: QueryHookOptions): Progress | undefined => { const query = progressQuery(service); const func = options?.suspense ? useSuspenseQuery : useQuery; const { data } = func(query); return data; }; +/** + * Hook that registers a useEffect to listen for progress changes + * + * It listens for all progress changes but updates only existing + * progress queries. + */ const useProgressChanges = () => { const client = useInstallerClient(); const queryClient = useQueryClient(); @@ -78,6 +95,12 @@ const useProgressChanges = () => { }, [client, queryClient]); }; +/** + * Hook that invalidates all the existing queries. + * + * It offers a way to clear previously cached progress information. It is expected to + * be used before starting to display the progress. + */ const useResetProgress = () => { const queryClient = useQueryClient(); From 3605496aba5a8c5947be0fab0b29b584975faf73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 29 Jul 2024 09:56:42 +0100 Subject: [PATCH 321/430] fix(web): specify a missing dependency in useResetProgress --- web/src/queries/progress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/queries/progress.ts b/web/src/queries/progress.ts index 1904c4a6a5..7820c3be9c 100644 --- a/web/src/queries/progress.ts +++ b/web/src/queries/progress.ts @@ -108,7 +108,7 @@ const useResetProgress = () => { return () => { queryClient.invalidateQueries({ queryKey: ["progress"] }); }; - }, []); + }, [queryClient]); }; export { useProgress, useProgressChanges, useResetProgress, progressQuery }; From 6f0a306e7d7a1066188ef9ea4d9ee29e06ea83cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 29 Jul 2024 09:58:11 +0100 Subject: [PATCH 322/430] fix(web): clear progress information first --- web/src/components/core/ProgressReport.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index 9791d8dd49..bd5c2ae7ba 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -113,11 +113,11 @@ function findDetail(progresses) { * Shows progress steps when a product is selected. */ function ProgressReport({ title, firstStep }) { + useResetProgress(); const progress = useProgress("manager", { suspense: true }); const [steps, setSteps] = useState(progress.steps); const softwareProgress = useProgress("software"); const storageProgress = useProgress("storage"); - useResetProgress(); useProgressChanges(); useEffect(() => { From 0bcc846bcbfd1be5dc930532583ced5218d0341b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 29 Jul 2024 11:33:18 +0200 Subject: [PATCH 323/430] Require at least 8GB RAM for building the Rust package in OBS Require at least 4 parallel jobs (the build on S390 takes ages with just 2 jobs...) --- rust/package/_constraints | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rust/package/_constraints b/rust/package/_constraints index d16e04ec51..2a52ed7f2b 100644 --- a/rust/package/_constraints +++ b/rust/package/_constraints @@ -1,7 +1,12 @@ + 4 20 + + 8 + + SLOW_CPU From 9aac6048aa458d7be17c49155babbaeb51c605a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 29 Jul 2024 12:55:52 +0100 Subject: [PATCH 324/430] refactor(web): move LogsButton to TypeScript --- ...ogsButton.test.jsx => LogsButton.test.tsx} | 31 ++++++++++--------- .../core/{LogsButton.jsx => LogsButton.tsx} | 23 ++++++-------- 2 files changed, 27 insertions(+), 27 deletions(-) rename web/src/components/core/{LogsButton.test.jsx => LogsButton.test.tsx} (83%) rename web/src/components/core/{LogsButton.jsx => LogsButton.tsx} (92%) diff --git a/web/src/components/core/LogsButton.test.jsx b/web/src/components/core/LogsButton.test.tsx similarity index 83% rename from web/src/components/core/LogsButton.test.jsx rename to web/src/components/core/LogsButton.test.tsx index eab60e1c44..77af69c247 100644 --- a/web/src/components/core/LogsButton.test.jsx +++ b/web/src/components/core/LogsButton.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -32,13 +32,14 @@ const originalCreateElement = document.createElement; const executor = jest.fn(); const fetchLogsFn = jest.fn(); -beforeEach(() => { +beforeAll(() => { + jest.spyOn(console, "error").mockImplementation(); window.URL.createObjectURL = jest.fn(() => "fake-blob-url"); window.URL.revokeObjectURL = jest.fn(); fetchLogsFn.mockImplementation(() => new Promise(executor)); - createClient.mockImplementation(() => { + (createClient as jest.Mock).mockImplementation(() => { return { manager: { fetchLogs: fetchLogsFn, @@ -47,22 +48,22 @@ beforeEach(() => { }); }); -afterEach(() => { - jest.restoreAllMocks(); - window.URL.createObjectURL.mockRestore(); - window.URL.revokeObjectURL.mockRestore(); +afterAll(() => { + jest.restoreAllMocks(); // <-- it restore all spies + (window.URL.createObjectURL as jest.Mock).mockRestore(); + (window.URL.revokeObjectURL as jest.Mock).mockRestore(); }); describe("LogsButton", () => { it("renders a button for downloading logs", () => { installerRender(); - screen.getByRole("button", "Download logs"); + screen.getByRole("button", { name: "Download logs" }); }); describe("when user clicks on it", () => { it("inits download logs process", async () => { const { user } = installerRender(); - const button = screen.getByRole("button", "Download logs"); + const button = screen.getByRole("button", { name: "Download logs" }); await user.click(button); expect(fetchLogsFn).toHaveBeenCalled(); }); @@ -70,7 +71,7 @@ describe("LogsButton", () => { it("changes button text, puts it as disabled, and displays an informative alert", async () => { const { user } = installerRender(); - const button = screen.getByRole("button", "Download logs"); + const button = screen.getByRole("button", { name: "Download logs" }); expect(button).not.toHaveAttribute("disabled"); await user.click(button); @@ -102,18 +103,20 @@ describe("LogsButton", () => { // since its used internally by jsdom. Simply spying it is not enough because we want to // mock only the call to the HTMLAnchorElement creation that happens when user clicks on the // "Download logs". - document._createElement = document.createElement; + // @ts-expect-error + document.originalCreateElement = originalCreateElement; const anchorMock = document.createElement("a"); anchorMock.setAttribute = jest.fn(); anchorMock.click = jest.fn(); jest.spyOn(document, "createElement").mockImplementation((tag) => { - return tag === "a" ? anchorMock : document._createElement(tag); + // @ts-expect-error + return tag === "a" ? anchorMock : document.originalCreateElement(tag); }); // Now, let's simulate the "Download logs" user click - const button = screen.getByRole("button", "Download logs"); + const button = screen.getByRole("button", { name: "Download logs" }); await user.click(button); // And test what we're looking for @@ -140,7 +143,7 @@ describe("LogsButton", () => { it("displays a warning alert along with the Download logs button", async () => { const { user } = installerRender(); - const button = screen.getByRole("button", "Download logs"); + const button = screen.getByRole("button", { name: "Download logs" }); expect(button).not.toHaveAttribute("disabled"); await user.click(button); diff --git a/web/src/components/core/LogsButton.jsx b/web/src/components/core/LogsButton.tsx similarity index 92% rename from web/src/components/core/LogsButton.jsx rename to web/src/components/core/LogsButton.tsx index c3315cab54..01e2bef0b3 100644 --- a/web/src/components/core/LogsButton.jsx +++ b/web/src/components/core/LogsButton.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -20,26 +20,22 @@ */ import React, { useState } from "react"; -import { useInstallerClient } from "~/context/installer"; -import { useCancellablePromise } from "~/utils"; - -import { Alert, Button } from "@patternfly/react-core"; +import { Alert, Button, ButtonProps } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { _ } from "~/i18n"; +import { useInstallerClient } from "~/context/installer"; +import { useCancellablePromise } from "~/utils"; const FILENAME = "agama-installation-logs.tar.gz"; /** - * Button for collecting and downloading YaST logs - * @component - * - * @param {object} props + * Button for collecting and downloading Agama/YaST logs */ -const LogsButton = ({ ...props }) => { +const LogsButton = (props: ButtonProps) => { const client = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); - const [isCollecting, setIsCollecting] = useState(false); const [error, setError] = useState(null); + const [isCollecting, setIsCollecting] = useState(false); /** * Helper function for triggering the download automatically @@ -49,7 +45,7 @@ const LogsButton = ({ ...props }) => { * * @param {string} url - the file location to download from */ - const autoDownload = (url) => { + const autoDownload = (url: string) => { const a = document.createElement("a"); a.href = url; a.download = FILENAME; @@ -91,7 +87,8 @@ const LogsButton = ({ ...props }) => { return ( <> - - - - - ); -}; - -const Sidebar = () => { - // TODO: Improve this and/or extract the NavItem to a wrapper component. - const links = rootRoutes().map((r) => { - if (!r.handle || r.handle.hidden) return null; - - // eslint-disable-next-line agama-i18n/string-literals - const name = _(r.handle?.name); - - return ( - ( - [className, isActive ? "pf-m-current" : ""].join(" ")} - > - {name} - - )} - /> - ); - }); - - return ( - - - - - - - - - - ); -}; - -/** - * Root application component for laying out the content. - */ -export default function Root() { - return ( - } sidebar={}> - }> - - - - ); -} diff --git a/web/src/assets/styles/global.scss b/web/src/assets/styles/global.scss index 0a0faf460e..98cfaa3635 100644 --- a/web/src/assets/styles/global.scss +++ b/web/src/assets/styles/global.scss @@ -44,6 +44,14 @@ button.pf-m-link { } } +.pf-v5-c-page__sidebar { + button.pf-m-link, + a.pf-m-link { + color: white; + text-decoration: underline; + } +} + // Do not reserve space for empty nodes. div:empty { display: none; diff --git a/web/src/components/core/LoginPage.jsx b/web/src/components/core/LoginPage.jsx index 5ef18ed4e4..4a008904f9 100644 --- a/web/src/components/core/LoginPage.jsx +++ b/web/src/components/core/LoginPage.jsx @@ -115,12 +115,7 @@ user privileges.", - + diff --git a/web/src/components/core/Page.tsx b/web/src/components/core/Page.tsx index 9ee3bfa528..8e7c0ffe19 100644 --- a/web/src/components/core/Page.tsx +++ b/web/src/components/core/Page.tsx @@ -20,7 +20,7 @@ */ import React from "react"; -import { NavLink, Outlet, useNavigate } from "react-router-dom"; +import { NavLink, useNavigate } from "react-router-dom"; import { Button, ButtonProps, diff --git a/web/src/components/layout/Header.test.tsx b/web/src/components/layout/Header.test.tsx new file mode 100644 index 0000000000..b620a83aab --- /dev/null +++ b/web/src/components/layout/Header.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import Header from "./Header"; + +const tumbleweed = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + description: "Tumbleweed description...", +}; +const microos = { + id: "MicroOS", + name: "openSUSE MicroOS", + description: "MicroOS description", +}; + +jest.mock("~/components/core/InstallerOptions", () => () =>
      Installer Options Mock
      ); + +jest.mock("~/queries/software", () => ({ + useProduct: () => ({ + products: [tumbleweed, microos], + selectedProduct: tumbleweed, + }), +})); + +describe("Header", () => { + it("renders the product name unless mount with hideProductName prop", () => { + const { rerender } = installerRender(
      ); + screen.getByRole("heading", { name: tumbleweed.name, level: 1 }); + rerender(
      ); + screen.getByRole("heading", { name: tumbleweed.name, level: 1 }); + rerender(
      ); + expect(screen.queryByRole("heading", { name: tumbleweed.name, level: 1 })).toBeNull(); + }); + + it("renders the installer options unless mount with hideInstallerOptions prop", () => { + const { rerender } = installerRender(
      ); + screen.getByText("Installer Options Mock"); + rerender(
      ); + screen.getByText("Installer Options Mock"); + rerender(
      ); + expect(screen.queryByText("Installer Options Mock")).toBeNull(); + }); +}); diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx new file mode 100644 index 0000000000..40b3e63462 --- /dev/null +++ b/web/src/components/layout/Header.tsx @@ -0,0 +1,80 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { + Masthead, + MastheadContent, + MastheadToggle, + MastheadMain, + MastheadBrand, + PageToggleButton, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from "@patternfly/react-core"; +import { Icon } from "~/components/layout"; +import { InstallerOptions } from "~/components/core"; +import { useProduct } from "~/queries/software"; +import { _ } from "~/i18n"; + +/** + * Internal component for building the layout header + * + * It's just a wrapper for {@link https://www.patternfly.org/components/masthead | PF/Masthead} and + * its expected children components. + */ +export default function Header({ + hideProductName = false, + hideInstallerOptions = false, +}: { + hideProductName?: boolean; + hideInstallerOptions?: boolean; +}): React.ReactNode { + const { selectedProduct } = useProduct(); + + return ( + + + + + + + + {hideProductName || {selectedProduct.name}} + + + + + + {hideInstallerOptions || } + + + + + + ); +} diff --git a/web/src/components/layout/Main.tsx b/web/src/components/layout/Main.tsx new file mode 100644 index 0000000000..d3b04aecea --- /dev/null +++ b/web/src/components/layout/Main.tsx @@ -0,0 +1,39 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { Suspense } from "react"; +import { Outlet } from "react-router-dom"; +import { Page } from "@patternfly/react-core"; +import { Header, Loading, Sidebar } from "~/components/layout"; +import { _ } from "~/i18n"; + +/** + * Wrapper application component for laying out the content. + */ +export default function Main() { + return ( + } sidebar={}> + }> + + + + ); +} diff --git a/web/src/components/layout/Sidebar.test.tsx b/web/src/components/layout/Sidebar.test.tsx new file mode 100644 index 0000000000..486420a1d2 --- /dev/null +++ b/web/src/components/layout/Sidebar.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import Sidebar from "./Sidebar"; + +jest.mock("~/components/core/About", () => () =>
      About Mock
      ); +jest.mock("~/components/core/LogsButton", () => () =>
      LogsButton Mock
      ); +jest.mock("~/components/core/ChangeProductLink", () => () =>
      ChangeProductLink Mock
      ); + +jest.mock("~/router", () => ({ + rootRoutes: () => [ + { path: "/", handle: { name: "Main" } }, + { path: "/l10n", handle: { name: "L10n" } }, + { path: "/hidden" }, + ], +})); + +describe("Sidebar", () => { + it("renders a navigation on top of root routes with handle object", () => { + installerRender(); + const mainNavigation = screen.getByRole("navigation"); + const mainNavigationLinks = within(mainNavigation).getAllByRole("link"); + expect(mainNavigationLinks.length).toBe(2); + screen.getByRole("link", { name: "Main" }); + screen.getByRole("link", { name: "L10n" }); + }); + + it("mounts core/About component", () => { + installerRender(); + screen.getByText("About Mock"); + }); + + it("mounts core/LogsButton component", () => { + installerRender(); + screen.getByText("LogsButton Mock"); + }); + + it("mounts core/ChangeProductLink component", () => { + installerRender(); + screen.getByText("ChangeProductLink Mock"); + }); +}); diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000000..dd1b94fc3c --- /dev/null +++ b/web/src/components/layout/Sidebar.tsx @@ -0,0 +1,75 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { NavLink } from "react-router-dom"; +import { Nav, NavItem, NavList, PageSidebar, PageSidebarBody, Stack } from "@patternfly/react-core"; +import { Icon } from "~/components/layout"; +import { About, LogsButton, ChangeProductLink } from "~/components/core"; +import { rootRoutes } from "~/router"; +import { _ } from "~/i18n"; + +const MainNavigation = (): React.ReactNode => { + const links = rootRoutes().map((r) => { + if (!r.handle) return null; + + // eslint-disable-next-line agama-i18n/string-literals + const name = _(r.handle.name); + const iconName = r.handle.icon; + + return ( + ( + [className, isActive ? "pf-m-current" : ""].join(" ")} + > + {iconName && } {name} + + )} + /> + ); + }); + + return ( + + ); +}; + +export default function Sidebar(): React.ReactNode { + return ( + + + + + + + + + + + + + ); +} diff --git a/web/src/components/layout/index.js b/web/src/components/layout/index.js index d9e095f2fa..3834bcc3fb 100644 --- a/web/src/components/layout/index.js +++ b/web/src/components/layout/index.js @@ -22,3 +22,6 @@ export { default as Icon } from "./Icon"; export { default as Center } from "./Center"; export { default as Loading } from "./Loading"; +export { default as Sidebar } from "./Sidebar"; +export { default as Header } from "./Header"; +export { default as Main } from "./Main"; diff --git a/web/src/router.js b/web/src/router.js index 10bf2e2f43..91cdd8809f 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -23,7 +23,7 @@ import React from "react"; import { createHashRouter } from "react-router-dom"; import App from "~/App"; import Protected from "~/Protected"; -import MainLayout from "~/MainLayout"; +import MainLayout from "~/components/layout/Main"; import SimpleLayout from "./SimpleLayout"; import { LoginPage } from "~/components/core"; import { OverviewPage } from "~/components/overview"; From 166e61dc2da64e419f5ebaaa6cb3e38e81f5431c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 29 Jul 2024 13:45:57 +0100 Subject: [PATCH 328/430] fix(web): add missing wrapper For wrapping the Loading component with SimpleLayout. Related to 888830a23c92c8cc85a143b345f4070aed4c5ce2 --- web/src/App.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/App.jsx b/web/src/App.jsx index c0293f25c6..5ddfced24a 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -32,6 +32,7 @@ import { BUSY } from "~/client/status"; import { useL10nConfigChanges } from "~/queries/l10n"; import { useIssuesChanges } from "./queries/issues"; import { PATHS as PRODUCT_PATHS } from "./routes/products"; +import SimpleLayout from "./SimpleLayout"; /** * Main application component. @@ -56,7 +57,12 @@ function App() { return ; } - if (!products || !connected) return ; + if (!products || !connected) + return ( + + + + ); if ((phase === STARTUP && status === BUSY) || phase === undefined || status === undefined) { return ; From 117df89c5dad5554e7f52eabbef0cdf328fd2086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 29 Jul 2024 13:47:08 +0100 Subject: [PATCH 329/430] refator(web): move ButtonLink component to TypeScript Although it could be drop in a future. --- .../components/core/{ButtonLink.jsx => ButtonLink.tsx} | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) rename web/src/components/core/{ButtonLink.jsx => ButtonLink.tsx} (89%) diff --git a/web/src/components/core/ButtonLink.jsx b/web/src/components/core/ButtonLink.tsx similarity index 89% rename from web/src/components/core/ButtonLink.jsx rename to web/src/components/core/ButtonLink.tsx index 87492b79a9..6031085014 100644 --- a/web/src/components/core/ButtonLink.jsx +++ b/web/src/components/core/ButtonLink.tsx @@ -19,8 +19,6 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; import { Link } from "react-router-dom"; import buttonStyles from "@patternfly/react-styles/css/components/Button/button"; @@ -32,10 +30,9 @@ export default function ButtonLink({ to, isPrimary = false, children, ...props } return ( {children} From a53f0070117970584c947f4df4d073d87f714bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 29 Jul 2024 13:47:56 +0100 Subject: [PATCH 330/430] fix(web) drop leftover import --- web/src/components/product/ProductSelectionPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/product/ProductSelectionPage.jsx b/web/src/components/product/ProductSelectionPage.jsx index d3e08cf97a..696e5da979 100644 --- a/web/src/components/product/ProductSelectionPage.jsx +++ b/web/src/components/product/ProductSelectionPage.jsx @@ -22,7 +22,7 @@ import React, { useState } from "react"; import { Card, CardBody, Flex, Form, Grid, GridItem, Radio } from "@patternfly/react-core"; import { Page } from "~/components/core"; -import { Loading, Center } from "~/components/layout"; +import { Center } from "~/components/layout"; import { useConfigMutation, useProduct } from "~/queries/software"; import { _ } from "~/i18n"; import styles from "@patternfly/react-styles/css/utilities/Text/text"; From dad71beffc601839a3ff74e916d305c28333522f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 30 Jul 2024 09:12:09 +0100 Subject: [PATCH 331/430] fix(web): add missing option to tsconfig file --- web/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/tsconfig.json b/web/tsconfig.json index 804a30f02b..cef27da6b5 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "baseUrl": "./", + "outDir": "dist/", "isolatedModules": true, "target": "esnext", "moduleResolution": "node", From afef47b21071d7b3addad1dd6c8c6f8d641843a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 11:08:54 +0100 Subject: [PATCH 332/430] feat(rust): expose whether the manager is busy or not * Do not expose the list of busy services anymore. --- rust/agama-server/src/manager/web.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/agama-server/src/manager/web.rs b/rust/agama-server/src/manager/web.rs index 4dc7e352e8..79d87c4f2c 100644 --- a/rust/agama-server/src/manager/web.rs +++ b/rust/agama-server/src/manager/web.rs @@ -43,10 +43,10 @@ pub struct ManagerState<'a> { pub struct InstallerStatus { /// Current installation phase. phase: InstallationPhase, - /// List of busy services. - busy: Vec, + /// Whether the service is busy. + is_busy: bool, /// Whether Agama is running on Iguana. - iguana: bool, + use_iguana: bool, /// Whether it is possible to start the installation. can_install: bool, } @@ -183,8 +183,8 @@ async fn installer_status( let status = InstallerStatus { phase, can_install, - busy: state.manager.busy_services().await?, - iguana: state.manager.use_iguana().await?, + is_busy: state.manager.is_busy().await, + use_iguana: state.manager.use_iguana().await?, }; Ok(Json(status)) } From 7751a0c38f3256f4cb7027b6cf71677fcf930b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 11:21:05 +0100 Subject: [PATCH 333/430] refactor(web): use queries to track installer status --- web/src/App.jsx | 12 ++- web/src/App.test.jsx | 19 ++-- web/src/components/core/Installation.jsx | 5 +- .../components/core/InstallationFinished.jsx | 9 +- .../core/InstallationFinished.test.jsx | 6 +- .../product/ProductSelectionProgress.jsx | 11 +-- web/src/queries/status.ts | 91 +++++++++++++++++++ web/src/types/status.ts | 45 +++++++++ 8 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 web/src/queries/status.ts create mode 100644 web/src/types/status.ts diff --git a/web/src/App.jsx b/web/src/App.jsx index 5ddfced24a..ccf524b9d8 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -28,9 +28,9 @@ import { useInstallerL10n } from "./context/installerL10n"; import { useInstallerClientStatus } from "~/context/installer"; import { useProduct, useProductChanges } from "./queries/software"; import { CONFIG, INSTALL, STARTUP } from "~/client/phase"; -import { BUSY } from "~/client/status"; import { useL10nConfigChanges } from "~/queries/l10n"; import { useIssuesChanges } from "./queries/issues"; +import { useInstallerStatus, useInstallerStatusChanges } from "./queries/status"; import { PATHS as PRODUCT_PATHS } from "./routes/products"; import SimpleLayout from "./SimpleLayout"; @@ -43,18 +43,20 @@ import SimpleLayout from "./SimpleLayout"; */ function App() { const location = useLocation(); - const { connected, error, phase, status } = useInstallerClientStatus(); + const { isBusy, phase } = useInstallerStatus({ suspense: true }); + const { connected, error } = useInstallerClientStatus(); const { selectedProduct, products } = useProduct(); const { language } = useInstallerL10n(); useL10nConfigChanges(); useProductChanges(); useIssuesChanges(); + useInstallerStatusChanges(); const Content = () => { if (error) return ; if (phase === INSTALL) { - return ; + return ; } if (!products || !connected) @@ -64,7 +66,7 @@ function App() { ); - if ((phase === STARTUP && status === BUSY) || phase === undefined || status === undefined) { + if (phase === STARTUP && isBusy) { return ; } @@ -72,7 +74,7 @@ function App() { return ; } - if (phase === CONFIG && status === BUSY && location.pathname !== PRODUCT_PATHS.progress) { + if (phase === CONFIG && isBusy && location.pathname !== PRODUCT_PATHS.progress) { return ; } diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index a4a4b0d96d..f799c342ce 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -26,7 +26,6 @@ import { installerRender } from "~/test-utils"; import App from "./App"; import { createClient } from "~/client"; import { STARTUP, CONFIG, INSTALL } from "~/client/phase"; -import { IDLE, BUSY } from "~/client/status"; import { useL10nConfigChanges } from "./queries/l10n"; import { useProductChanges } from "./queries/software"; import { useIssuesChanges } from "./queries/issues"; @@ -59,15 +58,19 @@ jest.mock("~/queries/issues", () => ({ })); const mockClientStatus = { - connected: true, - error: false, phase: STARTUP, - status: BUSY, + isBusy: true, }; +jest.mock("~/queries/status", () => ({ + ...jest.requireActual("~/queries/status"), + useInstallerStatus: () => mockClientStatus, + useInstallerStatusChanges: () => jest.fn(), +})); + jest.mock("~/context/installer", () => ({ ...jest.requireActual("~/context/installer"), - useInstallerClientStatus: () => mockClientStatus, + useInstallerClientStatus: () => ({ connected: true, error: false }), })); // Mock some components, @@ -116,7 +119,7 @@ describe("App", () => { describe("when the service is busy during startup", () => { beforeEach(() => { mockClientStatus.phase = STARTUP; - mockClientStatus.status = BUSY; + mockClientStatus.isBusy = true; }); it("renders the Loading screen", async () => { @@ -132,7 +135,7 @@ describe("App", () => { describe("if the service is busy", () => { beforeEach(() => { - mockClientStatus.status = BUSY; + mockClientStatus.isBusy = true; mockSelectedProduct = { id: "Tumbleweed" }; }); @@ -144,7 +147,7 @@ describe("App", () => { describe("if the service is not busy", () => { beforeEach(() => { - mockClientStatus.status = IDLE; + mockClientStatus.isBusy = false; }); it("renders the application content", async () => { diff --git a/web/src/components/core/Installation.jsx b/web/src/components/core/Installation.jsx index abd7504560..59f8f5bd9d 100644 --- a/web/src/components/core/Installation.jsx +++ b/web/src/components/core/Installation.jsx @@ -21,10 +21,9 @@ import React from "react"; import { InstallationProgress, InstallationFinished } from "~/components/core"; -import { IDLE } from "~/client/status"; -function Installation({ status }) { - return status === IDLE ? : ; +function Installation({ isBusy }) { + return isBusy ? : ; } export default Installation; diff --git a/web/src/components/core/InstallationFinished.jsx b/web/src/components/core/InstallationFinished.jsx index 4f37f8a57f..2b02d6935e 100644 --- a/web/src/components/core/InstallationFinished.jsx +++ b/web/src/components/core/InstallationFinished.jsx @@ -42,6 +42,7 @@ import { EncryptionMethods } from "~/client/storage"; import { _ } from "~/i18n"; import { useInstallerClient } from "~/context/installer"; import alignmentStyles from "@patternfly/react-styles/css/utilities/Alignment/alignment"; +import { useInstallerStatus } from "~/queries/status"; const TpmHint = () => { const [isExpanded, setIsExpanded] = useState(false); @@ -74,19 +75,17 @@ const SuccessIcon = () => - {usingIguana + {useIguana ? _("At this point you can power off the machine.") : _( "At this point you can reboot the machine to log in to the new system.", @@ -127,7 +126,7 @@ function InstallationFinished() { diff --git a/web/src/components/core/InstallationFinished.test.jsx b/web/src/components/core/InstallationFinished.test.jsx index c6901c4981..51a3d27f89 100644 --- a/web/src/components/core/InstallationFinished.test.jsx +++ b/web/src/components/core/InstallationFinished.test.jsx @@ -25,9 +25,13 @@ import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { createClient } from "~/client"; import { EncryptionMethods } from "~/client/storage"; - import InstallationFinished from "./InstallationFinished"; +jest.mock("~/queries/status", () => ({ + ...jest.requireActual("~/queries/status"), + useInstallerStatus: () => ({ isBusy: false, useIguana: false, phase: 2, canInstall: false }), +})); + jest.mock("~/client"); jest.mock("~/components/core/InstallerOptions", () => () =>
      Installer Options
      ); diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.jsx index 9b1c93e2b1..d654f7b5ac 100644 --- a/web/src/components/product/ProductSelectionProgress.jsx +++ b/web/src/components/product/ProductSelectionProgress.jsx @@ -27,6 +27,7 @@ import { Page, ProgressReport } from "~/components/core"; import { IDLE } from "~/client/status"; import { useInstallerClient } from "~/context/installer"; import { PATHS } from "~/router"; +import { useInstallerStatus } from "~/queries/status"; /** * @component @@ -35,15 +36,9 @@ import { PATHS } from "~/router"; */ function ProductSelectionProgress() { const { selectedProduct } = useProduct({ suspense: true }); - const { manager } = useInstallerClient(); - const [status, setStatus] = useState(); + const { isBusy } = useInstallerStatus({ suspense: true }); - useEffect(() => { - manager.getStatus().then(setStatus); - return manager.onStatusChange(setStatus); - }, [manager, setStatus]); - - if (status === IDLE) return ; + if (!isBusy) return ; return ( diff --git a/web/src/queries/status.ts b/web/src/queries/status.ts new file mode 100644 index 0000000000..d5e391e8ae --- /dev/null +++ b/web/src/queries/status.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import { useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import React from "react"; +import { useInstallerClient } from "~/context/installer"; +import { InstallerStatus } from "~/types/status"; + +const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; + +/** + * Returns a query for retrieving the installer status + */ +const statusQuery = () => ({ + queryKey: ["status"], + queryFn: (): Promise => + fetch(`/api/manager/installer`) + .then((res) => res.json()) + .then((body) => { + const { phase, isBusy, useIguana, canInstall } = body; + return { phase, isBusy, useIguana, canInstall }; + }), +}); + +/** + * Hook that returns the installer status + * + * @param options - Query options + */ +const useInstallerStatus = (options?: QueryHookOptions): InstallerStatus | undefined => { + const query = statusQuery(); + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func(query); + return data; +}; + +/** + * Hook that registers a useEffect to listen for status changes + * + * It listens for all status changes but updates the query only + * if it is already cached. + */ +const useInstallerStatusChanges = () => { + const client = useInstallerClient(); + const queryClient = useQueryClient(); + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + const { type } = event; + const data = queryClient.getQueryData(["status"]) as object; + if (!data) { + return; + } + + if (type === "InstallationPhaseChanged") { + const { phase } = event; + queryClient.setQueryData(["status"], { ...data, phase }); + } + + if (type === "ServiceStatusChanged" && event.service === MANAGER_SERVICE) { + const { status } = event; + queryClient.setQueryData(["status"], { ...data, busy: status === 1 }); + } + + if (type === "IssuesChanged") { + queryClient.invalidateQueries({ queryKey: ["status"] }); + } + }); + }); +}; + +export { useInstallerStatus, useInstallerStatusChanges }; diff --git a/web/src/types/status.ts b/web/src/types/status.ts new file mode 100644 index 0000000000..898037bf55 --- /dev/null +++ b/web/src/types/status.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +/* + * Enum that represents the installation phase + */ +enum InstallationPhase { + Startup = 0, + Config = 1, + Install = 2, +} + +/* + * Status of the installer + */ +type InstallerStatus = { + /** Whether the installer is busy */ + isBusy: boolean; + /** Installation phase */ + phase: InstallationPhase; + /** Whether the installation can be performed or not */ + canInstall: boolean; + /** Whether the installer is running on Iguana */ + useIguana: boolean; +}; + +export type { InstallerStatus }; From a04d9f1a92a7517fe7286d536601c113e95faf4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 11:40:06 +0100 Subject: [PATCH 334/430] refactor(web): drop the old code to handle the installer's state --- web/src/client/manager.js | 77 +------------------------- web/src/client/manager.test.js | 42 -------------- web/src/context/installer.jsx | 31 +---------- web/src/context/installer.test.jsx | 12 +--- web/src/context/installerL10n.test.jsx | 6 -- 5 files changed, 4 insertions(+), 164 deletions(-) diff --git a/web/src/client/manager.js b/web/src/client/manager.js index 2c4a50113b..313cf46f6b 100644 --- a/web/src/client/manager.js +++ b/web/src/client/manager.js @@ -21,16 +21,10 @@ // @ts-check -import { WithStatus } from "./mixins"; - -const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; - /** - * Manager base client - * - * @ignore + * Client to interact with the Agama manager service */ -class ManagerBaseClient { +class ManagerClient { /** * @param {import("./http").HTTPClient} client - HTTP client. */ @@ -60,24 +54,6 @@ class ManagerBaseClient { return this.client.post("/manager/install", {}); } - /** - * Checks whether it is possible to start the installation - * - * It might happen that there are some validation errors. In that case, - * it is not possible to proceed with the installation. - * - * @return {Promise} - */ - async canInstall() { - const response = await this.client.get("/manager/installer"); - if (!response.ok) { - console.error("Failed to get installer config: ", response); - return false; - } - const installer = await response.json(); - return installer.canInstall; - } - /** * Returns the binary content of the YaST logs file * @@ -93,61 +69,12 @@ class ManagerBaseClient { return response; } - /** - * Return the installer status - * - * @return {Promise} - */ - async getPhase() { - const response = await this.client.get("/manager/installer"); - if (!response.ok) { - console.error("Failed to get installer config: ", response); - return 0; - } - const installer = await response.json(); - return installer.phase; - } - - /** - * Register a callback to run when the "CurrentInstallationPhase" changes - * - * @param {function} handler - callback function - * @return {import ("./dbus").RemoveFn} function to disable the callback - */ - onPhaseChange(handler) { - return this.client.onEvent("InstallationPhaseChanged", ({ phase }) => { - if (phase) { - handler(phase); - } - }); - } - /** * Runs cleanup when installation is done */ finishInstallation() { return this.client.post("/manager/finish", {}); } - - /** - * Returns whether Iguana is used on the system - * - * @return {Promise} - */ - async useIguana() { - const response = await this.client.get("/manager/installer"); - if (!response.ok) { - console.error("Failed to get installer config: ", response); - return false; - } - const installer = await response.json(); - return installer.iguana; - } } -/** - * Client to interact with the Agama manager service - */ -class ManagerClient extends WithStatus(ManagerBaseClient, "/manager/status", MANAGER_SERVICE) {} - export { ManagerClient }; diff --git a/web/src/client/manager.test.js b/web/src/client/manager.test.js index ab5cd66ae0..3e1363978e 100644 --- a/web/src/client/manager.test.js +++ b/web/src/client/manager.test.js @@ -46,13 +46,6 @@ jest.mock("./http", () => { let client; -const installerStatus = { - phase: 1, - busy: [], - iguana: false, - canInstall: true, -}; - beforeEach(() => { client = new ManagerClient(new HTTPClient(new URL("http://localhost"))); }); @@ -64,17 +57,6 @@ describe("#startProbing", () => { }); }); -describe("#getPhase", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(installerStatus); - }); - - it("resolves to the current phase", () => { - const phase = client.getPhase(); - expect(phase).resolves.toEqual(1); - }); -}); - describe("#startInstallation", () => { it("starts the installation", async () => { await client.startInstallation(); @@ -93,30 +75,6 @@ describe("#rebootSystem", () => { }); }); -describe("#canInstall", () => { - describe("when the system can be installed", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(installerStatus); - }); - - it("returns true", async () => { - const install = await client.canInstall(); - expect(install).toEqual(true); - }); - }); - - describe("when the system cannot be installed", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue({ ...installerStatus, canInstall: false }); - }); - - it("returns false", async () => { - const install = await client.canInstall(); - expect(install).toEqual(false); - }); - }); -}); - describe.skip("#fetchLogs", () => { // beforeEach(() => { // managerProxy.CollectLogs = jest.fn(() => "/tmp/y2log-hWBn95.tar.xz"); diff --git a/web/src/context/installer.jsx b/web/src/context/installer.jsx index 0b4b4c8246..2cbec5ad18 100644 --- a/web/src/context/installer.jsx +++ b/web/src/context/installer.jsx @@ -30,8 +30,6 @@ const InstallerClientContext = React.createContext(null); const InstallerClientStatusContext = React.createContext({ connected: false, error: false, - phase: undefined, - status: undefined, }); /** @@ -80,8 +78,6 @@ function InstallerClientProvider({ children, client = null }) { const [value, setValue] = useState(client); const [connected, setConnected] = useState(false); const [error, setError] = useState(false); - const [status, setStatus] = useState(undefined); - const [phase, setPhase] = useState(undefined); useEffect(() => { const connectClient = async () => { @@ -104,31 +100,6 @@ function InstallerClientProvider({ children, client = null }) { if (!value) connectClient(); }, [setValue, value]); - useEffect(() => { - if (value) { - return value.manager.onPhaseChange(setPhase); - } - }, [value, setPhase]); - - useEffect(() => { - if (value) { - return value.manager.onStatusChange(setStatus); - } - }, [value, setStatus]); - - useEffect(() => { - const loadPhase = async () => { - const initialPhase = await value.manager.getPhase(); - const initialStatus = await value.manager.getStatus(); - setPhase(initialPhase); - setStatus(initialStatus); - }; - - if (value) { - loadPhase().catch(console.error); - } - }, [value, setPhase, setStatus]); - useEffect(() => { if (!value) return; @@ -145,7 +116,7 @@ function InstallerClientProvider({ children, client = null }) { return ( - + {children} diff --git a/web/src/context/installer.test.jsx b/web/src/context/installer.test.jsx index 0e025f2a82..83ad6daa64 100644 --- a/web/src/context/installer.test.jsx +++ b/web/src/context/installer.test.jsx @@ -31,13 +31,11 @@ jest.mock("~/client"); // Helper component to check the client status. const ClientStatus = () => { - const { connected, phase, status } = useInstallerClientStatus(); + const { connected } = useInstallerClientStatus(); return (
      • {`connected: ${connected}`}
      • -
      • {`phase: ${phase}`}
      • -
      • {`status: ${status}`}
      ); }; @@ -48,12 +46,6 @@ describe("installer context", () => { return { onConnect: jest.fn(), onDisconnect: jest.fn(), - manager: { - getPhase: jest.fn().mockResolvedValue(STARTUP), - getStatus: jest.fn().mockResolvedValue(BUSY), - onPhaseChange: jest.fn(), - onStatusChange: jest.fn(), - }, }; }); }); @@ -65,7 +57,5 @@ describe("installer context", () => { , ); await screen.findByText("connected: false"); - await screen.findByText("phase: 0"); - await screen.findByText("status: 1"); }); }); diff --git a/web/src/context/installerL10n.test.jsx b/web/src/context/installerL10n.test.jsx index 7b47d3f140..50af75fa4b 100644 --- a/web/src/context/installerL10n.test.jsx +++ b/web/src/context/installerL10n.test.jsx @@ -35,12 +35,6 @@ const setUILocaleFn = jest.fn().mockResolvedValue(); const client = { onConnect: jest.fn(), onDisconnect: jest.fn(), - manager: { - getPhase: jest.fn(), - getStatus: jest.fn(), - onPhaseChange: jest.fn(), - onStatusChange: jest.fn(), - }, l10n: { getUILocale: getUILocaleFn, setUILocale: setUILocaleFn, From 85432825142bd71f3c23315eccd81df11bdaa78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 11:41:13 +0100 Subject: [PATCH 335/430] chore(web): clean-up --- web/src/components/overview/SoftwareSection.test.tsx | 2 +- web/src/queries/users.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/components/overview/SoftwareSection.test.tsx b/web/src/components/overview/SoftwareSection.test.tsx index eb02e06e78..d476b71799 100644 --- a/web/src/components/overview/SoftwareSection.test.tsx +++ b/web/src/components/overview/SoftwareSection.test.tsx @@ -32,7 +32,7 @@ let mockTestingProposal: SoftwareProposal; jest.mock("~/queries/software", () => ({ usePatterns: () => mockTestingPatterns, useProposal: () => mockTestingProposal, - useProposalChanges: jest.fn(), + useProposalChanges: () => jest.fn(), })); describe("SoftwareSection", () => { diff --git a/web/src/queries/users.ts b/web/src/queries/users.ts index 2404810de8..8946268478 100644 --- a/web/src/queries/users.ts +++ b/web/src/queries/users.ts @@ -151,7 +151,6 @@ const useRootUserChanges = () => { if (!client) return; return client.ws().onEvent((event) => { - console.log("event.type", event.type); if (event.type === "RootChanged") { const { password, sshkey } = event; queryClient.setQueryData(["users", "root"], (oldRoot: RootUser) => { From 49af49278a1fd3aaa4dbe1ba9f0b2cdca2607277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 12:12:39 +0100 Subject: [PATCH 336/430] refactor(rust): drop the unused BusyServicesChanged event --- rust/agama-server/src/web/event.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 87a4c94787..5b11eae12b 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -49,9 +49,6 @@ pub enum Event { InstallationPhaseChanged { phase: InstallationPhase, }, - BusyServicesChanged { - services: Vec, - }, ServiceStatusChanged { service: String, status: u32, From ae8724d744c57ff6598549d76eed2035d256bd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 30 Jul 2024 14:49:05 +0200 Subject: [PATCH 337/430] Improve printing the Agama URL in the console - Use the NetworkManager backend - Hide Avahi URL if network is not configured - Display only configured interfaces - Refresh the URLs when a device is changed (or connected/disconnected) - Display a warning if the network is not available --- .../systemd/system/agama-url-issue.service | 11 ++ live/root/usr/bin/agama-issue-generator | 136 ++++++++++++------ .../udev/rules.d/80-agama-connect-issue.rules | 5 - live/src/config.sh | 1 + 4 files changed, 101 insertions(+), 52 deletions(-) create mode 100644 live/root/etc/systemd/system/agama-url-issue.service delete mode 100644 live/root/usr/lib/udev/rules.d/80-agama-connect-issue.rules diff --git a/live/root/etc/systemd/system/agama-url-issue.service b/live/root/etc/systemd/system/agama-url-issue.service new file mode 100644 index 0000000000..0a6bfc1530 --- /dev/null +++ b/live/root/etc/systemd/system/agama-url-issue.service @@ -0,0 +1,11 @@ +[Unit] +Description=Generate issue file for Agama URLs + +After=network-online.target + +[Service] +ExecStart=agama-issue-generator --watch-network +Type=simple + +[Install] +WantedBy=default.target diff --git a/live/root/usr/bin/agama-issue-generator b/live/root/usr/bin/agama-issue-generator index 0f4b8dc357..2377f26142 100755 --- a/live/root/usr/bin/agama-issue-generator +++ b/live/root/usr/bin/agama-issue-generator @@ -8,8 +8,9 @@ # - Welcome message with Agama version number (--welcome option) # - Agama SSL certificate fingerprints (--ssl option) # - SSH host key fingerprints (--ssh option) -# - Agama access URL for all network devices (--network option), this is -# triggered via udev rules +# - Agama access URL for all network devices (--watch-network option) +# NOTE: in this case the script does not finish, it watches the changes in +# the NetworkManager configuration and updates the URL if needed # - Agama access URL using the mDNS (Avahi) URL (--watch-avahi option), # NOTE: in this case the script does not finish, it watches the changes in # the Avahi service and updates the URL if needed @@ -64,11 +65,46 @@ generate_certificate_fingerprints() { touch /run/agetty.reload } +# message file for the Agama mDNS URL +AVAHI_MESSAGE=/run/issue.d/70-agama-connect-avahi.message +# symlink for the issue file, created only when network is available +AVAHI_ISSUE=/run/issue.d/70-agama-connect-avahi.issue +# issue file with Agama URLs +URL_ISSUES="/run/issue.d/70-agama-connect-urls.issue" +# issue displayed when there is no network connection +DISCONNECTED_ISSUE="/run/issue.d/70-agama-disconnected.issue" + +# helper function, build the Agama URL messages or display a warning when +# network is not available +write_url_headers() { + # generate a header and footer around the Agama URL issues + ISSUE_HEADER=/run/issue.d/69-agama-connect.issue + ISSUE_FOOTER=/run/issue.d/71-agama-connect.issue + + if [ -e "$URL_ISSUES" ]; then + # if Avahi URL is set then display it as well + if [ -e "$AVAHI_MESSAGE" ]; then + ln -sf "$AVAHI_MESSAGE" "$AVAHI_ISSUE" + fi + + rm -f "$DISCONNECTED_ISSUE" + + # at least one address present, display the header and footer + echo "Connect to the Agama installer using these URLs:" > "$ISSUE_HEADER" + echo > "$ISSUE_FOOTER" + else + # no network, delete the header, footer and the Avahi issue symlink + rm -f "$ISSUE_HEADER" "$ISSUE_FOOTER" "$AVAHI_ISSUE" + + # display a warning message + printf "\\\\e{brown}Network is not available, the Agama installer cannot \ +be used remotely.\\\\e{reset}\n\n" > "$DISCONNECTED_ISSUE" + fi +} + # a helper function which generates the mDNS URL for accessing the Agama server # displayed at the console generate_avahi_url() { - # issue file for the Agama mDNS URL - ISSUE=/run/issue.d/70-agama-connect-avahi.issue # track the name, update the issue file only if the name is changed OLDNAME="" @@ -84,7 +120,8 @@ generate_avahi_url() { # mDNS host name found and it is different than the previous one (or the initial value) if [ -n "$AVAHINAME" ] && [ "$AVAHINAME" != "$OLDNAME" ]; then OLDNAME="$AVAHINAME" - echo " https://$AVAHINAME" > "$ISSUE" + echo " https://$AVAHINAME" > "$AVAHI_MESSAGE" + write_url_headers # reload if not in the initial state if [ -e "$CERT_ISSUE" ]; then @@ -92,64 +129,69 @@ generate_avahi_url() { fi fi - # daemon stopped, remove the issue file + # daemon stopped, remove the message file if echo "$line" | grep -q "avahi-daemon .* exiting"; then OLDNAME="" - rm -f "$ISSUE" + rm -f "$AVAHI_MESSAGE" + write_url_headers touch /run/agetty.reload fi done } -# a helper function which generates the URLs for accessing the Agama server -# displayed at the console -generate_network_url() { - # the interface might be a device path, use the base name - if [[ "$2" =~ ^/ ]]; then - IFACE="${2##*/}" - else - IFACE="${2}" - fi +# helper function, write the issue with the currently available URLs for +# accessing Agama from outside +build_addresses() { + ADDRESSES=() - ACTION="$1" - ISSUE="/run/issue.d/70-agama-connect-$IFACE.issue" - # generate a header and footer around the Agama URL issues - ISSUE_HEADER=/run/issue.d/69-agama-connect.issue - ISSUE_FOOTER=/run/issue.d/71-agama-connect.issue + readarray -t CONNECTIONS < <(busctl -j get-property org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.NetworkManager ActiveConnections | jq --raw-output ".data[]") + for CONNECTION in "${CONNECTIONS[@]}"; do + TYPE=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Type 2> /dev/null | jq --raw-output ".data") - # only handle interfaces starting with ^[bew] - # (bridges, ethernet and wifi devices), same as in the issue-generator - [[ "$IFACE" =~ ^[bew] ]] || exit 0 - - case "$ACTION" in - add) - # \4{} is a placeholder supported directly by the agetty issue reader - # see "man agetty" - echo " https://\\4{$IFACE} " > "$ISSUE" - ;; - remove) - rm -f "$ISSUE" - ;; - esac - - # check the number of URL messages - ISSUES=$(ls /run/issue.d/70-agama-connect-*.issue 2> /dev/null) - if [ -n "$ISSUES" ]; then - # at least one message present, display the header and footer - echo "Connect to the Agama installer using these URLs:" > "$ISSUE_HEADER" - echo > "$ISSUE_FOOTER" + # ignore loopbacks, we need external adresses + if [ "$TYPE" != "loopback" ]; then + IP4CONFIG=$(busctl -j get-property org.freedesktop.NetworkManager "$CONNECTION" org.freedesktop.NetworkManager.Connection.Active Ip4Config 2> /dev/null | jq --raw-output ".data") + ADDRESSES+=($(busctl -j get-property org.freedesktop.NetworkManager "$IP4CONFIG" org.freedesktop.NetworkManager.IP4Config AddressData 2> /dev/null | jq --raw-output ".data[].address.data")) + fi + done + + # remove duplicates + readarray -t ADDRESSES < <(printf "%s\n" "${ADDRESSES[@]}" | sort -u) + + if [ -n "${ADDRESSES[*]}" ]; then + printf " https://%s\n" "${ADDRESSES[@]}" > "$URL_ISSUES" else - # no messages, delete the header and footer - rm -f "$ISSUE_HEADER" "$ISSUE_FOOTER" + # no messages, delete the URLs + rm -f "$URL_ISSUES" fi + write_url_headers + # reload if not in the initial state if [ -e "$CERT_ISSUE" ]; then touch /run/agetty.reload fi } -# wait until the SSL fingreprint issue is create, but at most 10 seconds +# a helper function which generates the URLs for accessing the Agama server +# displayed at the console +generate_network_url() { + # build a message with the current URLs + build_addresses + + # watch for IP address changes in the NetworkManager service + dbus-monitor --system "sender='org.freedesktop.NetworkManager',\ + interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',\ + type=signal" 2> /dev/null | while read -r line; do + # some IP4 configuration has been changed, rebuild the URLs + if echo "$line" | grep -q 'string "org.freedesktop.NetworkManager.IP4Config"'; then + build_addresses + fi + done +} + +# wait until the SSL fingreprint issue is created with a time limit passed as +# the second argument (in seconds) wait_for_ssl_issue() { for i in $(seq 1 "$1"); do [ -f "$CERT_ISSUE" ] && exit 0 @@ -169,11 +211,11 @@ elif [ "$1" = "--ssl" ]; then generate_certificate_fingerprints elif [ "$1" = "--wait-for-ssl" ]; then wait_for_ssl_issue "$2" -elif [ "$1" = "--network" ]; then +elif [ "$1" = "--watch-network" ]; then generate_network_url "$2" "$3" elif [ "$1" = "--watch-avahi" ]; then generate_avahi_url else - echo "Missing argument" + echo "Missing or incorrect argument" exit 1 fi diff --git a/live/root/usr/lib/udev/rules.d/80-agama-connect-issue.rules b/live/root/usr/lib/udev/rules.d/80-agama-connect-issue.rules deleted file mode 100644 index 2ba3202a23..0000000000 --- a/live/root/usr/lib/udev/rules.d/80-agama-connect-issue.rules +++ /dev/null @@ -1,5 +0,0 @@ -# udev rules for generating the Agama access URLs displayed at the console -ACTION=="add", SUBSYSTEM=="net", RUN+="/usr/bin/agama-issue-generator --network add $env{INTERFACE}" -ACTION=="remove", SUBSYSTEM=="net", RUN+="/usr/bin/agama-issue-generator --network rm $env{INTERFACE}" -ACTION=="move", SUBSYSTEM=="net", RUN+="/usr/bin/agama-issue-generator --network add $env{INTERFACE}" -ACTION=="move", SUBSYSTEM=="net", RUN+="/usr/bin/agama-issue-generator --network rm $env{DEVPATH_OLD}" diff --git a/live/src/config.sh b/live/src/config.sh index 0dbca4aa74..c870e74103 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -43,6 +43,7 @@ systemctl enable agama-certificate-issue.path systemctl enable agama-certificate-wait.service systemctl enable agama-welcome-issue.service systemctl enable agama-avahi-issue.service +systemctl enable agama-url-issue.service systemctl enable agama-ssh-issue.service systemctl enable agama-self-update.service systemctl enable live-free-space.service From b5fe96e55861d58ba0effa67708c3385a22abdec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 14:22:12 +0100 Subject: [PATCH 338/430] refactor(web): clean-up unused clients --- web/src/client/index.js | 11 ------- web/src/client/monitor.js | 61 -------------------------------------- web/src/client/software.js | 59 ------------------------------------ 3 files changed, 131 deletions(-) delete mode 100644 web/src/client/monitor.js delete mode 100644 web/src/client/software.js diff --git a/web/src/client/index.js b/web/src/client/index.js index b1920f1f18..501feed83f 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -23,8 +23,6 @@ import { L10nClient } from "./l10n"; import { ManagerClient } from "./manager"; -import { Monitor } from "./monitor"; -import { ProductClient, SoftwareClient } from "./software"; import { StorageClient } from "./storage"; import phase from "./phase"; import { QuestionsClient } from "./questions"; @@ -35,10 +33,7 @@ import { HTTPClient, WSClient } from "./http"; * @typedef {object} InstallerClient * @property {L10nClient} l10n - localization client. * @property {ManagerClient} manager - manager client. - * property {Monitor} monitor - service monitor. (FIXME) * @property {NetworkClient} network - network client. - * @property {ProductClient} product - product client. - * @property {SoftwareClient} software - software client. * @property {StorageClient} storage - storage client. * @property {QuestionsClient} questions - questions client. * @property {() => WSClient} ws - Agama WebSocket client. @@ -60,11 +55,8 @@ const createClient = (url) => { const client = new HTTPClient(url); const l10n = new L10nClient(client); // TODO: unify with the manager client - const product = new ProductClient(client); const manager = new ManagerClient(client); - // const monitor = new Monitor(address, MANAGER_SERVICE); const network = new NetworkClient(client); - const software = new SoftwareClient(client); const storage = new StorageClient(client); const questions = new QuestionsClient(client); @@ -73,11 +65,8 @@ const createClient = (url) => { return { l10n, - product, manager, - // monitor, network, - software, storage, questions, isConnected, diff --git a/web/src/client/monitor.js b/web/src/client/monitor.js deleted file mode 100644 index a298f64f17..0000000000 --- a/web/src/client/monitor.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) [2022] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -import DBusClient from "./dbus"; - -const NAME_OWNER_CHANGED = { - interface: "org.freedesktop.DBus", - member: "NameOwnerChanged", -}; - -/** - * Monitor a D-Bus service - */ -class Monitor { - /** - * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. - * @param {string} serviceName - name of the service to monitor - */ - constructor(address, serviceName) { - this.serviceName = serviceName; - this.client = new DBusClient("org.freedesktop.DBus", address); - } - - /** - * Registers a callback to be executed when the D-Bus service connection changes - * - * @param {() => void} handler - function to execute when the client gets - * disconnected. - * @return {() => void} function to deregister the callbacks. - */ - onDisconnect(handler) { - return this.client.onSignal(NAME_OWNER_CHANGED, (_path, _interface, _signal, args) => { - const [service, , newOwner] = args; - if (service === this.serviceName && newOwner === "") { - handler(); - } - }); - } -} - -export { Monitor }; diff --git a/web/src/client/software.js b/web/src/client/software.js deleted file mode 100644 index 1e886bad56..0000000000 --- a/web/src/client/software.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -import { WithStatus } from "./mixins"; - -const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1"; - -/** - * Software client - * - * @ignore - */ -class SoftwareBaseClient { - /** - * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. - */ - /** - * @param {import("./http").HTTPClient} client - HTTP client. - */ - constructor(client) { - this.client = client; - } -} - -/** - * Manages software and product configuration. - */ -class SoftwareClient extends WithStatus(SoftwareBaseClient, "/software/status", SOFTWARE_SERVICE) {} - -class ProductClient { - /** - * @param {import("./http").HTTPClient} client - HTTP client. - */ - constructor(client) { - this.client = client; - } -} - -export { ProductClient, SoftwareClient }; From 435c36b0173ab9abcf985799bbd5233f1607e2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 14:23:29 +0100 Subject: [PATCH 339/430] refactor(web): clean-up manager mock --- web/src/test-utils.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/web/src/test-utils.js b/web/src/test-utils.js index 8452491f88..b296837e10 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -97,14 +97,6 @@ const Providers = ({ children, withL10n }) => { client.manager = {}; } - client.manager = { - getPhase: noop, - getStatus: noop, - onPhaseChange: noop, - onStatusChange: noop, - ...client.manager, - }; - if (withL10n) { return ( From 49353f756ee253d99a40c2572a4b18945f878588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 14:23:12 +0100 Subject: [PATCH 340/430] refactor(web): replace client/phase with InstallerPhase --- web/src/App.jsx | 12 ++++++++---- web/src/App.test.jsx | 11 ++++++----- web/src/client/index.js | 3 +-- web/src/context/installer.test.jsx | 2 -- web/src/types/status.ts | 1 + 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/web/src/App.jsx b/web/src/App.jsx index ccf524b9d8..877f94d305 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -27,12 +27,12 @@ import { ServerError, Installation } from "~/components/core"; import { useInstallerL10n } from "./context/installerL10n"; import { useInstallerClientStatus } from "~/context/installer"; import { useProduct, useProductChanges } from "./queries/software"; -import { CONFIG, INSTALL, STARTUP } from "~/client/phase"; import { useL10nConfigChanges } from "~/queries/l10n"; import { useIssuesChanges } from "./queries/issues"; import { useInstallerStatus, useInstallerStatusChanges } from "./queries/status"; import { PATHS as PRODUCT_PATHS } from "./routes/products"; import SimpleLayout from "./SimpleLayout"; +import { InstallationPhase } from "./types/status"; /** * Main application component. @@ -55,7 +55,7 @@ function App() { const Content = () => { if (error) return ; - if (phase === INSTALL) { + if (phase === InstallationPhase.Install) { return ; } @@ -66,7 +66,7 @@ function App() { ); - if (phase === STARTUP && isBusy) { + if (phase === InstallationPhase.Startup && isBusy) { return ; } @@ -74,7 +74,11 @@ function App() { return ; } - if (phase === CONFIG && isBusy && location.pathname !== PRODUCT_PATHS.progress) { + if ( + phase === InstallationPhase.Config && + isBusy && + location.pathname !== PRODUCT_PATHS.progress + ) { return ; } diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index f799c342ce..8659c7cc65 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -29,6 +29,7 @@ import { STARTUP, CONFIG, INSTALL } from "~/client/phase"; import { useL10nConfigChanges } from "./queries/l10n"; import { useProductChanges } from "./queries/software"; import { useIssuesChanges } from "./queries/issues"; +import { InstallationPhase } from "./types/status"; jest.mock("~/client"); @@ -118,7 +119,7 @@ describe("App", () => { describe("when the service is busy during startup", () => { beforeEach(() => { - mockClientStatus.phase = STARTUP; + mockClientStatus.phase = InstallationPhase.Startup; mockClientStatus.isBusy = true; }); @@ -128,9 +129,9 @@ describe("App", () => { }); }); - describe("on the CONFIG phase", () => { + describe("on the configuration phase", () => { beforeEach(() => { - mockClientStatus.phase = CONFIG; + mockClientStatus.phase = InstallationPhase.Config; }); describe("if the service is busy", () => { @@ -157,9 +158,9 @@ describe("App", () => { }); }); - describe("on the INSTALL phase", () => { + describe("on the installaiton phase", () => { beforeEach(() => { - mockClientStatus.phase = INSTALL; + mockClientStatus.phase = InstallationPhase.Install; mockSelectedProduct = { id: "Fake product" }; }); diff --git a/web/src/client/index.js b/web/src/client/index.js index 501feed83f..95185d56aa 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -24,7 +24,6 @@ import { L10nClient } from "./l10n"; import { ManagerClient } from "./manager"; import { StorageClient } from "./storage"; -import phase from "./phase"; import { QuestionsClient } from "./questions"; import { NetworkClient } from "./network"; import { HTTPClient, WSClient } from "./http"; @@ -83,4 +82,4 @@ const createDefaultClient = async () => { return createClient(httpUrl); }; -export { createClient, createDefaultClient, phase }; +export { createClient, createDefaultClient }; diff --git a/web/src/context/installer.test.jsx b/web/src/context/installer.test.jsx index 83ad6daa64..0c190518ef 100644 --- a/web/src/context/installer.test.jsx +++ b/web/src/context/installer.test.jsx @@ -24,8 +24,6 @@ import { act, screen } from "@testing-library/react"; import { createDefaultClient } from "~/client"; import { plainRender, createCallbackMock } from "~/test-utils"; import { InstallerClientProvider, useInstallerClientStatus } from "./installer"; -import { STARTUP } from "~/client/phase"; -import { BUSY } from "~/client/status"; jest.mock("~/client"); diff --git a/web/src/types/status.ts b/web/src/types/status.ts index 898037bf55..9174911c84 100644 --- a/web/src/types/status.ts +++ b/web/src/types/status.ts @@ -43,3 +43,4 @@ type InstallerStatus = { }; export type { InstallerStatus }; +export { InstallationPhase }; From ea42d31246e7a6edcf3b275fc2c78bfc6308156b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 14:47:17 +0100 Subject: [PATCH 341/430] fix(test): remove references to client/phase --- web/src/App.test.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index 8659c7cc65..a3a2a4ec51 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -25,10 +25,6 @@ import { installerRender } from "~/test-utils"; import App from "./App"; import { createClient } from "~/client"; -import { STARTUP, CONFIG, INSTALL } from "~/client/phase"; -import { useL10nConfigChanges } from "./queries/l10n"; -import { useProductChanges } from "./queries/software"; -import { useIssuesChanges } from "./queries/issues"; import { InstallationPhase } from "./types/status"; jest.mock("~/client"); @@ -59,7 +55,7 @@ jest.mock("~/queries/issues", () => ({ })); const mockClientStatus = { - phase: STARTUP, + phase: InstallationPhase.Startup, isBusy: true, }; From 30fb9737caed5e2e47aebf4da8f1e9e060c062b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 14:47:46 +0100 Subject: [PATCH 342/430] refactor(web): drop the unused client/phase --- web/src/client/phase.js | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 web/src/client/phase.js diff --git a/web/src/client/phase.js b/web/src/client/phase.js deleted file mode 100644 index 5024a61448..0000000000 --- a/web/src/client/phase.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) [2022] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -export const STARTUP = 0; -export const CONFIG = 1; -export const INSTALL = 2; - -export default { - STARTUP, - CONFIG, - INSTALL, -}; From 20c5694274a439728a6216e911ee4ac239ff4f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 30 Jul 2024 15:08:18 +0100 Subject: [PATCH 343/430] fix(web): fix the Installation component --- web/src/components/core/Installation.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/core/Installation.jsx b/web/src/components/core/Installation.jsx index 59f8f5bd9d..6cf7532681 100644 --- a/web/src/components/core/Installation.jsx +++ b/web/src/components/core/Installation.jsx @@ -23,7 +23,7 @@ import React from "react"; import { InstallationProgress, InstallationFinished } from "~/components/core"; function Installation({ isBusy }) { - return isBusy ? : ; + return isBusy ? : ; } export default Installation; From 7659dea65ba2b263e49c1a3acab37f158f535059 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 31 Jul 2024 13:39:28 +0200 Subject: [PATCH 344/430] BaseHTTPClient: fields made public, better constructor API, Default to enable - connecting to alternative servers, either production or test mocks - unauthenticated calls, either for the initial auth or test mocsk --- rust/agama-lib/src/base_http_client.rs | 37 +++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 655a1ce5d7..bc31c485ef 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -1,4 +1,4 @@ -use reqwest::{header, Client, Response}; +use reqwest::{header, Response}; use serde::{de::DeserializeOwned, Serialize}; use crate::{auth::AuthToken, error::ServiceError}; @@ -21,15 +21,36 @@ use crate::{auth::AuthToken, error::ServiceError}; /// } /// ``` pub struct BaseHTTPClient { - client: Client, + pub client: reqwest::Client, pub base_url: String, } const API_URL: &str = "http://localhost/api"; +impl Default for BaseHTTPClient { + /// A `default` client + /// - is NOT authenticated (maybe you want to call `new` instead) + /// - uses `localhost` + fn default() -> Self { + Self { + client: reqwest::Client::new(), + base_url: API_URL.to_owned(), + } + } +} + impl BaseHTTPClient { - // if there is need for client without authorization, create new constructor for it + /// Uses `localhost`, authenticates with [`AuthToken`]. pub fn new() -> Result { + Ok(Self { + client: Self::authenticated_reqwest_client()?, + ..Default::default() + }) + } + + fn authenticated_reqwest_client() -> Result { + // TODO: this error is subtly misleading, leading me to believe the SERVER said it, + // but in fact it is the CLIENT not finding an auth token let token = AuthToken::find().ok_or(ServiceError::NotAuthenticated)?; let mut headers = header::HeaderMap::new(); @@ -39,12 +60,10 @@ impl BaseHTTPClient { headers.insert(header::AUTHORIZATION, value); - let client = Client::builder().default_headers(headers).build()?; - - Ok(Self { - client, - base_url: API_URL.to_string(), // TODO: add support for remote server - }) + let client = reqwest::Client::builder() + .default_headers(headers) + .build()?; + Ok(client) } /// Simple wrapper around [`Response`] to get object from response. From 809b50dd6ec8a256ba245dec708bc7f75e1f8edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 30 Jul 2024 12:34:57 +0100 Subject: [PATCH 345/430] refactor(web): add basic types for questions --- web/src/types/questions.ts | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 web/src/types/questions.ts diff --git a/web/src/types/questions.ts b/web/src/types/questions.ts new file mode 100644 index 0000000000..85c88fe984 --- /dev/null +++ b/web/src/types/questions.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +/** + * Enum for question types + */ +enum QuestionType { + generic = "generic", + withPassword = "withPassword", +} + +type Question = { + id: number; + type?: QuestionType; + class?: string; + options?: string[]; + defaultOption?: string; + text?: string; + data?: { [key: string]: string }; + answer?: string; + password?: string; +}; + +type Answer = { + generic?: { answer: string }; + withPassword?: { password: string }; +}; + +type AnswerCallback = (answeredQuestion: Question) => void; + +export { QuestionType }; +export type { Answer, AnswerCallback, Question }; From 4d67bb4c8655440ce8dd828c69535561b6a9d174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 30 Jul 2024 12:35:22 +0100 Subject: [PATCH 346/430] refactor(web): add questions queries --- web/src/queries/questions.ts | 119 +++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 web/src/queries/questions.ts diff --git a/web/src/queries/questions.ts b/web/src/queries/questions.ts new file mode 100644 index 0000000000..118fd0e029 --- /dev/null +++ b/web/src/queries/questions.ts @@ -0,0 +1,119 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useInstallerClient } from "~/context/installer"; +import { Answer, Question, QuestionType } from "~/types/questions"; + +type APIQuestion = { + generic?: Question; + withPassword?: Pick; +}; + +/** + * Internal method to build proper question objects + * + * TODO: improve/simplify it once the backend API is improved. + */ +function buildQuestion(httpQuestion: APIQuestion) { + let question: Question; + + if (httpQuestion.generic) { + question = { + ...httpQuestion.generic, + type: QuestionType.generic, + answer: httpQuestion.generic.answer, + }; + } + + if (httpQuestion.withPassword) { + question = { + id: httpQuestion.generic.id, + type: QuestionType.withPassword, + password: httpQuestion.withPassword.password, + }; + } + + return question; +} + +/** + * Query to retrieve questions + */ +const questionsQuery = () => ({ + queryKey: ["questions"], + queryFn: () => fetch("/api/questions").then((res) => res.json()), +}); + +/** + * Hook that builds a mutation given question, allowing to answer it + + * TODO: improve/simplify it once the backend API is improved. + */ +const useQuestionsConfig = () => { + const query = { + mutationFn: (question: Question) => { + const answer: Answer = { generic: { answer: question.answer } }; + + if (question.type === QuestionType.withPassword) { + answer.withPassword = { password: question.password }; + } + + return fetch(`/api/questions/${question.id}/answer`, { + method: "PATCH", + body: JSON.stringify(answer), + headers: { + "Content-Type": "application/json", + }, + }); + }, + }; + return useMutation(query); +}; + +/** + * Hook for listening questions changes and performing proper invalidations + */ +const useQuestionsChanges = () => { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + if (event.type === "QuestionsChanged") { + queryClient.invalidateQueries({ queryKey: ["questions"] }); + } + }); + }, [client, queryClient]); +}; + +/** + * Hook for retrieving available questions + */ +const useQuestions = () => { + const { data, isPending } = useQuery(questionsQuery()); + return isPending ? [] : data.map(buildQuestion); +}; + +export { questionsQuery, useQuestions, useQuestionsConfig, useQuestionsChanges }; From 150bd517f3fd8d97fdaaeaf88993b1e6b8643971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 30 Jul 2024 12:35:46 +0100 Subject: [PATCH 347/430] refactor(web): adapt question components to queries --- .../questions/GenericQuestion.test.jsx | 4 +- .../questions/LuksActivationQuestion.test.jsx | 4 +- .../questions/QuestionWithPassword.test.jsx | 4 +- web/src/components/questions/Questions.jsx | 61 +++------ .../components/questions/Questions.test.jsx | 125 +++++++++--------- 5 files changed, 82 insertions(+), 116 deletions(-) diff --git a/web/src/components/questions/GenericQuestion.test.jsx b/web/src/components/questions/GenericQuestion.test.jsx index ef3f3f176d..6573af614f 100644 --- a/web/src/components/questions/GenericQuestion.test.jsx +++ b/web/src/components/questions/GenericQuestion.test.jsx @@ -21,7 +21,7 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; +import { plainRender } from "~/test-utils"; import { GenericQuestion } from "~/components/questions"; const question = { @@ -34,7 +34,7 @@ const question = { const answerFn = jest.fn(); const renderQuestion = () => - installerRender(); + plainRender(); describe("GenericQuestion", () => { it("renders the question text", async () => { diff --git a/web/src/components/questions/LuksActivationQuestion.test.jsx b/web/src/components/questions/LuksActivationQuestion.test.jsx index 6bee9e3754..8c56815873 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.jsx +++ b/web/src/components/questions/LuksActivationQuestion.test.jsx @@ -21,14 +21,14 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; +import { plainRender } from "~/test-utils"; import { LuksActivationQuestion } from "~/components/questions"; let question; const answerFn = jest.fn(); const renderQuestion = () => - installerRender(); + plainRender(); describe("LuksActivationQuestion", () => { beforeEach(() => { diff --git a/web/src/components/questions/QuestionWithPassword.test.jsx b/web/src/components/questions/QuestionWithPassword.test.jsx index c9831d72d0..bedcbb3cb1 100644 --- a/web/src/components/questions/QuestionWithPassword.test.jsx +++ b/web/src/components/questions/QuestionWithPassword.test.jsx @@ -49,12 +49,12 @@ describe("QuestionWithPassword", () => { }); describe("when the user enters the password", () => { - it("calls the callback", async () => { + it("calls the callback with given password", async () => { const { user } = renderQuestion(); const passwordInput = await screen.findByLabelText("Password"); await user.type(passwordInput, "notSecret"); - const skipButton = await screen.findByRole("button", { name: /Ok/ }); + const skipButton = await screen.findByRole("button", { name: "Ok" }); await user.click(skipButton); expect(question).toEqual(expect.objectContaining({ password: "notSecret", answer: "ok" })); diff --git a/web/src/components/questions/Questions.jsx b/web/src/components/questions/Questions.jsx index 4280a4cf6b..1da02c97a6 100644 --- a/web/src/components/questions/Questions.jsx +++ b/web/src/components/questions/Questions.jsx @@ -19,73 +19,42 @@ * find current contact information at www.suse.com. */ -import React, { useCallback, useEffect, useState } from "react"; -import { useInstallerClient } from "~/context/installer"; -import { useCancellablePromise } from "~/utils"; -import { QUESTION_TYPES } from "~/client/questions"; +// @ts-check +import React from "react"; import { GenericQuestion, QuestionWithPassword, LuksActivationQuestion, } from "~/components/questions"; +import { useQuestions, useQuestionsConfig, useQuestionsChanges } from "~/queries/questions"; +import { Question, QuestionType } from "~/types/questions"; export default function Questions() { - const client = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - - const [pendingQuestions, setPendingQuestions] = useState([]); - - const addQuestion = useCallback((question) => { - setPendingQuestions((pending) => [...pending, question]); - }, []); - - const removeQuestion = useCallback( - (id) => setPendingQuestions((pending) => pending.filter((q) => q.id !== id)), - [], - ); - - const answerQuestion = useCallback( - (question) => { - client.questions.answer(question); - removeQuestion(question.id); - }, - [client.questions, removeQuestion], - ); - - useEffect(() => { - client.questions.listenQuestions(); - }, [client.questions, cancellablePromise]); - - useEffect(() => { - cancellablePromise(client.questions.getQuestions()) - .then(setPendingQuestions) - .catch((e) => console.error("Something went wrong retrieving pending questions", e)); - }, [client.questions, cancellablePromise]); - - useEffect(() => { - const unsubscribeCallbacks = []; - unsubscribeCallbacks.push(client.questions.onQuestionAdded(addQuestion)); - unsubscribeCallbacks.push(client.questions.onQuestionRemoved(removeQuestion)); - - return () => { - unsubscribeCallbacks.forEach((cb) => cb()); - }; - }, [client.questions, addQuestion, removeQuestion]); + useQuestionsChanges(); + const pendingQuestions = useQuestions(); + const questionsConfig = useQuestionsConfig(); if (pendingQuestions.length === 0) return null; + const answerQuestion = (/** @type {Question} */ answeredQuestion) => + questionsConfig.mutate(answeredQuestion); + // Renders the first pending question const [currentQuestion] = pendingQuestions; + let QuestionComponent = GenericQuestion; + // show specialized popup for question which need password - if (currentQuestion.type === QUESTION_TYPES.withPassword) { + if (currentQuestion.type === QuestionType.withPassword) { QuestionComponent = QuestionWithPassword; } + // show specialized popup for luks activation question // more can follow as it will be needed if (currentQuestion.class === "storage.luks_activation") { QuestionComponent = LuksActivationQuestion; } + return ; } diff --git a/web/src/components/questions/Questions.test.jsx b/web/src/components/questions/Questions.test.jsx index e951a853f9..02a2459d58 100644 --- a/web/src/components/questions/Questions.test.jsx +++ b/web/src/components/questions/Questions.test.jsx @@ -20,107 +20,104 @@ */ import React from "react"; - -import { act, waitFor, within } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; -import { createClient } from "~/client"; - +import { screen } from "@testing-library/react"; +import { installerRender, plainRender } from "~/test-utils"; import { Questions } from "~/components/questions"; +import { QuestionType } from "~/types/questions"; +import * as GenericQuestionComponent from "~/components/questions/GenericQuestion"; + +let mockQuestions; +const mockMutation = jest.fn(); -jest.mock("~/client"); -jest.mock("~/components/questions/GenericQuestion", () => () =>
      A Generic question mock
      ); jest.mock("~/components/questions/LuksActivationQuestion", () => () => (
      A LUKS activation question mock
      )); +jest.mock("~/components/questions/QuestionWithPassword", () => () => ( +
      A question with password mock
      +)); -const handlers = {}; -const genericQuestion = { id: 1, type: "generic" }; +jest.mock("~/queries/questions", () => ({ + ...jest.requireActual("~/queries/software"), + useQuestions: () => mockQuestions, + useQuestionsChanges: () => jest.fn(), + useQuestionsConfig: () => ({ mutate: mockMutation }), +})); + +const genericQuestion = { + id: 1, + type: QuestionType.generic, + text: "Do you write unit tests?", + options: ["always", "sometimes", "never"], + defaultOption: "sometimes", +}; +const passwordQuestion = { id: 1, type: QuestionType.withPassword }; const luksActivationQuestion = { id: 1, class: "storage.luks_activation" }; -let pendingQuestions = []; - -beforeEach(() => { - createClient.mockImplementation(() => { - return { - questions: { - getQuestions: () => Promise.resolve(pendingQuestions), - // Capture the handler for the onQuestionAdded signal for triggering it manually - onQuestionAdded: (onAddHandler) => { - handlers.onAdd = onAddHandler; - return jest.fn; - }, - // Capture the handler for the onQuestionREmoved signal for triggering it manually - onQuestionRemoved: (onRemoveHandler) => { - handlers.onRemove = onRemoveHandler; - return jest.fn; - }, - listenQuestions: jest.fn(), - }, - }; - }); -}); describe("Questions", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + describe("when there are no pending questions", () => { beforeEach(() => { - pendingQuestions = []; + mockQuestions = []; }); - it("renders nothing", async () => { - const { container } = installerRender(); - await waitFor(() => expect(container).toBeEmptyDOMElement()); + it("renders nothing", () => { + const { container } = plainRender(); + expect(container).toBeEmptyDOMElement(); }); }); - describe("when a new question is added", () => { - it("push it into the pending queue", async () => { - const { container } = installerRender(); - await waitFor(() => expect(container).toBeEmptyDOMElement()); - - // Manually triggers the handler given for the onQuestionAdded signal - act(() => handlers.onAdd(genericQuestion)); + describe("when a question is answered", () => { + beforeEach(() => { + mockQuestions = [genericQuestion]; + }); - await within(container).findByText("A Generic question mock"); + it("triggers the useQuestionMutationk", async () => { + const { user } = plainRender(); + const button = screen.getByRole("button", { name: "Always" }); + await user.click(button); + expect(mockMutation).toHaveBeenCalledWith({ ...genericQuestion, answer: "always" }); }); }); - describe("when a question is removed", () => { + describe("when there is a generic question pending", () => { beforeEach(() => { - pendingQuestions = [genericQuestion]; + mockQuestions = [genericQuestion]; + // Not using jest.mock at the top like for the other question components + // because the original implementation was needed for testing that + // mutation is triggered when proceed. + jest + .spyOn(GenericQuestionComponent, "default") + .mockReturnValue(
      A generic question mock
      ); }); - it("removes it from the queue", async () => { - const { container } = installerRender(); - await within(container).findByText("A Generic question mock"); - - // Manually triggers the handler given for the onQuestionRemoved signal - act(() => handlers.onRemove(genericQuestion.id)); - - const content = within(container).queryByText("A Generic question mock"); - expect(content).toBeNull(); + it("renders a GenericQuestion component", () => { + plainRender(); + screen.getByText("A generic question mock"); }); }); describe("when there is a generic question pending", () => { beforeEach(() => { - pendingQuestions = [genericQuestion]; + mockQuestions = [passwordQuestion]; }); - it("renders a GenericQuestion component", async () => { - const { container } = installerRender(); - - await within(container).findByText("A Generic question mock"); + it("renders a QuestionWithPassword component", () => { + plainRender(); + screen.getByText("A question with password mock"); }); }); describe("when there is a LUKS activation question pending", () => { beforeEach(() => { - pendingQuestions = [luksActivationQuestion]; + mockQuestions = [luksActivationQuestion]; }); - it("renders a LuksActivationQuestion component", async () => { - const { container } = installerRender(); - - await within(container).findByText("A LUKS activation question mock"); + it("renders a LuksActivationQuestion component", () => { + installerRender(); + screen.getByText("A LUKS activation question mock"); }); }); }); From f42aa70d5c72d3683726920d139b80b505e3072c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 30 Jul 2024 12:36:13 +0100 Subject: [PATCH 348/430] refactor(web): drop questions client Since it has been replaced by queries. --- web/src/client/index.js | 4 - web/src/client/questions.js | 153 ------------------------------- web/src/client/questions.test.js | 124 ------------------------- 3 files changed, 281 deletions(-) delete mode 100644 web/src/client/questions.js delete mode 100644 web/src/client/questions.test.js diff --git a/web/src/client/index.js b/web/src/client/index.js index b1920f1f18..e3ca0a3010 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -27,7 +27,6 @@ import { Monitor } from "./monitor"; import { ProductClient, SoftwareClient } from "./software"; import { StorageClient } from "./storage"; import phase from "./phase"; -import { QuestionsClient } from "./questions"; import { NetworkClient } from "./network"; import { HTTPClient, WSClient } from "./http"; @@ -40,7 +39,6 @@ import { HTTPClient, WSClient } from "./http"; * @property {ProductClient} product - product client. * @property {SoftwareClient} software - software client. * @property {StorageClient} storage - storage client. - * @property {QuestionsClient} questions - questions client. * @property {() => WSClient} ws - Agama WebSocket client. * @property {() => boolean} isConnected - determines whether the client is connected * @property {() => boolean} isRecoverable - determines whether the client is recoverable after disconnected @@ -66,7 +64,6 @@ const createClient = (url) => { const network = new NetworkClient(client); const software = new SoftwareClient(client); const storage = new StorageClient(client); - const questions = new QuestionsClient(client); const isConnected = () => client.ws().isConnected() || false; const isRecoverable = () => !!client.ws().isRecoverable(); @@ -79,7 +76,6 @@ const createClient = (url) => { network, software, storage, - questions, isConnected, isRecoverable, onConnect: (handler) => client.ws().onOpen(handler), diff --git a/web/src/client/questions.js b/web/src/client/questions.js deleted file mode 100644 index d2e822c238..0000000000 --- a/web/src/client/questions.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -const QUESTION_TYPES = { - generic: "generic", - withPassword: "withPassword", -}; - -/** - * @param {Object} httpQuestion - * @return {Object} - */ -function buildQuestion(httpQuestion) { - let question = {}; - if (httpQuestion.generic) { - question.type = QUESTION_TYPES.generic; - question = { ...httpQuestion.generic, type: QUESTION_TYPES.generic }; - question.answer = httpQuestion.generic.answer; - } - - if (httpQuestion.withPassword) { - question.type = QUESTION_TYPES.withPassword; - question.password = httpQuestion.withPassword.password; - } - - return question; -} - -/** - * Questions client - */ -class QuestionsClient { - /** - * @param {import("./http").HTTPClient} client - HTTP client. - */ - constructor(client) { - this.client = client; - this.listening = false; - this.questionIds = []; - this.handlers = { - added: [], - removed: [], - }; - } - - /** - * Return all the questions - * - * @return {Promise>} - */ - async getQuestions() { - const response = await this.client.get("/questions"); - if (!response.ok) { - console.warn("Failed to get questions: ", response); - return []; - } - const questions = await response.json(); - return questions.map(buildQuestion); - } - - /** - * Answer with the information in the given question - * - * @param {Object} question - */ - answer(question) { - const answer = { generic: { answer: question.answer } }; - if (question.type === QUESTION_TYPES.withPassword) { - answer.withPassword = { password: question.password }; - } - - const path = `/questions/${question.id}/answer`; - return this.client.put(path, answer); - } - - /** - * Register a callback to run when a questions is added - * - * @param {function} handler - callback function - * @return {function} function to unsubscribe - */ - onQuestionAdded(handler) { - this.handlers.added.push(handler); - - return () => { - const position = this.handlers.added.indexOf(handler); - if (position > -1) this.handlers.added.splice(position, 1); - }; - } - - /** - * Register a callback to run when a questions is removed - * - * @param {function} handler - callback function - * @return {function} function to unsubscribe - */ - onQuestionRemoved(handler) { - this.handlers.removed.push(handler); - - return () => { - const position = this.handlers.removed.indexOf(handler); - if (position > -1) this.handlers.removed.splice(position, 1); - }; - } - - async listenQuestions() { - if (this.listening) return; - - this.listening = true; - this.getQuestions().then((qs) => { - this.questionIds = qs.map((q) => q.id); - }); - return this.client.onEvent("QuestionsChanged", () => { - this.getQuestions().then((qs) => { - const updatedIds = qs.map((q) => q.id); - - const newQuestions = qs.filter((q) => !this.questionIds.includes(q.id)); - newQuestions.forEach((q) => { - this.handlers.added.forEach((f) => f(q)); - }); - - const removed = this.questionIds.filter((id) => !updatedIds.includes(id)); - removed.forEach((id) => { - this.handlers.removed.forEach((f) => f(id)); - }); - - this.questionIds = updatedIds; - }); - }); - } -} - -export { QUESTION_TYPES, QuestionsClient }; diff --git a/web/src/client/questions.test.js b/web/src/client/questions.test.js deleted file mode 100644 index 21835aced5..0000000000 --- a/web/src/client/questions.test.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { HTTPClient } from "./http"; -import { QuestionsClient } from "./questions"; - -const mockJsonFn = jest.fn(); -const mockGetFn = jest.fn().mockImplementation(() => { - return { ok: true, json: mockJsonFn }; -}); -const mockPutFn = jest.fn().mockImplementation(() => { - return { ok: true }; -}); - -jest.mock("./http", () => { - return { - HTTPClient: jest.fn().mockImplementation(() => { - return { - get: mockGetFn, - put: mockPutFn, - onEvent: jest.fn(), - }; - }), - }; -}); - -let client; - -const expectedQuestions = [ - { - id: 432, - class: "storage.luks_activation", - type: "withPassword", - text: "The device /dev/vdb1 (2.00 GiB) is encrypted. Do you want to decrypt it?", - options: ["skip", "decrypt"], - defaultOption: "decrypt", - answer: "", - data: { Attempt: "1" }, - password: "", - }, -]; - -const luksQuestion = { - generic: { - id: 432, - class: "storage.luks_activation", - text: "The device /dev/vdb1 (2.00 GiB) is encrypted. Do you want to decrypt it?", - options: ["skip", "decrypt"], - defaultOption: "decrypt", - data: { Attempt: "1" }, - answer: "", - }, - withPassword: { password: "" }, -}; - -describe("#getQuestions", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue([luksQuestion]); - client = new QuestionsClient(new HTTPClient(new URL("http://localhost"))); - }); - - it("returns pending questions", async () => { - const questions = await client.getQuestions(); - expect(mockGetFn).toHaveBeenCalledWith("/questions"); - expect(questions).toEqual(expectedQuestions); - }); -}); - -describe("#answer", () => { - let question; - - beforeEach(() => { - question = { id: 321, type: "whatever", answer: "the-answer" }; - }); - - it("sets given answer", async () => { - client = new QuestionsClient(new HTTPClient(new URL("http://localhost"))); - await client.answer(question); - - expect(mockPutFn).toHaveBeenCalledWith("/questions/321/answer", { - generic: { answer: "the-answer" }, - }); - }); - - describe("when answering a question implementing the LUKS activation interface", () => { - beforeEach(() => { - question = { - id: 432, - type: "withPassword", - class: "storage.luks_activation", - answer: "decrypt", - password: "notSecret", - }; - }); - - it("sets given password", async () => { - client = new QuestionsClient(new HTTPClient(new URL("http://localhost"))); - await client.answer(question); - - expect(mockPutFn).toHaveBeenCalledWith("/questions/432/answer", { - generic: { answer: "decrypt" }, - withPassword: { password: "notSecret" }, - }); - }); - }); -}); From 560f953e9fac707688718fbddf8bf3e9dc249be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 31 Jul 2024 13:41:35 +0100 Subject: [PATCH 349/430] refactor(web): migrate components/questions to TypeScript --- ...tion.test.jsx => GenericQuestion.test.tsx} | 5 +- ...enericQuestion.jsx => GenericQuestion.tsx} | 19 +++++-- ...st.jsx => LuksActivationQuestion.test.tsx} | 53 ++++++------------- ...uestion.jsx => LuksActivationQuestion.tsx} | 28 +++++++--- ...ions.test.jsx => QuestionActions.test.tsx} | 5 +- ...uestionActions.jsx => QuestionActions.tsx} | 29 ++++++---- ...test.jsx => QuestionWithPassword.test.tsx} | 20 +++---- ...hPassword.jsx => QuestionWithPassword.tsx} | 19 +++++-- ...{Questions.test.jsx => Questions.test.tsx} | 12 ++--- .../{Questions.jsx => Questions.tsx} | 10 ++-- .../questions/{index.js => index.ts} | 2 +- 11 files changed, 111 insertions(+), 91 deletions(-) rename web/src/components/questions/{GenericQuestion.test.jsx => GenericQuestion.test.tsx} (95%) rename web/src/components/questions/{GenericQuestion.jsx => GenericQuestion.tsx} (73%) rename web/src/components/questions/{LuksActivationQuestion.test.jsx => LuksActivationQuestion.test.tsx} (77%) rename web/src/components/questions/{LuksActivationQuestion.jsx => LuksActivationQuestion.tsx} (76%) rename web/src/components/questions/{QuestionActions.test.jsx => QuestionActions.test.tsx} (97%) rename web/src/components/questions/{QuestionActions.jsx => QuestionActions.tsx} (74%) rename web/src/components/questions/{QuestionWithPassword.test.jsx => QuestionWithPassword.test.tsx} (87%) rename web/src/components/questions/{QuestionWithPassword.jsx => QuestionWithPassword.tsx} (78%) rename web/src/components/questions/{Questions.test.jsx => Questions.test.tsx} (91%) rename web/src/components/questions/{Questions.jsx => Questions.tsx} (88%) rename web/src/components/questions/{index.js => index.ts} (96%) diff --git a/web/src/components/questions/GenericQuestion.test.jsx b/web/src/components/questions/GenericQuestion.test.tsx similarity index 95% rename from web/src/components/questions/GenericQuestion.test.jsx rename to web/src/components/questions/GenericQuestion.test.tsx index 6573af614f..c5f3536688 100644 --- a/web/src/components/questions/GenericQuestion.test.jsx +++ b/web/src/components/questions/GenericQuestion.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -23,8 +23,9 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { GenericQuestion } from "~/components/questions"; +import { Question } from "~/types/questions"; -const question = { +const question: Question = { id: 1, text: "Do you write unit tests?", options: ["always", "sometimes", "never"], diff --git a/web/src/components/questions/GenericQuestion.jsx b/web/src/components/questions/GenericQuestion.tsx similarity index 73% rename from web/src/components/questions/GenericQuestion.jsx rename to web/src/components/questions/GenericQuestion.tsx index 8028b4be97..6a3a8f974f 100644 --- a/web/src/components/questions/GenericQuestion.jsx +++ b/web/src/components/questions/GenericQuestion.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -23,10 +23,23 @@ import React from "react"; import { Text } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { QuestionActions } from "~/components/questions"; +import { AnswerCallback, Question } from "~/types/questions"; import { _ } from "~/i18n"; -export default function GenericQuestion({ question, answerCallback }) { - const actionCallback = (option) => { +/** + * Component for rendering generic questions + * + * @param question - the question to be answered + * @param answerCallback - the callback to be triggered on answer + */ +export default function GenericQuestion({ + question, + answerCallback, +}: { + question: Question; + answerCallback: AnswerCallback; +}): React.ReactNode { + const actionCallback = (option: string) => { question.answer = option; answerCallback(question); }; diff --git a/web/src/components/questions/LuksActivationQuestion.test.jsx b/web/src/components/questions/LuksActivationQuestion.test.tsx similarity index 77% rename from web/src/components/questions/LuksActivationQuestion.test.jsx rename to web/src/components/questions/LuksActivationQuestion.test.tsx index 8c56815873..41a5326d05 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.jsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -23,23 +23,25 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { LuksActivationQuestion } from "~/components/questions"; - -let question; -const answerFn = jest.fn(); +import { AnswerCallback, Question } from "~/types/questions"; + +let question: Question; +const questionMock: Question = { + id: 1, + class: "storage.luks_activation", + text: "A Luks device found. Do you want to open it?", + options: ["decrypt", "skip"], + defaultOption: "decrypt", + data: { attempt: "1" }, +}; +const answerFn: AnswerCallback = jest.fn(); const renderQuestion = () => plainRender(); describe("LuksActivationQuestion", () => { beforeEach(() => { - question = { - id: 1, - class: "storage.luks_activation", - text: "A Luks device found. Do you want to open it?", - options: ["decrypt", "skip"], - defaultOption: "decrypt", - data: { attempt: "1" }, - }; + question = { ...questionMock }; }); it("renders the question text", async () => { @@ -48,13 +50,6 @@ describe("LuksActivationQuestion", () => { await screen.findByText(question.text); }); - it("contains a textinput for entering the password", async () => { - renderQuestion(); - - const passwordInput = await screen.findByLabelText("Encryption Password"); - expect(passwordInput).not.toBeNull(); - }); - describe("when it is the first attempt", () => { it("does not contain a warning", async () => { renderQuestion(); @@ -66,14 +61,7 @@ describe("LuksActivationQuestion", () => { describe("when it is not the first attempt", () => { beforeEach(() => { - question = { - id: 1, - class: "storage.luks_activation", - text: "A Luks device found. Do you want to open it?", - options: ["decrypt", "skip"], - defaultOption: "decrypt", - data: { attempt: "3" }, - }; + question = { ...questionMock, data: { attempt: "2" } }; }); it("contains a warning", async () => { @@ -84,17 +72,6 @@ describe("LuksActivationQuestion", () => { }); describe("when the user selects one of the options", () => { - beforeEach(() => { - question = { - id: 1, - class: "storage.luks_activation", - text: "A Luks device found. Do you want to open it?", - options: ["decrypt", "skip"], - defaultOption: "decrypt", - data: { attempt: "1" }, - }; - }); - describe("by clicking on 'Skip'", () => { it("calls the callback after setting both, answer and password", async () => { const { user } = renderQuestion(); diff --git a/web/src/components/questions/LuksActivationQuestion.jsx b/web/src/components/questions/LuksActivationQuestion.tsx similarity index 76% rename from web/src/components/questions/LuksActivationQuestion.jsx rename to web/src/components/questions/LuksActivationQuestion.tsx index 8e31b915fc..233a1367f7 100644 --- a/web/src/components/questions/LuksActivationQuestion.jsx +++ b/web/src/components/questions/LuksActivationQuestion.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -20,27 +20,41 @@ */ import React, { useState } from "react"; -import { Alert, Form, FormGroup, Text } from "@patternfly/react-core"; +import { Alert as PFAlert, Form, FormGroup, Text } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { PasswordInput, Popup } from "~/components/core"; import { QuestionActions } from "~/components/questions"; import { _ } from "~/i18n"; -const renderAlert = (attempt) => { - if (!attempt || attempt === 1) return null; +/** + * Internal component for rendering an alert if given password failed + */ +const Alert = ({ attempt }: { attempt: string | undefined }): React.ReactNode => { + if (!attempt || parseInt(attempt) === 1) return null; return ( // TRANSLATORS: error message, user entered a wrong password - + ); }; +/** + * Component for rendering questions related to LUKS activation + * + * @param question - the question to be answered + * @param answerCallback - the callback to be triggered on answer + */ export default function LuksActivationQuestion({ question, answerCallback }) { const [password, setPassword] = useState(question.password || ""); const conditions = { disable: { decrypt: password === "" } }; const defaultAction = "decrypt"; - const actionCallback = (option) => { + const actionCallback = (option: string) => { question.password = password; question.answer = option; answerCallback(question); @@ -60,7 +74,7 @@ export default function LuksActivationQuestion({ question, answerCallback }) { aria-label={_("Question")} titleIconVariant={() => } > - {renderAlert(parseInt(question.data.attempt))} + {question.text}
      {/* TRANSLATORS: field label */} diff --git a/web/src/components/questions/QuestionActions.test.jsx b/web/src/components/questions/QuestionActions.test.tsx similarity index 97% rename from web/src/components/questions/QuestionActions.test.jsx rename to web/src/components/questions/QuestionActions.test.tsx index f390d1d2d2..92276120f5 100644 --- a/web/src/components/questions/QuestionActions.test.jsx +++ b/web/src/components/questions/QuestionActions.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -23,10 +23,11 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { QuestionActions } from "~/components/questions"; +import { Question } from "~/types/questions"; let defaultOption = "sure"; -let question = { +let question: Question = { id: 1, text: "Should we use a component for rendering actions?", options: ["no", "maybe", "sure"], diff --git a/web/src/components/questions/QuestionActions.jsx b/web/src/components/questions/QuestionActions.tsx similarity index 74% rename from web/src/components/questions/QuestionActions.jsx rename to web/src/components/questions/QuestionActions.tsx index f0ececc376..c90539fd20 100644 --- a/web/src/components/questions/QuestionActions.jsx +++ b/web/src/components/questions/QuestionActions.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -27,11 +27,8 @@ import { Popup } from "~/components/core"; * Returns given text capitalized * * TODO: make it work with i18n - * - * @param {String} text - string to be capitalized - * @return {String} capitalized text */ -const label = (text) => `${text[0].toUpperCase()}${text.slice(1)}`; +const label = (text: string): string => `${text[0].toUpperCase()}${text.slice(1)}`; /** * A component for building a Question actions, using the defaultAction @@ -42,13 +39,23 @@ const label = (text) => `${text[0].toUpperCase()}${text.slice(1)}`; * React.Fragment (aka <>) here for wrapping the actions instead of directly using the Popup.Actions. * * @param {object} props - component props - * @param {Array.} props.actions - the actions to be shown - * @param {String} [props.defaultAction] - the action to be shown as primary - * @param {function} props.actionCallback - the function to be called when user clicks on action - * @param {Object} [props.conditions={}] - an object holding conditions, like when an action is disabled + * @param props.actions - the actions to be shown + * @param props.defaultAction - the action to be shown as primary + * @param props.actionCallback - the function to be called when user clicks on action + * @param props.conditions={} - an object holding conditions, like when an action is disabled */ -export default function QuestionActions({ actions, defaultAction, actionCallback, conditions }) { - let [[primaryAction], secondaryActions] = partition(actions, (a) => a === defaultAction); +export default function QuestionActions({ + actions, + defaultAction, + actionCallback, + conditions = {}, +}: { + actions: string[]; + defaultAction?: string; + actionCallback: (action: string) => void; + conditions?: { disable?: { [key: string]: boolean } }; +}): React.ReactNode { + let [[primaryAction], secondaryActions] = partition(actions, (a: string) => a === defaultAction); // Ensure there is always a primary action if (!primaryAction) [primaryAction, ...secondaryActions] = secondaryActions; diff --git a/web/src/components/questions/QuestionWithPassword.test.jsx b/web/src/components/questions/QuestionWithPassword.test.tsx similarity index 87% rename from web/src/components/questions/QuestionWithPassword.test.jsx rename to web/src/components/questions/QuestionWithPassword.test.tsx index bedcbb3cb1..bf5b3d0c3d 100644 --- a/web/src/components/questions/QuestionWithPassword.test.jsx +++ b/web/src/components/questions/QuestionWithPassword.test.tsx @@ -23,25 +23,21 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { QuestionWithPassword } from "~/components/questions"; +import { Question } from "~/types/questions"; -let question; const answerFn = jest.fn(); +const question: Question = { + id: 1, + class: "question.password", + text: "Random question. Will you provide random password?", + options: ["ok", "cancel"], + defaultOption: "cancel", +}; const renderQuestion = () => plainRender(); describe("QuestionWithPassword", () => { - beforeEach(() => { - question = { - id: 1, - class: "question.password", - text: "Random question. Will you provide random password?", - options: ["ok", "cancel"], - defaultOption: "cancel", - data: {}, - }; - }); - it("renders the question text", () => { renderQuestion(); diff --git a/web/src/components/questions/QuestionWithPassword.jsx b/web/src/components/questions/QuestionWithPassword.tsx similarity index 78% rename from web/src/components/questions/QuestionWithPassword.jsx rename to web/src/components/questions/QuestionWithPassword.tsx index 14e534ec44..651c875c85 100644 --- a/web/src/components/questions/QuestionWithPassword.jsx +++ b/web/src/components/questions/QuestionWithPassword.tsx @@ -20,17 +20,30 @@ */ import React, { useState } from "react"; -import { Alert, Form, FormGroup, Text } from "@patternfly/react-core"; +import { Form, FormGroup, Text } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { PasswordInput, Popup } from "~/components/core"; import { QuestionActions } from "~/components/questions"; +import { AnswerCallback, Question } from "~/types/questions"; import { _ } from "~/i18n"; -export default function QuestionWithPassword({ question, answerCallback }) { +/** + * Component for rendering questions asking for password + * + * @param question - the question to be answered + * @param answerCallback - the callback to be triggered on answer + */ +export default function QuestionWithPassword({ + question, + answerCallback, +}: { + question: Question; + answerCallback: AnswerCallback; +}): React.ReactNode { const [password, setPassword] = useState(question.password || ""); const defaultAction = question.defaultOption; - const actionCallback = (option) => { + const actionCallback = (option: string) => { question.password = password; question.answer = option; answerCallback(question); diff --git a/web/src/components/questions/Questions.test.jsx b/web/src/components/questions/Questions.test.tsx similarity index 91% rename from web/src/components/questions/Questions.test.jsx rename to web/src/components/questions/Questions.test.tsx index 02a2459d58..a68662dcc3 100644 --- a/web/src/components/questions/Questions.test.jsx +++ b/web/src/components/questions/Questions.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -23,10 +23,10 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, plainRender } from "~/test-utils"; import { Questions } from "~/components/questions"; -import { QuestionType } from "~/types/questions"; +import { Question, QuestionType } from "~/types/questions"; import * as GenericQuestionComponent from "~/components/questions/GenericQuestion"; -let mockQuestions; +let mockQuestions: Question[]; const mockMutation = jest.fn(); jest.mock("~/components/questions/LuksActivationQuestion", () => () => ( @@ -43,15 +43,15 @@ jest.mock("~/queries/questions", () => ({ useQuestionsConfig: () => ({ mutate: mockMutation }), })); -const genericQuestion = { +const genericQuestion: Question = { id: 1, type: QuestionType.generic, text: "Do you write unit tests?", options: ["always", "sometimes", "never"], defaultOption: "sometimes", }; -const passwordQuestion = { id: 1, type: QuestionType.withPassword }; -const luksActivationQuestion = { id: 1, class: "storage.luks_activation" }; +const passwordQuestion: Question = { id: 1, type: QuestionType.withPassword }; +const luksActivationQuestion: Question = { id: 2, class: "storage.luks_activation" }; describe("Questions", () => { afterEach(() => { diff --git a/web/src/components/questions/Questions.jsx b/web/src/components/questions/Questions.tsx similarity index 88% rename from web/src/components/questions/Questions.jsx rename to web/src/components/questions/Questions.tsx index 1da02c97a6..e08d31d60c 100644 --- a/web/src/components/questions/Questions.jsx +++ b/web/src/components/questions/Questions.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -19,8 +19,6 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; import { GenericQuestion, @@ -28,16 +26,16 @@ import { LuksActivationQuestion, } from "~/components/questions"; import { useQuestions, useQuestionsConfig, useQuestionsChanges } from "~/queries/questions"; -import { Question, QuestionType } from "~/types/questions"; +import { AnswerCallback, QuestionType } from "~/types/questions"; -export default function Questions() { +export default function Questions(): React.ReactNode { useQuestionsChanges(); const pendingQuestions = useQuestions(); const questionsConfig = useQuestionsConfig(); if (pendingQuestions.length === 0) return null; - const answerQuestion = (/** @type {Question} */ answeredQuestion) => + const answerQuestion: AnswerCallback = (answeredQuestion) => questionsConfig.mutate(answeredQuestion); // Renders the first pending question diff --git a/web/src/components/questions/index.js b/web/src/components/questions/index.ts similarity index 96% rename from web/src/components/questions/index.js rename to web/src/components/questions/index.ts index 15cc7dee28..0633e0610c 100644 --- a/web/src/components/questions/index.js +++ b/web/src/components/questions/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * From 7c7b049b9ba2f2b5a25817e1f350506ad03ae207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 31 Jul 2024 14:10:20 +0100 Subject: [PATCH 350/430] refactor(web): stop exporting everything from components/questions --- web/src/components/questions/GenericQuestion.test.tsx | 2 +- web/src/components/questions/GenericQuestion.tsx | 2 +- .../components/questions/LuksActivationQuestion.test.tsx | 2 +- web/src/components/questions/LuksActivationQuestion.tsx | 2 +- web/src/components/questions/QuestionActions.test.tsx | 2 +- .../components/questions/QuestionWithPassword.test.tsx | 2 +- web/src/components/questions/QuestionWithPassword.tsx | 2 +- web/src/components/questions/Questions.test.tsx | 2 +- web/src/components/questions/Questions.tsx | 8 +++----- web/src/components/questions/index.ts | 4 ---- 10 files changed, 11 insertions(+), 17 deletions(-) diff --git a/web/src/components/questions/GenericQuestion.test.tsx b/web/src/components/questions/GenericQuestion.test.tsx index c5f3536688..a45855a49b 100644 --- a/web/src/components/questions/GenericQuestion.test.tsx +++ b/web/src/components/questions/GenericQuestion.test.tsx @@ -22,8 +22,8 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { GenericQuestion } from "~/components/questions"; import { Question } from "~/types/questions"; +import GenericQuestion from "~/components/questions/GenericQuestion"; const question: Question = { id: 1, diff --git a/web/src/components/questions/GenericQuestion.tsx b/web/src/components/questions/GenericQuestion.tsx index 6a3a8f974f..d1f2f34de9 100644 --- a/web/src/components/questions/GenericQuestion.tsx +++ b/web/src/components/questions/GenericQuestion.tsx @@ -22,8 +22,8 @@ import React from "react"; import { Text } from "@patternfly/react-core"; import { Popup } from "~/components/core"; -import { QuestionActions } from "~/components/questions"; import { AnswerCallback, Question } from "~/types/questions"; +import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; /** diff --git a/web/src/components/questions/LuksActivationQuestion.test.tsx b/web/src/components/questions/LuksActivationQuestion.test.tsx index 41a5326d05..c2da7b99cd 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.tsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -22,8 +22,8 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { LuksActivationQuestion } from "~/components/questions"; import { AnswerCallback, Question } from "~/types/questions"; +import LuksActivationQuestion from "~/components/questions/LuksActivationQuestion"; let question: Question; const questionMock: Question = { diff --git a/web/src/components/questions/LuksActivationQuestion.tsx b/web/src/components/questions/LuksActivationQuestion.tsx index 233a1367f7..8862defbc6 100644 --- a/web/src/components/questions/LuksActivationQuestion.tsx +++ b/web/src/components/questions/LuksActivationQuestion.tsx @@ -23,7 +23,7 @@ import React, { useState } from "react"; import { Alert as PFAlert, Form, FormGroup, Text } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { PasswordInput, Popup } from "~/components/core"; -import { QuestionActions } from "~/components/questions"; +import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; /** diff --git a/web/src/components/questions/QuestionActions.test.tsx b/web/src/components/questions/QuestionActions.test.tsx index 92276120f5..ad6fc6280c 100644 --- a/web/src/components/questions/QuestionActions.test.tsx +++ b/web/src/components/questions/QuestionActions.test.tsx @@ -22,8 +22,8 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import { QuestionActions } from "~/components/questions"; import { Question } from "~/types/questions"; +import QuestionActions from "~/components/questions/QuestionActions"; let defaultOption = "sure"; diff --git a/web/src/components/questions/QuestionWithPassword.test.tsx b/web/src/components/questions/QuestionWithPassword.test.tsx index bf5b3d0c3d..371daaf20a 100644 --- a/web/src/components/questions/QuestionWithPassword.test.tsx +++ b/web/src/components/questions/QuestionWithPassword.test.tsx @@ -22,8 +22,8 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { QuestionWithPassword } from "~/components/questions"; import { Question } from "~/types/questions"; +import QuestionWithPassword from "~/components/questions/QuestionWithPassword"; const answerFn = jest.fn(); const question: Question = { diff --git a/web/src/components/questions/QuestionWithPassword.tsx b/web/src/components/questions/QuestionWithPassword.tsx index 651c875c85..f18f250625 100644 --- a/web/src/components/questions/QuestionWithPassword.tsx +++ b/web/src/components/questions/QuestionWithPassword.tsx @@ -23,8 +23,8 @@ import React, { useState } from "react"; import { Form, FormGroup, Text } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { PasswordInput, Popup } from "~/components/core"; -import { QuestionActions } from "~/components/questions"; import { AnswerCallback, Question } from "~/types/questions"; +import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; /** diff --git a/web/src/components/questions/Questions.test.tsx b/web/src/components/questions/Questions.test.tsx index a68662dcc3..f85555e760 100644 --- a/web/src/components/questions/Questions.test.tsx +++ b/web/src/components/questions/Questions.test.tsx @@ -22,8 +22,8 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, plainRender } from "~/test-utils"; -import { Questions } from "~/components/questions"; import { Question, QuestionType } from "~/types/questions"; +import Questions from "~/components/questions/Questions"; import * as GenericQuestionComponent from "~/components/questions/GenericQuestion"; let mockQuestions: Question[]; diff --git a/web/src/components/questions/Questions.tsx b/web/src/components/questions/Questions.tsx index e08d31d60c..42ef731d26 100644 --- a/web/src/components/questions/Questions.tsx +++ b/web/src/components/questions/Questions.tsx @@ -20,11 +20,9 @@ */ import React from "react"; -import { - GenericQuestion, - QuestionWithPassword, - LuksActivationQuestion, -} from "~/components/questions"; +import GenericQuestion from "~/components/questions/GenericQuestion"; +import QuestionWithPassword from "~/components/questions/QuestionWithPassword"; +import LuksActivationQuestion from "~/components/questions/LuksActivationQuestion"; import { useQuestions, useQuestionsConfig, useQuestionsChanges } from "~/queries/questions"; import { AnswerCallback, QuestionType } from "~/types/questions"; diff --git a/web/src/components/questions/index.ts b/web/src/components/questions/index.ts index 0633e0610c..95dcc90e28 100644 --- a/web/src/components/questions/index.ts +++ b/web/src/components/questions/index.ts @@ -19,8 +19,4 @@ * find current contact information at www.suse.com. */ -export { default as QuestionActions } from "./QuestionActions"; -export { default as GenericQuestion } from "./GenericQuestion"; -export { default as QuestionWithPassword } from "./QuestionWithPassword"; -export { default as LuksActivationQuestion } from "./LuksActivationQuestion"; export { default as Questions } from "./Questions"; From 68af2da14dfb35c08e3f9760567984218afe214a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 31 Jul 2024 18:08:53 +0200 Subject: [PATCH 351/430] Update nokogiri dependency to version 1.16 --- service/agama-yast.gemspec | 2 +- service/package/rubygem-agama-yast.changes | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/service/agama-yast.gemspec b/service/agama-yast.gemspec index d93dfdd3fd..07e413379a 100644 --- a/service/agama-yast.gemspec +++ b/service/agama-yast.gemspec @@ -56,7 +56,7 @@ Gem::Specification.new do |spec| spec.add_dependency "cheetah", "~> 1.0.0" spec.add_dependency "eventmachine", "~> 1.2.7" spec.add_dependency "fast_gettext", "~> 2.3.0" - spec.add_dependency "nokogiri", "~> 1.15.4" + spec.add_dependency "nokogiri", "~> 1.16.6" spec.add_dependency "rexml", "~> 3.2.5" spec.add_dependency "ruby-dbus", ">= 0.23.1", "< 1.0" end diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 5df57f925c..6fb16a8fbb 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jul 31 15:48:00 UTC 2024 - Ladislav Slezák + +- Update nokogiri dependency to version 1.16 + (gh#openSUSE/agama#1518) + ------------------------------------------------------------------- Mon Jul 22 15:26:48 UTC 2024 - Josef Reidinger From 7dde65656c16bd72aa096c3eaf7d03f538913438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 31 Jul 2024 18:33:39 +0200 Subject: [PATCH 352/430] Update Gemfile.lock --- service/Gemfile.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/service/Gemfile.lock b/service/Gemfile.lock index eacdcece4e..61d1c94a33 100755 --- a/service/Gemfile.lock +++ b/service/Gemfile.lock @@ -7,7 +7,7 @@ PATH cheetah (~> 1.0.0) eventmachine (~> 1.2.7) fast_gettext (~> 2.3.0) - nokogiri (~> 1.15.4) + nokogiri (~> 1.16.6) rexml (~> 3.2.5) ruby-dbus (>= 0.23.1, < 1.0) @@ -22,19 +22,18 @@ GEM cfa (~> 1.0) cheetah (1.0.0) abstract_method (~> 1.2) - diff-lcs (1.5.0) - docile (1.4.0) + diff-lcs (1.5.1) + docile (1.4.1) eventmachine (1.2.7) fast_gettext (2.3.0) - mini_portile2 (2.8.5) - nokogiri (1.15.5) - mini_portile2 (~> 2.8.2) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) packaging_rake_tasks (1.5.4) rake - racc (1.7.3) + racc (1.8.1) rake (13.0.6) - rexml (3.2.6) + rexml (3.2.9) + strscan rspec (3.11.0) rspec-core (~> 3.11.0) rspec-expectations (~> 3.11.0) @@ -58,7 +57,8 @@ GEM simplecov-html (0.12.3) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) - yard (0.9.34) + strscan (3.1.0) + yard (0.9.36) PLATFORMS x86_64-linux @@ -75,4 +75,4 @@ DEPENDENCIES yard (~> 0.9.0) BUNDLED WITH - 2.4.22 + 2.5.11 From 08f43695292f63ea6d921f41208dd3c707b7df1a Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 10:47:47 +0200 Subject: [PATCH 353/430] BaseHTTPClient.client private again, +new_unauthenticated_with_url and renaming the helper authenticator --- rust/agama-lib/src/base_http_client.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index bc31c485ef..0a28307420 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -21,7 +21,7 @@ use crate::{auth::AuthToken, error::ServiceError}; /// } /// ``` pub struct BaseHTTPClient { - pub client: reqwest::Client, + client: reqwest::Client, pub base_url: String, } @@ -43,12 +43,19 @@ impl BaseHTTPClient { /// Uses `localhost`, authenticates with [`AuthToken`]. pub fn new() -> Result { Ok(Self { - client: Self::authenticated_reqwest_client()?, + client: Self::authenticated_client()?, ..Default::default() }) } - fn authenticated_reqwest_client() -> Result { + pub fn new_unauthenticated_with_url(url: String) -> Result { + Ok(Self { + client: reqwest::Client::new(), + base_url: url, + }) + } + + fn authenticated_client() -> Result { // TODO: this error is subtly misleading, leading me to believe the SERVER said it, // but in fact it is the CLIENT not finding an auth token let token = AuthToken::find().ok_or(ServiceError::NotAuthenticated)?; From 8b04aafda20b265a8d56c411cee9cf31a543d862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 1 Aug 2024 14:55:41 +0100 Subject: [PATCH 354/430] fix(web): replace exfat-utils with exfatprogs --- service/package/gem2rpm.yml | 2 +- setup-services.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml index c95995ae40..aab4071193 100644 --- a/service/package/gem2rpm.yml +++ b/service/package/gem2rpm.yml @@ -52,7 +52,7 @@ Requires: dmraid Requires: dosfstools Requires: e2fsprogs - Requires: exfat-utils + Requires: exfatprogs Requires: f2fs-tools Requires: fcoe-utils %ifarch x86_64 aarch64 diff --git a/setup-services.sh b/setup-services.sh index 5a3127bf3c..0619434a71 100755 --- a/setup-services.sh +++ b/setup-services.sh @@ -68,7 +68,7 @@ $SUDO zypper --non-interactive install \ dmraid \ dosfstools \ e2fsprogs \ - exfat-utils \ + exfatprogs \ f2fs-tools \ fcoe-utils \ jfsutils \ From d3bfe27e03a6da578e0fe8611fd749e2b70c4f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 1 Aug 2024 14:59:13 +0100 Subject: [PATCH 355/430] doc(service): update changes file --- service/package/rubygem-agama-yast.changes | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 6fb16a8fbb..6b8f6c5e5e 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Aug 1 13:57:51 UTC 2024 - Imobach Gonzalez Sosa + +- Use exfatprogs instead of exfat-utils (gh#openSUSE/agama#1520). + ------------------------------------------------------------------- Wed Jul 31 15:48:00 UTC 2024 - Ladislav Slezák From 7c155d23f2754d5487318fa07907072b37cf856b Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 16:24:29 +0200 Subject: [PATCH 356/430] BaseHTTPClient: just use the public base_url field --- rust/agama-lib/src/base_http_client.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 0a28307420..1f78c2e040 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -48,13 +48,6 @@ impl BaseHTTPClient { }) } - pub fn new_unauthenticated_with_url(url: String) -> Result { - Ok(Self { - client: reqwest::Client::new(), - base_url: url, - }) - } - fn authenticated_client() -> Result { // TODO: this error is subtly misleading, leading me to believe the SERVER said it, // but in fact it is the CLIENT not finding an auth token From 756fc6ab9581c825643883e0c9c40f5dc54c3595 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 5 Jun 2024 08:18:55 +0200 Subject: [PATCH 357/430] run zypper --verbose to see where it gets stuck --- setup-services.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/setup-services.sh b/setup-services.sh index 0619434a71..3c6534ab29 100755 --- a/setup-services.sh +++ b/setup-services.sh @@ -36,8 +36,10 @@ $SUDO systemctl list-unit-files agama-web-server.service &>/dev/null && $SUDO sy # Ruby services +ZYPPER="zypper --non-interactive -v" + # Packages required for Ruby development (i.e., bundle install). -$SUDO zypper --non-interactive install \ +$SUDO $ZYPPER install \ gcc \ gcc-c++ \ make \ @@ -47,7 +49,7 @@ $SUDO zypper --non-interactive install \ # Packages required by Agama Ruby services (see ./service/package/gem2rpm.yml). # TODO extract list from gem2rpm.yml -$SUDO zypper --non-interactive install \ +$SUDO $ZYPPER install \ dbus-1-common \ suseconnect-ruby-bindings \ autoyast2-installation \ @@ -89,13 +91,13 @@ $SUDO zypper --non-interactive install \ # Install x86_64 packages if [ $(uname -m) == "x86_64" ]; then - $SUDO zypper --non-interactive install \ + $SUDO $ZYPPER install \ fde-tools fi # Install s390 packages if [ $(uname -m) == "s390x" ]; then - $SUDO zypper --non-interactive install \ + $SUDO $ZYPPER install \ yast2-s390 \ yast2-reipl \ yast2-cio @@ -120,10 +122,10 @@ fi # Rust service, CLI and auto-installation. # Only install cargo if it is not available (avoid conflicts with rustup) -which cargo || $SUDO zypper --non-interactive install cargo +which cargo || $SUDO $ZYPPER install cargo # Packages required by Rust code (see ./rust/package/agama.spec) -$SUDO zypper --non-interactive install \ +$SUDO $ZYPPER install \ clang-devel \ gzip \ jsonnet \ From b7713831fb96416e2e1c3aa734fed931a459f912 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 4 Jul 2024 13:55:48 +0200 Subject: [PATCH 358/430] UsersHttpClient --- rust/Cargo.lock | 1 + rust/agama-lib/src/base_http_client.rs | 44 ++++++++++- rust/agama-lib/src/store.rs | 5 +- rust/agama-lib/src/users.rs | 2 + rust/agama-lib/src/users/http_client.rs | 100 ++++++++++++++++++++++++ rust/agama-lib/src/users/store.rs | 14 ++-- rust/agama-server/Cargo.toml | 2 + rust/agama-server/src/users/web.rs | 28 ++++++- 8 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 rust/agama-lib/src/users/http_client.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9154b3659e..74b7d373c3 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -113,6 +113,7 @@ dependencies = [ "pin-project", "rand", "regex", + "reqwest 0.12.4", "serde", "serde_json", "serde_with", diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 655a1ce5d7..9dd07c914c 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -54,10 +54,13 @@ impl BaseHTTPClient { /// Arguments: /// /// * `path`: path relative to HTTP API like `/questions` - pub async fn get(&self, path: &str) -> Result { + pub async fn get(&self, path: &str) -> Result { let response = self.get_response(path).await?; + println!("GET response: {:?}", response); if response.status().is_success() { - response.json::().await.map_err(|e| e.into()) + let ret = response.json::().await.map_err(|e| e.into()); + println!("GET value: {:#?}", ret); + ret } else { Err(self.build_backend_error(response).await) } @@ -120,6 +123,43 @@ impl BaseHTTPClient { .map_err(|e| e.into()) } + /// put object to given path and report error if response is not success + /// + /// Arguments: + /// + /// * `path`: path relative to HTTP API like `/users/first` + /// * `object`: Object that can be serialiazed to JSON as body of request. + pub async fn put(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { + let response = self.put_response(path, object).await?; + if response.status().is_success() { + Ok(()) + } else { + Err(self.build_backend_error(response).await) + } + } + + /// post object to given path and returns server response. Reports error only if failed to send + /// request, but if server returns e.g. 500, it will be in Ok result. + /// + /// In general unless specific response handling is needed, simple post should be used. + /// + /// Arguments: + /// + /// * `path`: path relative to HTTP API like `/questions` + /// * `object`: Object that can be serialiazed to JSON as body of request. + pub async fn put_response( + &self, + path: &str, + object: &impl Serialize, + ) -> Result { + self.client + .put(self.url(path)) + .json(object) + .send() + .await + .map_err(|e| e.into()) + } + /// delete call on given path and report error if failed /// /// Arguments: diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 9add8349c6..bfbf0a6906 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -16,7 +16,7 @@ use zbus::Connection; /// /// This struct uses the default connection built by [connection function](super::connection). pub struct Store<'a> { - users: UsersStore<'a>, + users: UsersStore, network: NetworkStore, product: ProductStore<'a>, software: SoftwareStore<'a>, @@ -31,7 +31,8 @@ impl<'a> Store<'a> { ) -> Result, ServiceError> { Ok(Self { localization: LocalizationStore::new(connection.clone()).await?, - users: UsersStore::new(connection.clone()).await?, + // FIXME: http clone ok? ref better? + users: UsersStore::new(http_client.clone()).await?, network: NetworkStore::new(http_client).await?, product: ProductStore::new(connection.clone()).await?, software: SoftwareStore::new(connection.clone()).await?, diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs index 9ee6a72b5a..d570b15386 100644 --- a/rust/agama-lib/src/users.rs +++ b/rust/agama-lib/src/users.rs @@ -1,10 +1,12 @@ //! Implements support for handling the users settings mod client; +mod http_client; pub mod proxies; mod settings; mod store; pub use client::{FirstUser, UsersClient}; +pub use http_client::UsersHttpClient; pub use settings::{FirstUserSettings, RootUserSettings, UserSettings}; pub use store::UsersStore; diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs new file mode 100644 index 0000000000..72007f9fd0 --- /dev/null +++ b/rust/agama-lib/src/users/http_client.rs @@ -0,0 +1,100 @@ +use anyhow::anyhow; // WIP + +use serde::Deserialize; +//use reqwest::StatusCode; +use super::client::FirstUser; +use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; + +pub struct UsersHttpClient { + client: BaseHTTPClient, +} + +// copying agama_server::users::web::RootConfig +// but not all derives +// #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Clone, Debug, Default, Deserialize)] +pub struct RootConfig { + /// returns if password for root is set or not + password: bool, + /// empty string mean no sshkey is specified + sshkey: String, +} + +impl UsersHttpClient { + pub async fn new() -> Result { + Ok(Self { + client: BaseHTTPClient::new()?, + }) + } + + /// Returns the settings for first non admin user + pub async fn first_user(&self) -> Result { + self.client.get("/users/first").await + } + + + /// Set the configuration for the first user + pub async fn set_first_user( + &self, + first_user: &FirstUser, + ) -> Result<(bool, Vec), ServiceError> { + /* + self.users_proxy + .set_first_user( + &first_user.full_name, + &first_user.user_name, + &first_user.password, + first_user.autologin, + std::collections::HashMap::new(), + ) + .await + */ + //Err(anyhow!("TODO implement UsersHttpClient SETUSER").into()) + self.client.put("/users/first", first_user).await?; + // TODO: make BaseHTTPClient.put(_response) return the issues + Ok((true, vec!())) + } + + pub async fn remove_first_user(&self) -> Result { + //Ok(self.users_proxy.remove_first_user().await? == 0) + Err(anyhow!("TODO implement UsersHttpClient RMUSER").into()) + } + + async fn root_config(&self) -> Result { + self.client.get("/users/root").await + } + + /// Whether the root password is set or not + pub async fn is_root_password(&self) -> Result { + let root_config = self.root_config().await?; + Ok(root_config.password) + } + + /// SetRootPassword method + pub async fn set_root_password( + &self, + _value: &str, + _encrypted: bool, + ) -> Result { + //Ok(self.users_proxy.set_root_password(value, encrypted).await?) + Err(anyhow!("TODO implement UsersHttpClient SETPASS").into()) + } + + pub async fn remove_root_password(&self) -> Result { + //Ok(self.users_proxy.remove_root_password().await?) + Err(anyhow!("TODO implement UsersHttpClient RMPASS").into()) + } + + /// Returns the SSH key for the root user + pub async fn root_ssh_key(&self) -> Result { + let root_config = self.root_config().await?; + Ok(root_config.sshkey) + } + + + /// SetRootSSHKey method + pub async fn set_root_sshkey(&self, value: &str) -> Result { + //Ok(self.users_proxy.set_root_sshkey(value).await?) + Err(anyhow!("TODO implement UsersHttpClient SETKEY {}", value).into()) + } +} diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 6c34f0b2a1..2199276da8 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -1,16 +1,16 @@ -use super::{FirstUser, FirstUserSettings, RootUserSettings, UserSettings, UsersClient}; +use super::{FirstUser, FirstUserSettings, RootUserSettings, UserSettings, UsersHttpClient}; use crate::error::ServiceError; -use zbus::Connection; +use reqwest; /// Loads and stores the users settings from/to the D-Bus service. -pub struct UsersStore<'a> { - users_client: UsersClient<'a>, +pub struct UsersStore { + users_client: UsersHttpClient, } -impl<'a> UsersStore<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { +impl UsersStore { + pub async fn new(_client: reqwest::Client) -> Result { Ok(Self { - users_client: UsersClient::new(connection).await?, + users_client: UsersHttpClient::new().await?, }) } diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 52386a3005..7c5475a773 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -57,6 +57,8 @@ futures-util = { version = "0.3.30", default-features = false, features = [ libsystemd = "0.7.0" subprocess = "0.2.9" gethostname = "0.4.3" +# FIXME probably wrong here, copied from agama-lib +reqwest = { version = "0.12.4", features = ["json", "cookies"] } [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 487c51add2..d360c8bc1f 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -15,7 +15,14 @@ use agama_lib::{ error::ServiceError, users::{proxies::Users1Proxy, FirstUser, UsersClient}, }; -use axum::{extract::State, routing::get, Json, Router}; +use axum::{ + extract::State, + http::StatusCode, + response::IntoResponse, + routing::get, + Json, Router, +}; +//use reqwest; use serde::{Deserialize, Serialize}; use tokio_stream::{Stream, StreamExt}; @@ -152,15 +159,28 @@ async fn remove_first_user(State(state): State>) -> Result<(), Er } #[utoipa::path(put, path = "/users/first", responses( + // TODO 204 empty response (status = 200, description = "Sets the first user"), (status = 400, description = "The D-Bus service could not perform the action"), + (status = 422, description = "Invalid first user. Details are in body", body = Vec), ))] async fn set_first_user( State(state): State>, Json(config): Json, -) -> Result<(), Error> { - state.users.set_first_user(&config).await?; - Ok(()) +) -> Result { + // TODO: web ui not ready to handle this? + // issues: for example, trying to use a system user id; empty password + // success: simply !issues.is_empty() + let (_success, issues) = state.users.set_first_user(&config).await?; + if issues.is_empty() { + Ok((StatusCode::NO_CONTENT, ().into_response())) + } + else { + Ok(( + StatusCode::UNPROCESSABLE_ENTITY, + Json(issues).into_response(), + )) + } } #[utoipa::path(get, path = "/users/first", responses( From e2950165f8fb8aa81160bdbca330def2c2a6c442 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 23 Jul 2024 09:52:27 +0200 Subject: [PATCH 359/430] derive Debug for Question enable debug prints in BaseHTTPClient --- rust/agama-lib/src/questions/model.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/questions/model.rs b/rust/agama-lib/src/questions/model.rs index df35c0763f..9499dc3b5e 100644 --- a/rust/agama-lib/src/questions/model.rs +++ b/rust/agama-lib/src/questions/model.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Question { pub generic: GenericQuestion, @@ -16,7 +16,7 @@ pub struct Question { /// API which has both as attributes, but web API separate /// question and its answer. So here it is split into GenericQuestion /// and GenericAnswer -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct GenericQuestion { /// id is optional as newly created questions does not have it assigned @@ -38,7 +38,7 @@ pub struct GenericQuestion { /// Also note that question is empty as QuestionWithPassword does not /// provide more details for question, but require additional answer. /// Can be potentionally extended in future e.g. with list of allowed characters? -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct QuestionWithPassword {} From 7ea3c0ad2b8b898eefa4b5f7d868560ea694801a Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 23 Jul 2024 10:36:28 +0200 Subject: [PATCH 360/430] Move RootConfig, RootPatchSettings to a model module --- rust/agama-lib/src/users.rs | 1 + rust/agama-lib/src/users/http_client.rs | 14 +------------ rust/agama-lib/src/users/model.rs | 21 ++++++++++++++++++++ rust/agama-server/src/users/web.rs | 26 +++++-------------------- rust/agama-server/src/web/docs.rs | 4 ++-- 5 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 rust/agama-lib/src/users/model.rs diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs index d570b15386..45fb90b649 100644 --- a/rust/agama-lib/src/users.rs +++ b/rust/agama-lib/src/users.rs @@ -3,6 +3,7 @@ mod client; mod http_client; pub mod proxies; +pub mod model; mod settings; mod store; diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index 72007f9fd0..7897461319 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -1,25 +1,14 @@ use anyhow::anyhow; // WIP -use serde::Deserialize; //use reqwest::StatusCode; use super::client::FirstUser; use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; +use crate::users::model::RootConfig; pub struct UsersHttpClient { client: BaseHTTPClient, } -// copying agama_server::users::web::RootConfig -// but not all derives -// #[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -#[derive(Clone, Debug, Default, Deserialize)] -pub struct RootConfig { - /// returns if password for root is set or not - password: bool, - /// empty string mean no sshkey is specified - sshkey: String, -} - impl UsersHttpClient { pub async fn new() -> Result { Ok(Self { @@ -91,7 +80,6 @@ impl UsersHttpClient { Ok(root_config.sshkey) } - /// SetRootSSHKey method pub async fn set_root_sshkey(&self, value: &str) -> Result { //Ok(self.users_proxy.set_root_sshkey(value).await?) diff --git a/rust/agama-lib/src/users/model.rs b/rust/agama-lib/src/users/model.rs new file mode 100644 index 0000000000..098b9fdd92 --- /dev/null +++ b/rust/agama-lib/src/users/model.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RootConfig { + /// returns if password for root is set or not + pub password: bool, + /// empty string mean no sshkey is specified + pub sshkey: String, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RootPatchSettings { + /// empty string here means remove ssh key for root + pub sshkey: Option, + /// empty string here means remove password for root + pub password: Option, + /// specify if patched password is provided in encrypted form + pub password_encrypted: Option, +} + diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index d360c8bc1f..12be77bf59 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -13,7 +13,11 @@ use crate::{ }; use agama_lib::{ error::ServiceError, - users::{proxies::Users1Proxy, FirstUser, UsersClient}, + users::{ + model::{RootConfig, RootPatchSettings}, + proxies::Users1Proxy, + FirstUser, UsersClient + }, }; use axum::{ extract::State, @@ -23,7 +27,6 @@ use axum::{ Json, Router, }; //use reqwest; -use serde::{Deserialize, Serialize}; use tokio_stream::{Stream, StreamExt}; #[derive(Clone)] @@ -191,17 +194,6 @@ async fn get_user_config(State(state): State>) -> Result, - /// empty string here means remove password for root - pub password: Option, - /// specify if patched password is provided in encrypted form - pub password_encrypted: Option, -} - #[utoipa::path(patch, path = "/users/root", responses( (status = 200, description = "Root configuration is modified", body = RootPatchSettings), (status = 400, description = "The D-Bus service could not perform the action"), @@ -226,14 +218,6 @@ async fn patch_root( Ok(()) } -#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -pub struct RootConfig { - /// returns if password for root is set or not - password: bool, - /// empty string mean no sshkey is specified - sshkey: String, -} - #[utoipa::path(get, path = "/users/root", responses( (status = 200, description = "Configuration for the root user", body = RootConfig), (status = 400, description = "The D-Bus service could not perform the action"), diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index af9d28eacd..2241a686c6 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -113,8 +113,8 @@ use utoipa::OpenApi; schemas(crate::storage::web::iscsi::InitiatorParams), schemas(crate::storage::web::iscsi::LoginParams), schemas(crate::storage::web::iscsi::NodeParams), - schemas(crate::users::web::RootConfig), - schemas(crate::users::web::RootPatchSettings), + schemas(agama_lib::users::model::RootConfig), + schemas(agama_lib::users::model::RootPatchSettings), schemas(super::http::PingResponse) ) )] From fc3f848534f894b234291083ab909863f82c8eb7 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 23 Jul 2024 14:19:12 +0200 Subject: [PATCH 361/430] set root password, set root ssh key (happy path) --- rust/agama-lib/src/base_http_client.rs | 37 ++++++++++++++++++ rust/agama-lib/src/users/http_client.rs | 52 ++++++++++--------------- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 9dd07c914c..1738fbc2ce 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -160,6 +160,43 @@ impl BaseHTTPClient { .map_err(|e| e.into()) } + /// patch object at given path and report error if response is not success + /// + /// Arguments: + /// + /// * `path`: path relative to HTTP API like `/users/first` + /// * `object`: Object that can be serialiazed to JSON as body of request. + pub async fn patch(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { + let response = self.patch_response(path, object).await?; + if response.status().is_success() { + Ok(()) + } else { + Err(self.build_backend_error(response).await) + } + } + + /// patch object at given path and returns server response. Reports error only if failed to send + /// request, but if server returns e.g. 500, it will be in Ok result. + /// + /// In general unless specific response handling is needed, simple post should be used. + /// + /// Arguments: + /// + /// * `path`: path relative to HTTP API like `/questions` + /// * `object`: Object that can be serialiazed to JSON as body of request. + pub async fn patch_response( + &self, + path: &str, + object: &impl Serialize, + ) -> Result { + self.client + .patch(self.url(path)) + .json(object) + .send() + .await + .map_err(|e| e.into()) + } + /// delete call on given path and report error if failed /// /// Arguments: diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index 7897461319..e400198dcc 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -1,9 +1,7 @@ -use anyhow::anyhow; // WIP - //use reqwest::StatusCode; use super::client::FirstUser; use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; -use crate::users::model::RootConfig; +use crate::users::model::{RootConfig, RootPatchSettings}; pub struct UsersHttpClient { client: BaseHTTPClient, @@ -27,28 +25,11 @@ impl UsersHttpClient { &self, first_user: &FirstUser, ) -> Result<(bool, Vec), ServiceError> { - /* - self.users_proxy - .set_first_user( - &first_user.full_name, - &first_user.user_name, - &first_user.password, - first_user.autologin, - std::collections::HashMap::new(), - ) - .await - */ - //Err(anyhow!("TODO implement UsersHttpClient SETUSER").into()) self.client.put("/users/first", first_user).await?; // TODO: make BaseHTTPClient.put(_response) return the issues Ok((true, vec!())) } - pub async fn remove_first_user(&self) -> Result { - //Ok(self.users_proxy.remove_first_user().await? == 0) - Err(anyhow!("TODO implement UsersHttpClient RMUSER").into()) - } - async fn root_config(&self) -> Result { self.client.get("/users/root").await } @@ -62,16 +43,18 @@ impl UsersHttpClient { /// SetRootPassword method pub async fn set_root_password( &self, - _value: &str, - _encrypted: bool, + value: &str, + encrypted: bool, ) -> Result { - //Ok(self.users_proxy.set_root_password(value, encrypted).await?) - Err(anyhow!("TODO implement UsersHttpClient SETPASS").into()) - } - - pub async fn remove_root_password(&self) -> Result { - //Ok(self.users_proxy.remove_root_password().await?) - Err(anyhow!("TODO implement UsersHttpClient RMPASS").into()) + let rps = RootPatchSettings { + sshkey: None, + password: Some(value.to_owned()), + password_encrypted: Some(encrypted) + }; + // TODO various errors + // current backend always returns 0 + let _ret = self.client.patch("/users/root", &rps).await?; + Ok(0) } /// Returns the SSH key for the root user @@ -82,7 +65,14 @@ impl UsersHttpClient { /// SetRootSSHKey method pub async fn set_root_sshkey(&self, value: &str) -> Result { - //Ok(self.users_proxy.set_root_sshkey(value).await?) - Err(anyhow!("TODO implement UsersHttpClient SETKEY {}", value).into()) + let rps = RootPatchSettings { + sshkey: Some(value.to_owned()), + password: None, + password_encrypted: None + }; + // TODO various errors + // current backend always returns 0 + let _ret = self.client.patch("/users/root", &rps).await?; + Ok(0) } } From f3a2d3ecb3b1d2ffc5f29cc1b71b144606b0e6af Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 24 Jul 2024 10:48:29 +0200 Subject: [PATCH 362/430] set_first_user: try improving error reporting Problem: When there is an issue ("trying to use reserved username") it is reported as > Anyhow(Backend call failed with status 422 and text '["There is a conflict between the entered\nusername and an existing username.\nTry another one."]') (which is a wrapped `ServiceError::BackendError`) even though the CLI is ready to handle a `ServiceError::WrongUser` Solution: let BaseHTTPClient::put return a deserialized response but this commit still does not parse the JSON and does not convert the Err --- rust/agama-lib/src/base_http_client.rs | 23 ++++++++++++++++++----- rust/agama-lib/src/users/http_client.rs | 13 ++++++++++--- rust/agama-server/src/users/web.rs | 20 ++++++++------------ 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 1738fbc2ce..b835af15e5 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -129,15 +129,27 @@ impl BaseHTTPClient { /// /// * `path`: path relative to HTTP API like `/users/first` /// * `object`: Object that can be serialiazed to JSON as body of request. - pub async fn put(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { - let response = self.put_response(path, object).await?; + pub async fn put(&self, path: &str, object: &impl Serialize) -> Result + where + T: DeserializeOwned + std::fmt::Debug, + { + let response = self.request_response(reqwest::Method::PUT, path, object).await?; if response.status().is_success() { - Ok(()) + if let Some(clen) = response.content_length() { + if clen == 0 { + println!("empty body"); + } + } + + let ret = response.json::().await.map_err(|e| e.into()); + println!("PUT returns: {:#?}", ret); + ret } else { Err(self.build_backend_error(response).await) } } + /// FIXME redoc /// post object to given path and returns server response. Reports error only if failed to send /// request, but if server returns e.g. 500, it will be in Ok result. /// @@ -147,13 +159,14 @@ impl BaseHTTPClient { /// /// * `path`: path relative to HTTP API like `/questions` /// * `object`: Object that can be serialiazed to JSON as body of request. - pub async fn put_response( + pub async fn request_response( &self, + method: reqwest::Method, path: &str, object: &impl Serialize, ) -> Result { self.client - .put(self.url(path)) + .request(method, self.url(path)) .json(object) .send() .await diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index e400198dcc..b1ec789dfc 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -25,9 +25,16 @@ impl UsersHttpClient { &self, first_user: &FirstUser, ) -> Result<(bool, Vec), ServiceError> { - self.client.put("/users/first", first_user).await?; - // TODO: make BaseHTTPClient.put(_response) return the issues - Ok((true, vec!())) + let result: Result, ServiceError> = self.client.put("/users/first", first_user).await; + if let Err(ServiceError::BackendError(422, ref issues_s)) = result { + // way to go: + // deserialize json from string + // and use (void) put + println!("ISSUUUS {}", issues_s); + } + + let issues = result?; + Ok((issues.is_empty(), issues)) } async fn root_config(&self) -> Result { diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 12be77bf59..72241ef60b 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -162,7 +162,6 @@ async fn remove_first_user(State(state): State>) -> Result<(), Er } #[utoipa::path(put, path = "/users/first", responses( - // TODO 204 empty response (status = 200, description = "Sets the first user"), (status = 400, description = "The D-Bus service could not perform the action"), (status = 422, description = "Invalid first user. Details are in body", body = Vec), @@ -171,19 +170,16 @@ async fn set_first_user( State(state): State>, Json(config): Json, ) -> Result { - // TODO: web ui not ready to handle this? // issues: for example, trying to use a system user id; empty password - // success: simply !issues.is_empty() + // success: simply issues.is_empty() let (_success, issues) = state.users.set_first_user(&config).await?; - if issues.is_empty() { - Ok((StatusCode::NO_CONTENT, ().into_response())) - } - else { - Ok(( - StatusCode::UNPROCESSABLE_ENTITY, - Json(issues).into_response(), - )) - } + let status = if issues.is_empty() { + StatusCode::OK + } else { + StatusCode::UNPROCESSABLE_ENTITY + }; + + Ok((status, Json(issues).into_response())) } #[utoipa::path(get, path = "/users/first", responses( From dd32fc5c14b37863050d5c2d65fcffbfab6e1c03 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 26 Jul 2024 11:59:25 +0200 Subject: [PATCH 363/430] UsersHttpClient::set_first_user use Result(<>), issues as Err put_void (a PUT that ignores the reponse body) --- rust/agama-lib/src/base_http_client.rs | 11 +++++++++++ rust/agama-lib/src/users/http_client.rs | 18 +++++------------- rust/agama-lib/src/users/store.rs | 5 +---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index b835af15e5..5849002744 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -149,6 +149,17 @@ impl BaseHTTPClient { } } + pub async fn put_void(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { + let response = self + .request_response(reqwest::Method::PUT, path, object) + .await?; + if response.status().is_success() { + Ok(()) + } else { + Err(self.build_backend_error(response).await) + } + } + /// FIXME redoc /// post object to given path and returns server response. Reports error only if failed to send /// request, but if server returns e.g. 500, it will be in Ok result. diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index b1ec789dfc..4d08450ff3 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -19,22 +19,14 @@ impl UsersHttpClient { self.client.get("/users/first").await } - /// Set the configuration for the first user - pub async fn set_first_user( - &self, - first_user: &FirstUser, - ) -> Result<(bool, Vec), ServiceError> { - let result: Result, ServiceError> = self.client.put("/users/first", first_user).await; + pub async fn set_first_user(&self, first_user: &FirstUser) -> Result<(), ServiceError> { + let result = self.client.put_void("/users/first", first_user).await; if let Err(ServiceError::BackendError(422, ref issues_s)) = result { - // way to go: - // deserialize json from string - // and use (void) put - println!("ISSUUUS {}", issues_s); + let issues: Vec = serde_json::from_str(issues_s)?; + return Err(ServiceError::WrongUser(issues)); } - - let issues = result?; - Ok((issues.is_empty(), issues)) + result } async fn root_config(&self) -> Result { diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 2199276da8..83adf84440 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -53,10 +53,7 @@ impl UsersStore { password: settings.password.clone().unwrap_or_default(), ..Default::default() }; - let (success, issues) = self.users_client.set_first_user(&first_user).await?; - if !success { - return Err(ServiceError::WrongUser(issues)); - } + self.users_client.set_first_user(&first_user).await?; Ok(()) } From 5abaf51572b88e02981f99e2adcb699e453ff288 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 26 Jul 2024 12:52:55 +0200 Subject: [PATCH 364/430] BaseHTTPClient cleanup - remove debug prints - document `request_response` and use it - document `put_void` (which ignores response body, whereas `put` deserializes it) --- rust/agama-lib/src/base_http_client.rs | 76 +++++++++----------------- 1 file changed, 27 insertions(+), 49 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 5849002744..0c14a7a5ba 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -54,13 +54,13 @@ impl BaseHTTPClient { /// Arguments: /// /// * `path`: path relative to HTTP API like `/questions` - pub async fn get(&self, path: &str) -> Result { + pub async fn get(&self, path: &str) -> Result + where + T: DeserializeOwned, + { let response = self.get_response(path).await?; - println!("GET response: {:?}", response); if response.status().is_success() { - let ret = response.json::().await.map_err(|e| e.into()); - println!("GET value: {:#?}", ret); - ret + response.json::().await.map_err(|e| e.into()) } else { Err(self.build_backend_error(response).await) } @@ -93,7 +93,9 @@ impl BaseHTTPClient { /// * `path`: path relative to HTTP API like `/questions` /// * `object`: Object that can be serialiazed to JSON as body of request. pub async fn post(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { - let response = self.post_response(path, object).await?; + let response = self + .request_response(reqwest::Method::POST, path, object) + .await?; if response.status().is_success() { Ok(()) } else { @@ -115,15 +117,11 @@ impl BaseHTTPClient { path: &str, object: &impl Serialize, ) -> Result { - self.client - .post(self.url(path)) - .json(object) - .send() + self.request_response(reqwest::Method::POST, path, object) .await - .map_err(|e| e.into()) } - /// put object to given path and report error if response is not success + /// put object to given path, deserializes the response /// /// Arguments: /// @@ -131,24 +129,24 @@ impl BaseHTTPClient { /// * `object`: Object that can be serialiazed to JSON as body of request. pub async fn put(&self, path: &str, object: &impl Serialize) -> Result where - T: DeserializeOwned + std::fmt::Debug, + T: DeserializeOwned, { - let response = self.request_response(reqwest::Method::PUT, path, object).await?; + let response = self + .request_response(reqwest::Method::PUT, path, object) + .await?; if response.status().is_success() { - if let Some(clen) = response.content_length() { - if clen == 0 { - println!("empty body"); - } - } - - let ret = response.json::().await.map_err(|e| e.into()); - println!("PUT returns: {:#?}", ret); - ret + response.json::().await.map_err(|e| e.into()) } else { Err(self.build_backend_error(response).await) } } + /// put object to given path and report error if response is not success + /// + /// Arguments: + /// + /// * `path`: path relative to HTTP API like `/users/first` + /// * `object`: Object that can be serialiazed to JSON as body of request. pub async fn put_void(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { let response = self .request_response(reqwest::Method::PUT, path, object) @@ -160,14 +158,15 @@ impl BaseHTTPClient { } } - /// FIXME redoc - /// post object to given path and returns server response. Reports error only if failed to send + /// POST/PUT/PATCH an object to a given path and returns server response. + /// Reports Err only if failed to send /// request, but if server returns e.g. 500, it will be in Ok result. /// /// In general unless specific response handling is needed, simple post should be used. /// /// Arguments: /// + /// * `method`: for example `reqwest::Method::PUT` /// * `path`: path relative to HTTP API like `/questions` /// * `object`: Object that can be serialiazed to JSON as body of request. pub async fn request_response( @@ -191,36 +190,15 @@ impl BaseHTTPClient { /// * `path`: path relative to HTTP API like `/users/first` /// * `object`: Object that can be serialiazed to JSON as body of request. pub async fn patch(&self, path: &str, object: &impl Serialize) -> Result<(), ServiceError> { - let response = self.patch_response(path, object).await?; + let response = self + .request_response(reqwest::Method::PATCH, path, object) + .await?; if response.status().is_success() { Ok(()) } else { Err(self.build_backend_error(response).await) } } - - /// patch object at given path and returns server response. Reports error only if failed to send - /// request, but if server returns e.g. 500, it will be in Ok result. - /// - /// In general unless specific response handling is needed, simple post should be used. - /// - /// Arguments: - /// - /// * `path`: path relative to HTTP API like `/questions` - /// * `object`: Object that can be serialiazed to JSON as body of request. - pub async fn patch_response( - &self, - path: &str, - object: &impl Serialize, - ) -> Result { - self.client - .patch(self.url(path)) - .json(object) - .send() - .await - .map_err(|e| e.into()) - } - /// delete call on given path and report error if failed /// /// Arguments: From 298cabc5f2d04f53972b86a5639333b732ffb157 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 26 Jul 2024 12:59:49 +0200 Subject: [PATCH 365/430] cleanup - cargo fmt --all -- - remove unused UsersStore arg - agama-server does not depend on reqwest --- rust/Cargo.lock | 1 - rust/agama-lib/src/store.rs | 3 +-- rust/agama-lib/src/users.rs | 2 +- rust/agama-lib/src/users/http_client.rs | 7 +++---- rust/agama-lib/src/users/model.rs | 1 - rust/agama-lib/src/users/store.rs | 3 +-- rust/agama-server/Cargo.toml | 2 -- rust/agama-server/src/users/web.rs | 10 ++-------- 8 files changed, 8 insertions(+), 21 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 74b7d373c3..9154b3659e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -113,7 +113,6 @@ dependencies = [ "pin-project", "rand", "regex", - "reqwest 0.12.4", "serde", "serde_json", "serde_with", diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index bfbf0a6906..0cf24f2c71 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -31,8 +31,7 @@ impl<'a> Store<'a> { ) -> Result, ServiceError> { Ok(Self { localization: LocalizationStore::new(connection.clone()).await?, - // FIXME: http clone ok? ref better? - users: UsersStore::new(http_client.clone()).await?, + users: UsersStore::new().await?, network: NetworkStore::new(http_client).await?, product: ProductStore::new(connection.clone()).await?, software: SoftwareStore::new(connection.clone()).await?, diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs index 45fb90b649..d838ecc651 100644 --- a/rust/agama-lib/src/users.rs +++ b/rust/agama-lib/src/users.rs @@ -2,8 +2,8 @@ mod client; mod http_client; -pub mod proxies; pub mod model; +pub mod proxies; mod settings; mod store; diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index 4d08450ff3..dd016f64d6 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -1,7 +1,6 @@ -//use reqwest::StatusCode; use super::client::FirstUser; -use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; use crate::users::model::{RootConfig, RootPatchSettings}; +use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; pub struct UsersHttpClient { client: BaseHTTPClient, @@ -48,7 +47,7 @@ impl UsersHttpClient { let rps = RootPatchSettings { sshkey: None, password: Some(value.to_owned()), - password_encrypted: Some(encrypted) + password_encrypted: Some(encrypted), }; // TODO various errors // current backend always returns 0 @@ -67,7 +66,7 @@ impl UsersHttpClient { let rps = RootPatchSettings { sshkey: Some(value.to_owned()), password: None, - password_encrypted: None + password_encrypted: None, }; // TODO various errors // current backend always returns 0 diff --git a/rust/agama-lib/src/users/model.rs b/rust/agama-lib/src/users/model.rs index 098b9fdd92..062768bd81 100644 --- a/rust/agama-lib/src/users/model.rs +++ b/rust/agama-lib/src/users/model.rs @@ -18,4 +18,3 @@ pub struct RootPatchSettings { /// specify if patched password is provided in encrypted form pub password_encrypted: Option, } - diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 83adf84440..4dd4f370b4 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -1,6 +1,5 @@ use super::{FirstUser, FirstUserSettings, RootUserSettings, UserSettings, UsersHttpClient}; use crate::error::ServiceError; -use reqwest; /// Loads and stores the users settings from/to the D-Bus service. pub struct UsersStore { @@ -8,7 +7,7 @@ pub struct UsersStore { } impl UsersStore { - pub async fn new(_client: reqwest::Client) -> Result { + pub async fn new() -> Result { Ok(Self { users_client: UsersHttpClient::new().await?, }) diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 7c5475a773..52386a3005 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -57,8 +57,6 @@ futures-util = { version = "0.3.30", default-features = false, features = [ libsystemd = "0.7.0" subprocess = "0.2.9" gethostname = "0.4.3" -# FIXME probably wrong here, copied from agama-lib -reqwest = { version = "0.12.4", features = ["json", "cookies"] } [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 72241ef60b..5d3d35a54b 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -16,16 +16,10 @@ use agama_lib::{ users::{ model::{RootConfig, RootPatchSettings}, proxies::Users1Proxy, - FirstUser, UsersClient + FirstUser, UsersClient, }, }; -use axum::{ - extract::State, - http::StatusCode, - response::IntoResponse, - routing::get, - Json, Router, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; //use reqwest; use tokio_stream::{Stream, StreamExt}; From e094efbe911b1f6b9aaa82f6a4862afdb57d745f Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 26 Jul 2024 13:18:35 +0200 Subject: [PATCH 366/430] hint for fixing a failed test run --- rust/agama-server/src/l10n/locale.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/agama-server/src/l10n/locale.rs b/rust/agama-server/src/l10n/locale.rs index 4ea85bc03d..b6821ac88f 100644 --- a/rust/agama-server/src/l10n/locale.rs +++ b/rust/agama-server/src/l10n/locale.rs @@ -147,7 +147,10 @@ mod tests { db.read("de").unwrap(); let found_locales = db.entries(); let spanish: LocaleId = "es_ES".try_into().unwrap(); - let found = found_locales.iter().find(|l| l.id == spanish).unwrap(); + let found = found_locales + .iter() + .find(|l| l.id == spanish) + .expect("Spanish locale not found?! Suggestion: zypper in glibc-locale"); assert_eq!(&found.language, "Spanisch"); assert_eq!(&found.territory, "Spanien"); } From 3458c21e8dd71c98a262bcce4e57585ceefd8396 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 26 Jul 2024 14:47:13 +0200 Subject: [PATCH 367/430] consistent spelling: UsersHTTPClient --- rust/agama-lib/src/users.rs | 2 +- rust/agama-lib/src/users/http_client.rs | 4 ++-- rust/agama-lib/src/users/store.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs index d838ecc651..21e4b4b9f3 100644 --- a/rust/agama-lib/src/users.rs +++ b/rust/agama-lib/src/users.rs @@ -8,6 +8,6 @@ mod settings; mod store; pub use client::{FirstUser, UsersClient}; -pub use http_client::UsersHttpClient; +pub use http_client::UsersHTTPClient; pub use settings::{FirstUserSettings, RootUserSettings, UserSettings}; pub use store::UsersStore; diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index dd016f64d6..bf89d0f269 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -2,11 +2,11 @@ use super::client::FirstUser; use crate::users::model::{RootConfig, RootPatchSettings}; use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; -pub struct UsersHttpClient { +pub struct UsersHTTPClient { client: BaseHTTPClient, } -impl UsersHttpClient { +impl UsersHTTPClient { pub async fn new() -> Result { Ok(Self { client: BaseHTTPClient::new()?, diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 4dd4f370b4..c5f71fc206 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -1,15 +1,15 @@ -use super::{FirstUser, FirstUserSettings, RootUserSettings, UserSettings, UsersHttpClient}; +use super::{FirstUser, FirstUserSettings, RootUserSettings, UserSettings, UsersHTTPClient}; use crate::error::ServiceError; /// Loads and stores the users settings from/to the D-Bus service. pub struct UsersStore { - users_client: UsersHttpClient, + users_client: UsersHTTPClient, } impl UsersStore { pub async fn new() -> Result { Ok(Self { - users_client: UsersHttpClient::new().await?, + users_client: UsersHTTPClient::new().await?, }) } From 9c71690f377f98ef43121e260313f5871b5076d5 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Mon, 29 Jul 2024 10:16:09 +0200 Subject: [PATCH 368/430] Split out agama_lib::localization::model::LocaleConfig so that the CLI client can use it too --- rust/agama-lib/src/localization.rs | 1 + rust/agama-lib/src/localization/model.rs | 16 ++++++++++++++++ rust/agama-server/src/l10n.rs | 2 +- rust/agama-server/src/l10n/web.rs | 19 ++----------------- rust/agama-server/src/web/docs.rs | 2 +- rust/agama-server/src/web/event.rs | 7 ++++--- 6 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 rust/agama-lib/src/localization/model.rs diff --git a/rust/agama-lib/src/localization.rs b/rust/agama-lib/src/localization.rs index 65bb1ae8bc..78d1957bc5 100644 --- a/rust/agama-lib/src/localization.rs +++ b/rust/agama-lib/src/localization.rs @@ -1,6 +1,7 @@ //! Implements support for handling the localization settings mod client; +pub mod model; mod proxies; mod settings; mod store; diff --git a/rust/agama-lib/src/localization/model.rs b/rust/agama-lib/src/localization/model.rs new file mode 100644 index 0000000000..0fd38c4661 --- /dev/null +++ b/rust/agama-lib/src/localization/model.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocaleConfig { + /// Locales to install in the target system + pub locales: Option>, + /// Keymap for the target system + pub keymap: Option, + /// Timezone for the target system + pub timezone: Option, + /// User-interface locale. It is actually not related to the `locales` property. + pub ui_locale: Option, + /// User-interface locale. It is relevant only on local installations. + pub ui_keymap: Option, +} diff --git a/rust/agama-server/src/l10n.rs b/rust/agama-server/src/l10n.rs index 3ef747cd47..51d4c135b9 100644 --- a/rust/agama-server/src/l10n.rs +++ b/rust/agama-server/src/l10n.rs @@ -7,10 +7,10 @@ mod locale; mod timezone; pub mod web; +pub use agama_lib::localization::model::LocaleConfig; pub use dbus::export_dbus_objects; pub use error::LocaleError; pub use keyboard::Keymap; pub use l10n::L10n; pub use locale::LocaleEntry; pub use timezone::TimezoneEntry; -pub use web::LocaleConfig; diff --git a/rust/agama-server/src/l10n/web.rs b/rust/agama-server/src/l10n/web.rs index dfdab552de..4f668afe50 100644 --- a/rust/agama-server/src/l10n/web.rs +++ b/rust/agama-server/src/l10n/web.rs @@ -8,7 +8,8 @@ use crate::{ web::{Event, EventsSender}, }; use agama_lib::{ - error::ServiceError, localization::LocaleProxy, proxies::LocaleProxy as ManagerLocaleProxy, + error::ServiceError, localization::model::LocaleConfig, localization::LocaleProxy, + proxies::LocaleProxy as ManagerLocaleProxy, }; use agama_locale_data::LocaleId; use axum::{ @@ -18,7 +19,6 @@ use axum::{ routing::{get, patch}, Json, Router, }; -use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; @@ -66,21 +66,6 @@ async fn locales(State(state): State>) -> Json> Json(locales) } -#[derive(Clone, Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocaleConfig { - /// Locales to install in the target system - locales: Option>, - /// Keymap for the target system - keymap: Option, - /// Timezone for the target system - timezone: Option, - /// User-interface locale. It is actually not related to the `locales` property. - ui_locale: Option, - /// User-interface locale. It is relevant only on local installations. - ui_keymap: Option, -} - #[utoipa::path( get, path = "/timezones", diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 2241a686c6..b913f3974c 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -96,7 +96,7 @@ use utoipa::OpenApi; schemas(crate::l10n::Keymap), schemas(crate::l10n::LocaleEntry), schemas(crate::l10n::TimezoneEntry), - schemas(crate::l10n::web::LocaleConfig), + schemas(agama_lib::localization::model::LocaleConfig), schemas(crate::manager::web::InstallerStatus), schemas(crate::network::model::Connection), schemas(crate::network::model::Device), diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 5b11eae12b..20075ce00a 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -1,7 +1,8 @@ -use crate::{l10n::web::LocaleConfig, network::model::NetworkChange}; +use crate::network::model::NetworkChange; use agama_lib::{ - manager::InstallationPhase, product::RegistrationRequirement, progress::Progress, - software::SelectedBy, storage::ISCSINode, users::FirstUser, + localization::model::LocaleConfig, manager::InstallationPhase, + product::RegistrationRequirement, progress::Progress, software::SelectedBy, storage::ISCSINode, + users::FirstUser, }; use serde::Serialize; use std::collections::HashMap; From 074cbb4e3cba60ce5f5060fccd8819a2b5054a33 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Mon, 29 Jul 2024 12:29:19 +0200 Subject: [PATCH 369/430] use LocalizationHTTPClient --- rust/agama-lib/src/localization.rs | 2 + .../agama-lib/src/localization/http_client.rs | 22 ++++++ rust/agama-lib/src/localization/settings.rs | 1 + rust/agama-lib/src/localization/store.rs | 67 +++++++++++-------- rust/agama-lib/src/store.rs | 4 +- 5 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 rust/agama-lib/src/localization/http_client.rs diff --git a/rust/agama-lib/src/localization.rs b/rust/agama-lib/src/localization.rs index 78d1957bc5..85031324dd 100644 --- a/rust/agama-lib/src/localization.rs +++ b/rust/agama-lib/src/localization.rs @@ -1,12 +1,14 @@ //! Implements support for handling the localization settings mod client; +mod http_client; pub mod model; mod proxies; mod settings; mod store; pub use client::LocalizationClient; +pub use http_client::LocalizationHTTPClient; pub use proxies::LocaleProxy; pub use settings::LocalizationSettings; pub use store::LocalizationStore; diff --git a/rust/agama-lib/src/localization/http_client.rs b/rust/agama-lib/src/localization/http_client.rs new file mode 100644 index 0000000000..ffabe79a2d --- /dev/null +++ b/rust/agama-lib/src/localization/http_client.rs @@ -0,0 +1,22 @@ +use super::model::LocaleConfig; +use crate::{base_http_client::BaseHTTPClient, error::ServiceError}; + +pub struct LocalizationHTTPClient { + client: BaseHTTPClient, +} + +impl LocalizationHTTPClient { + pub async fn new() -> Result { + Ok(Self { + client: BaseHTTPClient::new()?, + }) + } + + pub async fn get_config(&self) -> Result { + self.client.get("/l10n/config").await + } + + pub async fn set_config(&self, config: &LocaleConfig) -> Result<(), ServiceError> { + self.client.patch("/l10n/config", config).await + } +} diff --git a/rust/agama-lib/src/localization/settings.rs b/rust/agama-lib/src/localization/settings.rs index a3692c8ddf..27f6bd5349 100644 --- a/rust/agama-lib/src/localization/settings.rs +++ b/rust/agama-lib/src/localization/settings.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; /// Localization settings for the system being installed (not the UI) +/// FIXME: this one is close to CLI. A possible duplicate close to HTTP is LocaleConfig #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LocalizationSettings { diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index 8ae25f7f43..1ee00dec43 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -1,51 +1,62 @@ //! Implements the store for the localization settings. // TODO: for an overview see crate::store (?) -use super::{LocalizationClient, LocalizationSettings}; +use super::{LocalizationHTTPClient, LocalizationSettings}; use crate::error::ServiceError; -use zbus::Connection; +use crate::localization::model::LocaleConfig; /// Loads and stores the storage settings from/to the D-Bus service. -pub struct LocalizationStore<'a> { - localization_client: LocalizationClient<'a>, +pub struct LocalizationStore { + localization_client: LocalizationHTTPClient, } -impl<'a> LocalizationStore<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { +impl LocalizationStore { + pub async fn new() -> Result { Ok(Self { - localization_client: LocalizationClient::new(connection).await?, + localization_client: LocalizationHTTPClient::new().await?, }) } + /// Consume *v* and return its first element, or None. + /// This is similar to VecDeque::pop_front but it consumes the whole Vec. + fn chestburster(mut v: Vec) -> Option { + if v.is_empty() { + None + } else { + Some(v.swap_remove(0)) + } + } + pub async fn load(&self) -> Result { - // TODO: we should use a single D-Bus call with Properties.GetAll - // but LocaleProxy does not have it, only get_property for individual methods - // and properties_proxy is private + let config = self.localization_client.get_config().await?; - let opt_language = self.localization_client.language().await?; - let keyboard = self.localization_client.keyboard().await?; - let timezone = self.localization_client.timezone().await?; + let opt_language = config + .locales + .map(|vec_s| Self::chestburster(vec_s)) + .flatten(); + let opt_keyboard = config.keymap; + let opt_timezone = config.timezone; Ok(LocalizationSettings { language: opt_language, - keyboard: Some(keyboard), - timezone: Some(timezone), + keyboard: opt_keyboard, + timezone: opt_timezone, }) } pub async fn store(&self, settings: &LocalizationSettings) -> Result<(), ServiceError> { - if let Some(language) = &settings.language { - self.localization_client.set_language(language).await?; - } - - if let Some(keyboard) = &settings.keyboard { - self.localization_client.set_keyboard(keyboard).await?; - } - - if let Some(timezone) = &settings.timezone { - self.localization_client.set_timezone(timezone).await?; - } - - Ok(()) + // clones are necessary as we have different structs owning their data + let opt_language = settings.language.clone(); + let opt_keymap = settings.keyboard.clone(); + let opt_timezone = settings.timezone.clone(); + + let config = LocaleConfig { + locales: opt_language.map(|s| vec![s]), + keymap: opt_keymap, + timezone: opt_timezone, + ui_locale: None, + ui_keymap: None, + }; + self.localization_client.set_config(&config).await } } diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 0cf24f2c71..a9c72b8641 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -21,7 +21,7 @@ pub struct Store<'a> { product: ProductStore<'a>, software: SoftwareStore<'a>, storage: StorageStore<'a>, - localization: LocalizationStore<'a>, + localization: LocalizationStore, } impl<'a> Store<'a> { @@ -30,7 +30,7 @@ impl<'a> Store<'a> { http_client: reqwest::Client, ) -> Result, ServiceError> { Ok(Self { - localization: LocalizationStore::new(connection.clone()).await?, + localization: LocalizationStore::new().await?, users: UsersStore::new().await?, network: NetworkStore::new(http_client).await?, product: ProductStore::new(connection.clone()).await?, From 6fcbf2f477046079294842692b2ca979c2c5475e Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Mon, 29 Jul 2024 12:34:13 +0200 Subject: [PATCH 370/430] remove LocalizationClient (D-Bus), no longer used --- rust/agama-lib/src/localization.rs | 2 - rust/agama-lib/src/localization/client.rs | 54 ----------------------- 2 files changed, 56 deletions(-) delete mode 100644 rust/agama-lib/src/localization/client.rs diff --git a/rust/agama-lib/src/localization.rs b/rust/agama-lib/src/localization.rs index 85031324dd..a53402db98 100644 --- a/rust/agama-lib/src/localization.rs +++ b/rust/agama-lib/src/localization.rs @@ -1,13 +1,11 @@ //! Implements support for handling the localization settings -mod client; mod http_client; pub mod model; mod proxies; mod settings; mod store; -pub use client::LocalizationClient; pub use http_client::LocalizationHTTPClient; pub use proxies::LocaleProxy; pub use settings::LocalizationSettings; diff --git a/rust/agama-lib/src/localization/client.rs b/rust/agama-lib/src/localization/client.rs deleted file mode 100644 index 2b15ea2c78..0000000000 --- a/rust/agama-lib/src/localization/client.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::proxies::LocaleProxy; -use crate::error::ServiceError; -use zbus::Connection; - -/// D-Bus client for the software service -#[derive(Clone)] -pub struct LocalizationClient<'a> { - localization_proxy: LocaleProxy<'a>, -} - -impl<'a> LocalizationClient<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { - Ok(Self { - localization_proxy: LocaleProxy::new(&connection).await?, - }) - } - - pub async fn language(&self) -> Result, ServiceError> { - let locales = self.localization_proxy.locales().await?; - let mut iter = locales.into_iter(); - let first = iter.next(); - // may be None - Ok(first) - } - - pub async fn locales(&self) -> zbus::Result> { - self.localization_proxy.locales().await - } - - pub async fn keyboard(&self) -> Result { - Ok(self.localization_proxy.keymap().await?) - } - - pub async fn timezone(&self) -> Result { - Ok(self.localization_proxy.timezone().await?) - } - - pub async fn set_language(&self, language: &str) -> zbus::Result<()> { - let locales = [language]; - self.localization_proxy.set_locales(&locales).await - } - - pub async fn set_locales(&self, locales: &[&str]) -> zbus::Result<()> { - self.localization_proxy.set_locales(locales).await - } - - pub async fn set_keyboard(&self, keyboard: &str) -> zbus::Result<()> { - self.localization_proxy.set_keymap(keyboard).await - } - - pub async fn set_timezone(&self, timezone: &str) -> zbus::Result<()> { - self.localization_proxy.set_timezone(timezone).await - } -} From e5cd7b44789445a2cbedb218997d0ffecabc3882 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Mon, 29 Jul 2024 13:07:43 +0200 Subject: [PATCH 371/430] chore: fix `cargo doc` links --- rust/agama-cli/src/profile.rs | 2 +- rust/agama-cli/src/questions.rs | 2 +- rust/agama-lib/src/auth.rs | 2 +- rust/agama-lib/src/base_http_client.rs | 2 +- rust/agama-lib/src/dbus.rs | 2 +- rust/agama-lib/src/lib.rs | 2 +- rust/agama-locale-data/src/keyboard/xkb_config_registry.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rust/agama-cli/src/profile.rs b/rust/agama-cli/src/profile.rs index c264e777d4..d2f0aa559b 100644 --- a/rust/agama-cli/src/profile.rs +++ b/rust/agama-cli/src/profile.rs @@ -38,7 +38,7 @@ pub enum ProfileCommands { /// Evaluate a profile, injecting the hardware information from D-Bus /// /// For an example of Jsonnet-based profile, see - /// https://github.com/openSUSE/agama/blob/master/rust/agama-lib/share/examples/profile.jsonnet + /// Evaluate { /// Path to jsonnet file. path: PathBuf, diff --git a/rust/agama-cli/src/questions.rs b/rust/agama-cli/src/questions.rs index 6d0d9c05a8..a5bceab40a 100644 --- a/rust/agama-cli/src/questions.rs +++ b/rust/agama-cli/src/questions.rs @@ -15,7 +15,7 @@ pub enum QuestionsCommands { /// mode or change the answer in automatic mode. /// /// Please check Agama documentation for more details and examples: - /// https://github.com/openSUSE/agama/blob/master/doc/questions.md + /// Answers { /// Path to a file containing the answers in YAML format. path: String, diff --git a/rust/agama-lib/src/auth.rs b/rust/agama-lib/src/auth.rs index ab48f8a399..b2da8b288c 100644 --- a/rust/agama-lib/src/auth.rs +++ b/rust/agama-lib/src/auth.rs @@ -166,7 +166,7 @@ impl Display for AuthToken { /// Claims that are included in the token. /// -/// See https://datatracker.ietf.org/doc/html/rfc7519 for reference. +/// See for reference. #[derive(Debug, Serialize, Deserialize)] pub struct TokenClaims { pub exp: i64, diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 0c14a7a5ba..6467ec6bc3 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -231,7 +231,7 @@ impl BaseHTTPClient { } const NO_TEXT: &'static str = "(Failed to extract error text from HTTP response)"; - /// Builds [`BackendError`] from response. + /// Builds [`ServiceError::BackendError`] from response. /// /// It contains also processing of response body, that is why it has to be async. /// diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-lib/src/dbus.rs index 6ff2e6eab4..3d07027a5e 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-lib/src/dbus.rs @@ -58,7 +58,7 @@ macro_rules! property_from_dbus { /// NOTE: we could follow a different approach like building our own type (e.g. /// using the newtype idiom) and offering a better API. /// -/// * `source`: hash map containing non-onwed values ([zbus::zvariant::Value]). +/// * `source`: hash map containing non-onwed values ([enum@zbus::zvariant::Value]). pub fn to_owned_hash(source: &HashMap<&str, Value<'_>>) -> HashMap { let mut owned = HashMap::new(); for (key, value) in source.iter() { diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 3dfffd3e07..1bbc4c48c8 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -3,7 +3,7 @@ //! This library offers an API to interact with Agama services. At this point, the library allows: //! //! * Reading and writing [installation settings](install_settings::InstallSettings). -//! * Monitoring the [progress](progress). +//! * Monitoring the [progress]. //! * Triggering actions through the [manager] (e.g., starting installation). //! //! ## Handling installation settings diff --git a/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs b/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs index a462f5cffe..62ff1cea88 100644 --- a/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs +++ b/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs @@ -1,6 +1,6 @@ //! This module aims to read the information in the X Keyboard Configuration Database. //! -//! https://freedesktop.org/Software/XKeyboardConfig +//! use quick_xml::de::from_str; use serde::Deserialize; From e6ccc55e8afe0d300060b495810f484ea87a5083 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 30 Jul 2024 08:41:06 +0200 Subject: [PATCH 372/430] BaseHTTPClient::unit_or_error factored out as suggested by Josef --- rust/agama-lib/src/base_http_client.rs | 33 ++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 6467ec6bc3..b6c6d80788 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -96,11 +96,7 @@ impl BaseHTTPClient { let response = self .request_response(reqwest::Method::POST, path, object) .await?; - if response.status().is_success() { - Ok(()) - } else { - Err(self.build_backend_error(response).await) - } + self.unit_or_error(response).await } /// post object to given path and returns server response. Reports error only if failed to send @@ -151,11 +147,7 @@ impl BaseHTTPClient { let response = self .request_response(reqwest::Method::PUT, path, object) .await?; - if response.status().is_success() { - Ok(()) - } else { - Err(self.build_backend_error(response).await) - } + self.unit_or_error(response).await } /// POST/PUT/PATCH an object to a given path and returns server response. @@ -193,11 +185,7 @@ impl BaseHTTPClient { let response = self .request_response(reqwest::Method::PATCH, path, object) .await?; - if response.status().is_success() { - Ok(()) - } else { - Err(self.build_backend_error(response).await) - } + self.unit_or_error(response).await } /// delete call on given path and report error if failed /// @@ -206,11 +194,7 @@ impl BaseHTTPClient { /// * `path`: path relative to HTTP API like `/questions/1` pub async fn delete(&self, path: &str) -> Result<(), ServiceError> { let response = self.delete_response(path).await?; - if response.status().is_success() { - Ok(()) - } else { - Err(self.build_backend_error(response).await) - } + self.unit_or_error(response).await } /// delete call on given path and returns server response. Reports error only if failed to send @@ -230,6 +214,15 @@ impl BaseHTTPClient { .map_err(|e| e.into()) } + /// Return `Ok(())` or an `Err` with [`ServiceError::BackendError`] + async fn unit_or_error(&self, response: Response) -> Result<(), ServiceError> { + if response.status().is_success() { + Ok(()) + } else { + Err(self.build_backend_error(response).await) + } + } + const NO_TEXT: &'static str = "(Failed to extract error text from HTTP response)"; /// Builds [`ServiceError::BackendError`] from response. /// From b8b3541fb7acc51301c6cc4a882e8f9358b0df9b Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Tue, 30 Jul 2024 14:51:14 +0200 Subject: [PATCH 373/430] test: LocalizationStore and LocalizationHTTPClient on a happy path failure expected, no mocking yet --- rust/agama-lib/src/localization/settings.rs | 2 +- rust/agama-lib/src/localization/store.rs | 24 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/rust/agama-lib/src/localization/settings.rs b/rust/agama-lib/src/localization/settings.rs index 27f6bd5349..d153c9706b 100644 --- a/rust/agama-lib/src/localization/settings.rs +++ b/rust/agama-lib/src/localization/settings.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; /// Localization settings for the system being installed (not the UI) /// FIXME: this one is close to CLI. A possible duplicate close to HTTP is LocaleConfig -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct LocalizationSettings { /// like "en_US.UTF-8" diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index 1ee00dec43..5f81013c42 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -60,3 +60,27 @@ impl LocalizationStore { self.localization_client.set_config(&config).await } } + +#[cfg(test)] +mod test { + use super::*; + use std::error::Error; + // without this, "error: async functions cannot be used for tests" + use tokio::test; + + #[test] + async fn test_getting_l10n() -> Result<(), Box> { + // TODO: setup a service that returns what I expect + + let store = LocalizationStore::new().await?; + let settings = store.load().await?; + + let expected = LocalizationSettings { + language: Some("fr_FR.UTF-8".to_owned()), + keyboard: Some("fr(dvorak)".to_owned()), + timezone: Some("Europe/Paris".to_owned()), + }; + assert_eq!(settings, expected); + Ok(()) + } +} From 7369123af883dfe5020ce55c10a1322f8ddebb42 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 31 Jul 2024 09:02:05 +0200 Subject: [PATCH 374/430] test: make it green by running a mock HTTP server httpmock - It is what I found with DDG: rust mock http request No idea about alternatives, this one fits well my simple use case, but does bring a load of dependencies (how much?) --- rust/Cargo.lock | 402 +++++++++++++++++- rust/agama-lib/Cargo.toml | 3 + rust/agama-lib/src/base_http_client.rs | 6 + .../agama-lib/src/localization/http_client.rs | 4 + rust/agama-lib/src/localization/store.rs | 36 +- 5 files changed, 442 insertions(+), 9 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9154b3659e..385469eb93 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -54,6 +54,7 @@ dependencies = [ "curl", "futures-util", "home", + "httpmock", "jsonschema", "jsonwebtoken", "log", @@ -242,6 +243,35 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "async-broadcast" version = "0.5.1" @@ -252,6 +282,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-channel" version = "2.3.1" @@ -277,6 +318,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.0", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io 2.3.3", + "async-lock 3.4.0", + "blocking", + "futures-lite 2.3.0", + "once_cell", +] + [[package]] name = "async-io" version = "1.13.0" @@ -336,6 +405,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-object-pool" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb901c30ebc2fc4ab46395bbfbdba9542c16559d853645d75190c3056caf3bc" +dependencies = [ + "async-std", +] + [[package]] name = "async-process" version = "1.8.1" @@ -382,6 +460,34 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -548,6 +654,17 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "basic-cookies" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + [[package]] name = "bindgen" version = "0.69.4" @@ -557,7 +674,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "proc-macro2", @@ -619,7 +736,7 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel", + "async-channel 2.3.1", "async-task", "futures-io", "futures-lite 2.3.0", @@ -1076,6 +1193,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -1097,6 +1235,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1212,6 +1359,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.30" @@ -1310,7 +1463,10 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ + "fastrand 2.1.0", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -1426,6 +1582,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "h2" version = "0.3.26" @@ -1622,6 +1790,34 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "httpmock" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-std", + "async-trait", + "base64 0.21.7", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper 0.14.30", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + [[package]] name = "hyper" version = "0.14.30" @@ -1874,6 +2070,15 @@ dependencies = [ "nom", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1951,6 +2156,46 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1963,12 +2208,28 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "libsystemd" version = "0.7.0" @@ -2045,6 +2306,9 @@ name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] [[package]] name = "macaddr" @@ -2166,6 +2430,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "newline-converter" version = "0.3.0" @@ -2563,13 +2833,23 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.2.6", +] + [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_shared", + "phf_shared 0.11.2", ] [[package]] @@ -2579,7 +2859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.11.2", ] [[package]] @@ -2588,10 +2868,19 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared", + "phf_shared 0.11.2", "rand", ] +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "phf_shared" version = "0.11.2" @@ -2601,6 +2890,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.5" @@ -2699,6 +2994,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2816,6 +3117,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -3067,6 +3379,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.23" @@ -3147,6 +3468,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -3289,6 +3620,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "simple_asn1" version = "0.6.2" @@ -3354,6 +3691,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3449,6 +3799,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "terminal_size" version = "0.3.0" @@ -3914,6 +4275,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -4000,6 +4367,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4018,6 +4391,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -4125,6 +4508,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index e6101ba679..a6689c3179 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -28,3 +28,6 @@ curl = { version = "0.4.44", features = ["protocol-ftp"] } jsonwebtoken = "9.3.0" chrono = { version = "0.4.38", default-features = false, features = ["now", "std", "alloc", "clock"] } home = "0.5.9" + +[dev-dependencies] +httpmock = "0.7.0" diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index b6c6d80788..41e092d6b4 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -47,6 +47,12 @@ impl BaseHTTPClient { }) } + pub fn new_with_url(url: String) -> Result { + let mut client = Self::new()?; + client.base_url = url; + Ok(client) + } + /// Simple wrapper around [`Response`] to get object from response. /// /// If a complete [`Response`] is needed, use the [`Self::get_response`] method. diff --git a/rust/agama-lib/src/localization/http_client.rs b/rust/agama-lib/src/localization/http_client.rs index ffabe79a2d..1312ec0eec 100644 --- a/rust/agama-lib/src/localization/http_client.rs +++ b/rust/agama-lib/src/localization/http_client.rs @@ -12,6 +12,10 @@ impl LocalizationHTTPClient { }) } + pub async fn new_with_base(base: BaseHTTPClient) -> Result { + Ok(Self { client: base }) + } + pub async fn get_config(&self) -> Result { self.client.get("/l10n/config").await } diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index 5f81013c42..d9b8d602fc 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -17,6 +17,14 @@ impl LocalizationStore { }) } + pub async fn new_with_client( + client: LocalizationHTTPClient, + ) -> Result { + Ok(Self { + localization_client: client, + }) + } + /// Consume *v* and return its first element, or None. /// This is similar to VecDeque::pop_front but it consumes the whole Vec. fn chestburster(mut v: Vec) -> Option { @@ -64,17 +72,37 @@ impl LocalizationStore { #[cfg(test)] mod test { use super::*; + use crate::base_http_client::BaseHTTPClient; + use httpmock::prelude::*; use std::error::Error; - // without this, "error: async functions cannot be used for tests" - use tokio::test; + use tokio::test; // without this, "error: async functions cannot be used for tests" #[test] async fn test_getting_l10n() -> Result<(), Box> { - // TODO: setup a service that returns what I expect + let server = MockServer::start(); + let l10n_mock = server.mock(|when, then| { + when.method(GET).path("/api/l10n/config"); + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "locales": ["fr_FR.UTF-8"], + "keymap": "fr(dvorak)", + "timezone": "Europe/Paris" + }"#, + ); + }); + let url = server.url("/api"); + + let bhc = BaseHTTPClient::new_with_url(url)?; + let client = LocalizationHTTPClient::new_with_base(bhc).await?; + let store = LocalizationStore::new_with_client(client).await?; - let store = LocalizationStore::new().await?; let settings = store.load().await?; + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + l10n_mock.assert(); + let expected = LocalizationSettings { language: Some("fr_FR.UTF-8".to_owned()), keyboard: Some("fr(dvorak)".to_owned()), From 7f89dc17252e506079e2e4cdd7c28cc826a01bfc Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 31 Jul 2024 13:16:05 +0200 Subject: [PATCH 375/430] BaseHTTPClient: fields made public, better constructor API, Default --- rust/agama-lib/src/base_http_client.rs | 42 +++++++++++++++--------- rust/agama-lib/src/localization/store.rs | 5 ++- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 41e092d6b4..9cff276d15 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -1,4 +1,4 @@ -use reqwest::{header, Client, Response}; +use reqwest::{header, Response}; use serde::{de::DeserializeOwned, Serialize}; use crate::{auth::AuthToken, error::ServiceError}; @@ -21,15 +21,36 @@ use crate::{auth::AuthToken, error::ServiceError}; /// } /// ``` pub struct BaseHTTPClient { - client: Client, + pub client: reqwest::Client, pub base_url: String, } const API_URL: &str = "http://localhost/api"; +impl Default for BaseHTTPClient { + /// A `default` client + /// - is NOT authenticated (maybe you want to call `new` instead) + /// - uses `localhost` + fn default() -> Self { + Self { + client: reqwest::Client::new(), + base_url: API_URL.to_owned(), + } + } +} + impl BaseHTTPClient { - // if there is need for client without authorization, create new constructor for it + /// Uses `localhost`, authenticates with [`AuthToken`]. pub fn new() -> Result { + Ok(Self { + client: Self::authenticated_reqwest_client()?, + ..Default::default() + }) + } + + fn authenticated_reqwest_client() -> Result { + // TODO: this error is subtly misleading, leading me to believe the SERVER said it, + // but in fact it is the CLIENT not finding an auth token let token = AuthToken::find().ok_or(ServiceError::NotAuthenticated)?; let mut headers = header::HeaderMap::new(); @@ -39,18 +60,9 @@ impl BaseHTTPClient { headers.insert(header::AUTHORIZATION, value); - let client = Client::builder().default_headers(headers).build()?; - - Ok(Self { - client, - base_url: API_URL.to_string(), // TODO: add support for remote server - }) - } - - pub fn new_with_url(url: String) -> Result { - let mut client = Self::new()?; - client.base_url = url; - Ok(client) + Ok(reqwest::Client::builder() + .default_headers(headers) + .build()?) } /// Simple wrapper around [`Response`] to get object from response. diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index d9b8d602fc..c68074636c 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -94,7 +94,10 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient::new_with_url(url)?; + let bhc = BaseHTTPClient { + base_url: url, + ..Default::default() + }; let client = LocalizationHTTPClient::new_with_base(bhc).await?; let store = LocalizationStore::new_with_client(client).await?; From 4e8eefe01bd4dd5d963fcd7549d935c32126d092 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 31 Jul 2024 16:53:55 +0200 Subject: [PATCH 376/430] test LocalizationStore test_setting_l10n --- rust/agama-lib/src/localization/store.rs | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index c68074636c..6c53fe967d 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -74,6 +74,7 @@ mod test { use super::*; use crate::base_http_client::BaseHTTPClient; use httpmock::prelude::*; + use httpmock::Method::PATCH; use std::error::Error; use tokio::test; // without this, "error: async functions cannot be used for tests" @@ -114,4 +115,38 @@ mod test { assert_eq!(settings, expected); Ok(()) } + + #[test] + async fn test_setting_l10n() -> Result<(), Box> { + let server = MockServer::start(); + let l10n_mock = server.mock(|when, then| { + when.method(PATCH) + .path("/api/l10n/config") + .header("content-type", "application/json") + .body( + r#"{"locales":["fr_FR.UTF-8"],"keymap":"fr(dvorak)","timezone":"Europe/Paris","uiLocale":null,"uiKeymap":null}"# + ); + then.status(204); + }); + let url = server.url("/api"); + + let bhc = BaseHTTPClient { + base_url: url, + ..Default::default() + }; + let client = LocalizationHTTPClient::new_with_base(bhc).await?; + let store = LocalizationStore::new_with_client(client).await?; + + let settings = LocalizationSettings { + language: Some("fr_FR.UTF-8".to_owned()), + keyboard: Some("fr(dvorak)".to_owned()), + timezone: Some("Europe/Paris".to_owned()), + }; + let result = store.store(&settings).await; + + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + l10n_mock.assert(); + assert!(result.is_ok()); + Ok(()) + } } From 24e15cb8290ebd5bb8605088c91914b0b0863539 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 10:36:34 +0200 Subject: [PATCH 377/430] test: UsersStore::load --- rust/agama-lib/src/users/http_client.rs | 4 ++ rust/agama-lib/src/users/settings.rs | 6 +- rust/agama-lib/src/users/store.rs | 76 +++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index bf89d0f269..805c1410b4 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -13,6 +13,10 @@ impl UsersHTTPClient { }) } + pub fn new_with_base(client: BaseHTTPClient) -> Result { + Ok(Self { client }) + } + /// Returns the settings for first non admin user pub async fn first_user(&self) -> Result { self.client.get("/users/first").await diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index c808e17d70..f9c6b76a9a 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; /// User settings /// /// Holds the user settings for the installation. -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct UserSettings { #[serde(rename = "user")] @@ -14,7 +14,7 @@ pub struct UserSettings { /// First user settings /// /// Holds the settings for the first user. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FirstUserSettings { /// First user's full name @@ -30,7 +30,7 @@ pub struct FirstUserSettings { /// Root user settings /// /// Holds the settings for the root user. -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RootUserSettings { /// Root's password (in clear text) diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index c5f71fc206..6774ce4cbf 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -10,6 +10,10 @@ impl UsersStore { pub async fn new() -> Result { Ok(Self { users_client: UsersHTTPClient::new().await?, + + pub fn new_with_client(client: UsersHTTPClient) -> Result { + Ok(Self { + users_client: client, }) } @@ -70,3 +74,75 @@ impl UsersStore { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::base_http_client::BaseHTTPClient; + use httpmock::prelude::*; + use std::error::Error; + use tokio::test; // without this, "error: async functions cannot be used for tests" + + #[test] + async fn test_getting_users() -> Result<(), Box> { + let server = MockServer::start(); + let user_mock = server.mock(|when, then| { + when.method(GET).path("/api/users/first"); + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "fullName": "Tux", + "userName": "tux", + "password": "fish", + "autologin": true, + "data": {} + }"#, + ); + }); + let root_mock = server.mock(|when, then| { + when.method(GET).path("/api/users/root"); + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "sshkey": "keykeykey", + "password": true + }"#, + ); + }); + let url = server.url("/api"); + + let bhc = BaseHTTPClient { + base_url: url, + ..Default::default() + }; + let client = UsersHTTPClient::new_with_base(bhc)?; + let store = UsersStore::new_with_client(client)?; + + let settings = store.load().await?; + + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + user_mock.assert(); + root_mock.assert(); + + let first_user = FirstUserSettings { + full_name: Some("Tux".to_owned()), + user_name: Some("tux".to_owned()), + password: Some("fish".to_owned()), + autologin: Some(true), + }; + let root_user = RootUserSettings { + // FIXME this is weird: no matter what HTTP reports, we end up with None + password: None, + ssh_public_key: Some("keykeykey".to_owned()), + }; + let expected = UserSettings { + first_user: Some(first_user), + root: Some(root_user), + }; + + assert_eq!(settings, expected); + Ok(()) + } +} From 59f1cacd47bff5d59a79d67eec95e403194963cd Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 10:37:20 +0200 Subject: [PATCH 378/430] remove unneeded async/await --- rust/agama-lib/src/store.rs | 2 +- rust/agama-lib/src/users/http_client.rs | 2 +- rust/agama-lib/src/users/store.rs | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index a9c72b8641..a7285d5ba3 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -31,7 +31,7 @@ impl<'a> Store<'a> { ) -> Result, ServiceError> { Ok(Self { localization: LocalizationStore::new().await?, - users: UsersStore::new().await?, + users: UsersStore::new()?, network: NetworkStore::new(http_client).await?, product: ProductStore::new(connection.clone()).await?, software: SoftwareStore::new(connection.clone()).await?, diff --git a/rust/agama-lib/src/users/http_client.rs b/rust/agama-lib/src/users/http_client.rs index 805c1410b4..88357708c2 100644 --- a/rust/agama-lib/src/users/http_client.rs +++ b/rust/agama-lib/src/users/http_client.rs @@ -7,7 +7,7 @@ pub struct UsersHTTPClient { } impl UsersHTTPClient { - pub async fn new() -> Result { + pub fn new() -> Result { Ok(Self { client: BaseHTTPClient::new()?, }) diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 6774ce4cbf..82c4ca5e89 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -7,9 +7,11 @@ pub struct UsersStore { } impl UsersStore { - pub async fn new() -> Result { + pub fn new() -> Result { Ok(Self { - users_client: UsersHTTPClient::new().await?, + users_client: UsersHTTPClient::new()?, + }) + } pub fn new_with_client(client: UsersHTTPClient) -> Result { Ok(Self { From 7e27c36e82600f08d38f28080115233f744db31d Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 10:47:47 +0200 Subject: [PATCH 379/430] BaseHTTPClient.client private again, +new_unauthenticated_with_url and renaming the helper authenticator --- rust/agama-lib/src/base_http_client.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 9cff276d15..423df2807c 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -21,7 +21,7 @@ use crate::{auth::AuthToken, error::ServiceError}; /// } /// ``` pub struct BaseHTTPClient { - pub client: reqwest::Client, + client: reqwest::Client, pub base_url: String, } @@ -43,12 +43,19 @@ impl BaseHTTPClient { /// Uses `localhost`, authenticates with [`AuthToken`]. pub fn new() -> Result { Ok(Self { - client: Self::authenticated_reqwest_client()?, + client: Self::authenticated_client()?, ..Default::default() }) } - fn authenticated_reqwest_client() -> Result { + pub fn new_unauthenticated_with_url(url: String) -> Result { + Ok(Self { + client: reqwest::Client::new(), + base_url: url, + }) + } + + fn authenticated_client() -> Result { // TODO: this error is subtly misleading, leading me to believe the SERVER said it, // but in fact it is the CLIENT not finding an auth token let token = AuthToken::find().ok_or(ServiceError::NotAuthenticated)?; From 9dedab9ea7ef2e8792a0f99a065918a54e3eef65 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 12:06:46 +0200 Subject: [PATCH 380/430] adapt to changed BaseHTTPClient API: new_unauthenticated_with_url --- rust/agama-lib/src/localization/store.rs | 10 ++-------- rust/agama-lib/src/users/store.rs | 5 +---- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index 6c53fe967d..94e557490d 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -95,10 +95,7 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient { - base_url: url, - ..Default::default() - }; + let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; let client = LocalizationHTTPClient::new_with_base(bhc).await?; let store = LocalizationStore::new_with_client(client).await?; @@ -130,10 +127,7 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient { - base_url: url, - ..Default::default() - }; + let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; let client = LocalizationHTTPClient::new_with_base(bhc).await?; let store = LocalizationStore::new_with_client(client).await?; diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 82c4ca5e89..64f7512393 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -115,10 +115,7 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient { - base_url: url, - ..Default::default() - }; + let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; let client = UsersHTTPClient::new_with_base(bhc)?; let store = UsersStore::new_with_client(client)?; From 25497641a543b71f9f3cce6344584d53a8b4e289 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 10:45:48 +0200 Subject: [PATCH 381/430] test UsersStore::store --- rust/agama-lib/src/users/store.rs | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 64f7512393..43b4d7975a 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -82,6 +82,7 @@ mod test { use super::*; use crate::base_http_client::BaseHTTPClient; use httpmock::prelude::*; + use httpmock::Method::PATCH; use std::error::Error; use tokio::test; // without this, "error: async functions cannot be used for tests" @@ -144,4 +145,61 @@ mod test { assert_eq!(settings, expected); Ok(()) } + + #[test] + async fn test_setting_users() -> Result<(), Box> { + let server = MockServer::start(); + let user_mock = server.mock(|when, then| { + when.method(PUT) + .path("/api/users/first") + .header("content-type", "application/json") + .body( + r#"{"fullName":"Tux","userName":"tux","password":"fish","autologin":true,"data":{}}"# + ); + then.status(200); + }); + // note that we use 2 requests for root + let root_mock = server.mock(|when, then| { + when.method(PATCH) + .path("/api/users/root") + .header("content-type", "application/json") + .body(r#"{"sshkey":null,"password":"1234","passwordEncrypted":false}"#); + then.status(200); + }); + let root_mock2 = server.mock(|when, then| { + when.method(PATCH) + .path("/api/users/root") + .header("content-type", "application/json") + .body(r#"{"sshkey":"keykeykey","password":null,"passwordEncrypted":null}"#); + then.status(200); + }); + let url = server.url("/api"); + + let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; + let client = UsersHTTPClient::new_with_base(bhc)?; + let store = UsersStore::new_with_client(client)?; + + let first_user = FirstUserSettings { + full_name: Some("Tux".to_owned()), + user_name: Some("tux".to_owned()), + password: Some("fish".to_owned()), + autologin: Some(true), + }; + let root_user = RootUserSettings { + password: Some("1234".to_owned()), + ssh_public_key: Some("keykeykey".to_owned()), + }; + let settings = UserSettings { + first_user: Some(first_user), + root: Some(root_user), + }; + let result = store.store(&settings).await; + + // Ensure the specified mock was called exactly one time (or fail with a detailed error description). + user_mock.assert(); + root_mock.assert(); + root_mock2.assert(); + assert!(result.is_ok()); + Ok(()) + } } From 1d4ca4e69707dfbe2809a90222a48419d7bf17a5 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 16:19:53 +0200 Subject: [PATCH 382/430] BaseHTTPClient: just use the public base_url field --- rust/agama-lib/src/base_http_client.rs | 7 ------- rust/agama-lib/src/localization/store.rs | 6 ++++-- rust/agama-lib/src/users/store.rs | 6 ++++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/rust/agama-lib/src/base_http_client.rs b/rust/agama-lib/src/base_http_client.rs index 423df2807c..a0121741c7 100644 --- a/rust/agama-lib/src/base_http_client.rs +++ b/rust/agama-lib/src/base_http_client.rs @@ -48,13 +48,6 @@ impl BaseHTTPClient { }) } - pub fn new_unauthenticated_with_url(url: String) -> Result { - Ok(Self { - client: reqwest::Client::new(), - base_url: url, - }) - } - fn authenticated_client() -> Result { // TODO: this error is subtly misleading, leading me to believe the SERVER said it, // but in fact it is the CLIENT not finding an auth token diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index 94e557490d..010ff5c21a 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -95,7 +95,8 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = url; let client = LocalizationHTTPClient::new_with_base(bhc).await?; let store = LocalizationStore::new_with_client(client).await?; @@ -127,7 +128,8 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = url; let client = LocalizationHTTPClient::new_with_base(bhc).await?; let store = LocalizationStore::new_with_client(client).await?; diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 43b4d7975a..8412343a31 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -116,7 +116,8 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = url; let client = UsersHTTPClient::new_with_base(bhc)?; let store = UsersStore::new_with_client(client)?; @@ -175,7 +176,8 @@ mod test { }); let url = server.url("/api"); - let bhc = BaseHTTPClient::new_unauthenticated_with_url(url)?; + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = url; let client = UsersHTTPClient::new_with_base(bhc)?; let store = UsersStore::new_with_client(client)?; From 8dad93ce5087abf95314d0a138ba3753abd63c08 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 1 Aug 2024 17:20:49 +0200 Subject: [PATCH 383/430] test: factor out setup code --- rust/agama-lib/src/localization/store.rs | 20 +++++++++++--------- rust/agama-lib/src/users/store.rs | 18 +++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index 010ff5c21a..f3508bd932 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -78,6 +78,15 @@ mod test { use std::error::Error; use tokio::test; // without this, "error: async functions cannot be used for tests" + async fn localization_store( + mock_server_url: String, + ) -> Result { + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = mock_server_url; + let client = LocalizationHTTPClient::new_with_base(bhc).await?; + LocalizationStore::new_with_client(client).await + } + #[test] async fn test_getting_l10n() -> Result<(), Box> { let server = MockServer::start(); @@ -95,11 +104,7 @@ mod test { }); let url = server.url("/api"); - let mut bhc = BaseHTTPClient::default(); - bhc.base_url = url; - let client = LocalizationHTTPClient::new_with_base(bhc).await?; - let store = LocalizationStore::new_with_client(client).await?; - + let store = localization_store(url).await?; let settings = store.load().await?; // Ensure the specified mock was called exactly one time (or fail with a detailed error description). @@ -128,10 +133,7 @@ mod test { }); let url = server.url("/api"); - let mut bhc = BaseHTTPClient::default(); - bhc.base_url = url; - let client = LocalizationHTTPClient::new_with_base(bhc).await?; - let store = LocalizationStore::new_with_client(client).await?; + let store = localization_store(url).await?; let settings = LocalizationSettings { language: Some("fr_FR.UTF-8".to_owned()), diff --git a/rust/agama-lib/src/users/store.rs b/rust/agama-lib/src/users/store.rs index 8412343a31..df2943a865 100644 --- a/rust/agama-lib/src/users/store.rs +++ b/rust/agama-lib/src/users/store.rs @@ -86,6 +86,13 @@ mod test { use std::error::Error; use tokio::test; // without this, "error: async functions cannot be used for tests" + fn users_store(mock_server_url: String) -> Result { + let mut bhc = BaseHTTPClient::default(); + bhc.base_url = mock_server_url; + let client = UsersHTTPClient::new_with_base(bhc)?; + UsersStore::new_with_client(client) + } + #[test] async fn test_getting_users() -> Result<(), Box> { let server = MockServer::start(); @@ -116,11 +123,7 @@ mod test { }); let url = server.url("/api"); - let mut bhc = BaseHTTPClient::default(); - bhc.base_url = url; - let client = UsersHTTPClient::new_with_base(bhc)?; - let store = UsersStore::new_with_client(client)?; - + let store = users_store(url)?; let settings = store.load().await?; // Ensure the specified mock was called exactly one time (or fail with a detailed error description). @@ -176,10 +179,7 @@ mod test { }); let url = server.url("/api"); - let mut bhc = BaseHTTPClient::default(); - bhc.base_url = url; - let client = UsersHTTPClient::new_with_base(bhc)?; - let store = UsersStore::new_with_client(client)?; + let store = users_store(url)?; let first_user = FirstUserSettings { full_name: Some("Tux".to_owned()), From 52006ac60eed379286cd9bc3d1ef4143608f95bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 1 Aug 2024 17:29:06 +0200 Subject: [PATCH 384/430] Display QR codes for accessing Agama at the console --- live/root/usr/bin/agama-issue-generator | 94 +++++++++++++++++++++++-- live/src/agama-installer.kiwi | 1 + 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/live/root/usr/bin/agama-issue-generator b/live/root/usr/bin/agama-issue-generator index 2377f26142..f21adbc4ce 100755 --- a/live/root/usr/bin/agama-issue-generator +++ b/live/root/usr/bin/agama-issue-generator @@ -139,6 +139,91 @@ generate_avahi_url() { done } +# function for centering text +# $1 - the text +# $2 - requested width +function center_text() { + LEN=${#1} + PADDING_LEN=$(($2 - LEN)) + PADDING_LEN=$((PADDING_LEN / 2)) + PADDING="$(printf '%*s' $PADDING_LEN)" + echo "$PADDING$1$PADDING" +} + +# generate QR codes for the access URLs +function create_qr_codes() { + ADDRESSES=("$@") + # width of the generated QR code + QR_WIDTH=30 + + # check if serial console is used, get the terminal size (width) + TERM_WIDTH=$(stty -F /dev/ttyS0 size 2> /dev/null | cut -d " " -f 2) + + # otherwise check the first console size + if [ -z "$TERM_WIDTH" ]; then + TERM_WIDTH=$(stty -F /dev/tty1 size 2> /dev/null | cut -d " " -f 2) + echo "Linux console width $TERM_WIDTH" + else + echo "Serial console width $TERM_WIDTH" + fi + + # display QR codes only if the terminal is bigger than the 80x24(25) default + if [ -n "$TERM_WIDTH" ] && [ "$TERM_WIDTH" -gt 80 ]; then + # compute how much QR codes can fit on the screen side-by-side + QR_NUM=$(( TERM_WIDTH / QR_WIDTH )) + # split the list into 2 parts, for the first part display the QR codes + QR_ADDRESSES=("${ADDRESSES[@]:0:$QR_NUM}") + SZ="${#ADDRESSES[@]}" + # for the rest display just the text URL + REST_ADDRESSES=("${ADDRESSES[@]:$QR_NUM:$SZ}") + else + QR_ADDRESSES=() + REST_ADDRESSES=("${ADDRESSES[@]}") + fi + + if [ -n "${REST_ADDRESSES[*]}" ]; then + printf " https://%s\n" "${REST_ADDRESSES[@]}" >> "$URL_ISSUES" + fi + + if [ -z "${QR_ADDRESSES[*]}" ]; then + return 0 + fi + + # temporary file for generated QR code + QR_TEMP=$(mktemp) + # temporary file for merged QR codes (displayed side-by-side) + QR_RESULT=$(mktemp) + # copy of the merged QR codes (the merged file cannot be used as input and + # output at the same time) + QR_RESULT_COPY=$(mktemp) + + # label with URLs displayed below the QR codes + LABEL="" + + # generate the QR codes and merge them side-by-side + for ADDR in "${QR_ADDRESSES[@]}"; do + cp "$QR_RESULT" "$QR_RESULT_COPY" + URL="https://$ADDR" + echo "Rendering QR code for $URL" + # force (the -v option) using at least symbol version 2 (QR size 25x25), + # for short addresses (like https://1.1.1.1) it would be enough using + # version 1 (QR size 21x21), but longer addresses need version 2 and + # putting different sizes side-by-side breaks formatting, see `man + # qrencode` and https://www.qrcode.com/en/about/version.html + qrencode -t ANSIUTF8 -m 2 -v 2 -o "$QR_TEMP" "$URL" + # put the QR codes side-by-side + paste -d " " "$QR_RESULT_COPY" "$QR_TEMP" > "$QR_RESULT" + PADDED_URL=$(center_text "$URL" "$QR_WIDTH") + LABEL="$LABEL$PADDED_URL " + done + + cat "$QR_RESULT" >> "$URL_ISSUES" + echo "$LABEL" >> "$URL_ISSUES" + + # delete the temporary files + rm -f "$QR_TEMP" "$QR_RESULT_COPY" "$QR_RESULT" +} + # helper function, write the issue with the currently available URLs for # accessing Agama from outside build_addresses() { @@ -158,11 +243,11 @@ build_addresses() { # remove duplicates readarray -t ADDRESSES < <(printf "%s\n" "${ADDRESSES[@]}" | sort -u) + # delete the old file + rm -f "$URL_ISSUES" + if [ -n "${ADDRESSES[*]}" ]; then - printf " https://%s\n" "${ADDRESSES[@]}" > "$URL_ISSUES" - else - # no messages, delete the URLs - rm -f "$URL_ISSUES" + create_qr_codes "${ADDRESSES[@]}" fi write_url_headers @@ -185,6 +270,7 @@ generate_network_url() { type=signal" 2> /dev/null | while read -r line; do # some IP4 configuration has been changed, rebuild the URLs if echo "$line" | grep -q 'string "org.freedesktop.NetworkManager.IP4Config"'; then + echo "Network configuration changed" build_addresses fi done diff --git a/live/src/agama-installer.kiwi b/live/src/agama-installer.kiwi index 48b1029e31..abd0599cad 100644 --- a/live/src/agama-installer.kiwi +++ b/live/src/agama-installer.kiwi @@ -155,6 +155,7 @@ + From 4faa1b878ba2b01208b186e961b49959a98c8232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 2 Aug 2024 10:04:06 +0200 Subject: [PATCH 385/430] Changelog for PR #1501 --- live/src/agama-installer.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/live/src/agama-installer.changes b/live/src/agama-installer.changes index 8c59a1cfb1..2a3792412f 100644 --- a/live/src/agama-installer.changes +++ b/live/src/agama-installer.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Aug 2 08:02:41 UTC 2024 - Ladislav Slezák + +- Display QR codes at the console for easier connecting to Agama + with smartphones (gh#openSUSE/agama#1522) + ------------------------------------------------------------------- Thu Jul 25 13:18:38 UTC 2024 - Ladislav Slezák From 740f22bdd6cb3455ad9188057bb9e1c766e5688a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Aug 2024 09:51:31 +0100 Subject: [PATCH 386/430] feat(products): update SLES 16 repositories --- products.d/sles_160.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index 9ca088b351..d92837cd26 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -33,10 +33,14 @@ translations: affärskritiska arbetsbelastningar på plats, i molnet och vid kanten. software: installation_repositories: - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0/product/repo/SLES-Packages-16.0-x86_64/ + - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-x86_64/ archs: x86_64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0/product/repo/SLES-Packages-16.0-aarch64/ + - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-aarch64/ archs: aarch64 + - url: https://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-ppc64le/ + archs: ppc + - url: https://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-s390x/ + archs: s390 mandatory_patterns: - sles_enhanced_base From 3182b9ba5c5ae6c02ee31d71bbc5e55c33e5d886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Aug 2024 09:54:47 +0100 Subject: [PATCH 387/430] doc(products): update changes file --- products.d/agama-products.changes | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes index 638a8a7e7d..3b292b2ff5 100644 --- a/products.d/agama-products.changes +++ b/products.d/agama-products.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Fri Aug 2 08:51:59 UTC 2024 - Imobach Gonzalez Sosa + +- Adjust the SLES 16 repositories (gh#openSUSE/agama#1524): + - The TEST repositories are more stable at this time. + - Add s390x and ppc64le repositories. + ------------------------------------------------------------------- Wed Jul 17 07:34:01 UTC 2024 - Imobach Gonzalez Sosa From 2add5b811415586a01180acda9dab97ba3a3f0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Aug 2024 10:15:12 +0100 Subject: [PATCH 388/430] feat(products): enable SELinux --- products.d/sles_160.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index d92837cd26..5c03b17e23 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -52,11 +52,8 @@ software: base_product: SLES security: - lsm: none + lsm: selinux available_lsms: - apparmor: - patterns: - - apparmor selinux: patterns: - selinux From be8a5d2e90a2981614bc2282c042b20a7083876f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 2 Aug 2024 10:15:23 +0100 Subject: [PATCH 389/430] doc(products): update changes file --- products.d/agama-products.changes | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes index 3b292b2ff5..d46f75fd28 100644 --- a/products.d/agama-products.changes +++ b/products.d/agama-products.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Fri Aug 2 09:12:25 UTC 2024 - Imobach Gonzalez Sosa + +- Adjust Linux Security Modules (LSM) configuration for SLES 16 +(gh#openSUSE/agama#1525): + - Enable SELinux by default. + - Drop AppArmor as it is not available. + ------------------------------------------------------------------- Fri Aug 2 08:51:59 UTC 2024 - Imobach Gonzalez Sosa From 459f06ca4c5727711bde7c64cfa7775a5e161e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:06:46 +0100 Subject: [PATCH 390/430] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Imobach González Sosa --- .../components/questions/LuksActivationQuestion.test.tsx | 4 ++-- web/src/components/questions/LuksActivationQuestion.tsx | 9 ++------- web/src/components/questions/QuestionActions.tsx | 6 +++--- web/src/components/questions/Questions.test.tsx | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/web/src/components/questions/LuksActivationQuestion.test.tsx b/web/src/components/questions/LuksActivationQuestion.test.tsx index c2da7b99cd..3af25fc170 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.tsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -54,7 +54,7 @@ describe("LuksActivationQuestion", () => { it("does not contain a warning", async () => { renderQuestion(); - const warning = screen.queryByText(/Given encryption password/); + const warning = screen.queryByText("The encryption password did not work"); expect(warning).toBeNull(); }); }); @@ -67,7 +67,7 @@ describe("LuksActivationQuestion", () => { it("contains a warning", async () => { renderQuestion(); - await screen.findByText(/Given encryption password/); + await screen.findByText("The encryption password did not work"); }); }); diff --git a/web/src/components/questions/LuksActivationQuestion.tsx b/web/src/components/questions/LuksActivationQuestion.tsx index 8862defbc6..5998f27472 100644 --- a/web/src/components/questions/LuksActivationQuestion.tsx +++ b/web/src/components/questions/LuksActivationQuestion.tsx @@ -29,17 +29,12 @@ import { _ } from "~/i18n"; /** * Internal component for rendering an alert if given password failed */ -const Alert = ({ attempt }: { attempt: string | undefined }): React.ReactNode => { +const Alert = ({ attempt }: { attempt?: string }): React.ReactNode => { if (!attempt || parseInt(attempt) === 1) return null; return ( // TRANSLATORS: error message, user entered a wrong password - + ); }; diff --git a/web/src/components/questions/QuestionActions.tsx b/web/src/components/questions/QuestionActions.tsx index c90539fd20..c96ddeb294 100644 --- a/web/src/components/questions/QuestionActions.tsx +++ b/web/src/components/questions/QuestionActions.tsx @@ -39,9 +39,9 @@ const label = (text: string): string => `${text[0].toUpperCase()}${text.slice(1) * React.Fragment (aka <>) here for wrapping the actions instead of directly using the Popup.Actions. * * @param {object} props - component props - * @param props.actions - the actions to be shown - * @param props.defaultAction - the action to be shown as primary - * @param props.actionCallback - the function to be called when user clicks on action + * @param props.actions - the actions show + * @param props.defaultAction - the action to show as primary + * @param props.actionCallback - the function to call when the user clicks on the action * @param props.conditions={} - an object holding conditions, like when an action is disabled */ export default function QuestionActions({ diff --git a/web/src/components/questions/Questions.test.tsx b/web/src/components/questions/Questions.test.tsx index f85555e760..31ce42bb9b 100644 --- a/web/src/components/questions/Questions.test.tsx +++ b/web/src/components/questions/Questions.test.tsx @@ -74,7 +74,7 @@ describe("Questions", () => { mockQuestions = [genericQuestion]; }); - it("triggers the useQuestionMutationk", async () => { + it("triggers the useQuestionMutation", async () => { const { user } = plainRender(); const button = screen.getByRole("button", { name: "Always" }); await user.click(button); From a4ffb98d0b50d53d3f1192f3336cd36528d1e9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 2 Aug 2024 13:13:51 +0100 Subject: [PATCH 391/430] fix(web): use the right http verb for mutate a question PUT instead of PATCH. --- web/src/queries/questions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/queries/questions.ts b/web/src/queries/questions.ts index 118fd0e029..62d505b15d 100644 --- a/web/src/queries/questions.ts +++ b/web/src/queries/questions.ts @@ -79,7 +79,7 @@ const useQuestionsConfig = () => { } return fetch(`/api/questions/${question.id}/answer`, { - method: "PATCH", + method: "PUT", body: JSON.stringify(answer), headers: { "Content-Type": "application/json", From 59d1a5714ab3102fc37261b32483496a6e262662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 2 Aug 2024 13:22:09 +0100 Subject: [PATCH 392/430] fix(web): improve buildQuestion method --- web/src/queries/questions.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/web/src/queries/questions.ts b/web/src/queries/questions.ts index 62d505b15d..f5b56f41b9 100644 --- a/web/src/queries/questions.ts +++ b/web/src/queries/questions.ts @@ -35,22 +35,16 @@ type APIQuestion = { * TODO: improve/simplify it once the backend API is improved. */ function buildQuestion(httpQuestion: APIQuestion) { - let question: Question; + const question: Question = { ...httpQuestion.generic }; if (httpQuestion.generic) { - question = { - ...httpQuestion.generic, - type: QuestionType.generic, - answer: httpQuestion.generic.answer, - }; + question.type = QuestionType.generic; + question.answer = httpQuestion.generic.answer; } if (httpQuestion.withPassword) { - question = { - id: httpQuestion.generic.id, - type: QuestionType.withPassword, - password: httpQuestion.withPassword.password, - }; + question.type = QuestionType.withPassword; + question.password = httpQuestion.withPassword.password; } return question; From 0f03be9d1691e19f5e3004647b6e9b8d7416f08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 2 Aug 2024 13:45:52 +0100 Subject: [PATCH 393/430] fix(web): improve the questions presentation By using PF/Stack#hasGutter for adding vertical space between internal components. --- .../questions/LuksActivationQuestion.tsx | 30 ++++++++++--------- .../questions/QuestionWithPassword.tsx | 28 +++++++++-------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/web/src/components/questions/LuksActivationQuestion.tsx b/web/src/components/questions/LuksActivationQuestion.tsx index 5998f27472..9b666ddd18 100644 --- a/web/src/components/questions/LuksActivationQuestion.tsx +++ b/web/src/components/questions/LuksActivationQuestion.tsx @@ -20,7 +20,7 @@ */ import React, { useState } from "react"; -import { Alert as PFAlert, Form, FormGroup, Text } from "@patternfly/react-core"; +import { Alert as PFAlert, Form, FormGroup, Text, Stack } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { PasswordInput, Popup } from "~/components/core"; import QuestionActions from "~/components/questions/QuestionActions"; @@ -69,19 +69,21 @@ export default function LuksActivationQuestion({ question, answerCallback }) { aria-label={_("Question")} titleIconVariant={() => } > - - {question.text} - - {/* TRANSLATORS: field label */} - - setPassword(value)} - /> - - + + + {question.text} +
      + {/* TRANSLATORS: field label */} + + setPassword(value)} + /> + +
      +
      } > - {question.text} -
      - {/* TRANSLATORS: field label */} - - setPassword(value)} - /> - -
      + + {question.text} +
      + {/* TRANSLATORS: field label */} + + setPassword(value)} + /> + +
      +
      Date: Fri, 2 Aug 2024 15:00:58 +0100 Subject: [PATCH 394/430] fix(web): make useProductChanges work as expected (#1526) By mistake, https://github.com/openSUSE/agama/pull/1483 introduced a tiny bug in the `useProductChanges` query hook by checking the `event.type` against an empty string instead of the exepected `ProductChanged` event. https://github.com/openSUSE/agama/pull/1483/files#diff-e671c06f4a1cefe3bef4af838681c780f2ba41356d44f72f5ce97be1b6eead66R172-R185 This PR fixes it for properly performs the software config query invalidation. --- web/src/queries/software.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 9834bf8ec9..fc41aafbea 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -188,7 +188,7 @@ const useProductChanges = () => { if (!client) return; return client.ws().onEvent((event) => { - if (event.type === "") { + if (event.type === "ProductChanged") { queryClient.invalidateQueries({ queryKey: ["software/config"] }); } }); From 3dca6b5ed6a30389b0023aea9a1ea031934a5859 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 2 Aug 2024 21:19:35 +0200 Subject: [PATCH 395/430] remove unneeded async/await --- rust/agama-lib/src/localization/http_client.rs | 4 ++-- rust/agama-lib/src/localization/store.rs | 10 +++++----- rust/agama-lib/src/store.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rust/agama-lib/src/localization/http_client.rs b/rust/agama-lib/src/localization/http_client.rs index 1312ec0eec..9a7c389d6f 100644 --- a/rust/agama-lib/src/localization/http_client.rs +++ b/rust/agama-lib/src/localization/http_client.rs @@ -6,13 +6,13 @@ pub struct LocalizationHTTPClient { } impl LocalizationHTTPClient { - pub async fn new() -> Result { + pub fn new() -> Result { Ok(Self { client: BaseHTTPClient::new()?, }) } - pub async fn new_with_base(base: BaseHTTPClient) -> Result { + pub fn new_with_base(base: BaseHTTPClient) -> Result { Ok(Self { client: base }) } diff --git a/rust/agama-lib/src/localization/store.rs b/rust/agama-lib/src/localization/store.rs index f3508bd932..fbe32372d2 100644 --- a/rust/agama-lib/src/localization/store.rs +++ b/rust/agama-lib/src/localization/store.rs @@ -11,13 +11,13 @@ pub struct LocalizationStore { } impl LocalizationStore { - pub async fn new() -> Result { + pub fn new() -> Result { Ok(Self { - localization_client: LocalizationHTTPClient::new().await?, + localization_client: LocalizationHTTPClient::new()?, }) } - pub async fn new_with_client( + pub fn new_with_client( client: LocalizationHTTPClient, ) -> Result { Ok(Self { @@ -83,8 +83,8 @@ mod test { ) -> Result { let mut bhc = BaseHTTPClient::default(); bhc.base_url = mock_server_url; - let client = LocalizationHTTPClient::new_with_base(bhc).await?; - LocalizationStore::new_with_client(client).await + let client = LocalizationHTTPClient::new_with_base(bhc)?; + LocalizationStore::new_with_client(client) } #[test] diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index a7285d5ba3..addf98f8ae 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -30,7 +30,7 @@ impl<'a> Store<'a> { http_client: reqwest::Client, ) -> Result, ServiceError> { Ok(Self { - localization: LocalizationStore::new().await?, + localization: LocalizationStore::new()?, users: UsersStore::new()?, network: NetworkStore::new(http_client).await?, product: ProductStore::new(connection.clone()).await?, From cc6e0c8374557edce208359e2f26acbeff5c8c3e Mon Sep 17 00:00:00 2001 From: YaST Bot Date: Sun, 4 Aug 2024 02:52:52 +0000 Subject: [PATCH 396/430] Update web PO files Agama-weblate commit: 0d0eb74a1366674f09c89e5dbae0318466b057e5 --- web/po/ca.po | 368 +++++++++++++++++++++------------------------ web/po/cs.po | 365 ++++++++++++++++++++------------------------ web/po/de.po | 375 +++++++++++++++++++++------------------------- web/po/es.po | 371 +++++++++++++++++++++------------------------ web/po/fr.po | 375 +++++++++++++++++++++------------------------- web/po/id.po | 369 +++++++++++++++++++++------------------------ web/po/ja.po | 362 ++++++++++++++++++++------------------------ web/po/ka.po | 328 +++++++++++++++++----------------------- web/po/mk.po | 329 ++++++++++++++++------------------------ web/po/nb_NO.po | 365 +++++++++++++++++++++----------------------- web/po/nl.po | 371 +++++++++++++++++++++------------------------ web/po/pt_BR.po | 363 ++++++++++++++++++++------------------------ web/po/ru.po | 370 +++++++++++++++++++++------------------------ web/po/sv.po | 363 ++++++++++++++++++++------------------------ web/po/tr.po | 352 +++++++++++++++++++------------------------ web/po/uk.po | 289 +++++++++++++---------------------- web/po/zh_Hans.po | 357 ++++++++++++++++++++----------------------- 17 files changed, 2698 insertions(+), 3374 deletions(-) diff --git a/web/po/ca.po b/web/po/ca.po index 7132f194c6..df36b2d8f9 100644 --- a/web/po/ca.po +++ b/web/po/ca.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-28 02:29+0000\n" +"POT-Creation-Date: 2024-08-04 02:29+0000\n" "PO-Revision-Date: 2024-07-25 08:46+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan