diff --git a/.editorconfig b/.editorconfig index d12b0a8762..6367fde790 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,6 +4,6 @@ root = true # 2 space indentation -[{*.sh}] +[*.sh] indent_style = space indent_size = 2 diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 722c67c7e6..d96017f744 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -28,6 +28,17 @@ jobs: fetch-depth: 0 fetch-tags: true + - name: Checkout integration tests + uses: actions/checkout@v4 + with: + path: integration-tests + repository: ${{ github.repository_owner }}/integration-tests + fetch-depth: 1 + + - name: Build integration test + run: npm ci && ESLINT=0 npm run build + working-directory: ./integration-tests + - name: Created shared YaST log directory run: mkdir -p /tmp/log/YaST2 /tmp/log/puppeteer @@ -71,16 +82,10 @@ jobs: - name: Run the Agama smoke test run: podman exec agama curl http://localhost - - 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 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" + node --enable-source-maps --test-reporter=spec /checkout/integration-tests/dist/test_root_password.js" - name: Again show the D-Bus services log # run even when any previous step fails diff --git a/.github/workflows/obs-release.yml b/.github/workflows/obs-release.yml index 2f24512a7d..9928560fa6 100644 --- a/.github/workflows/obs-release.yml +++ b/.github/workflows/obs-release.yml @@ -1,5 +1,8 @@ # Publish a new version -# - Submit the packages to systemsmanagement:Agama:Devel +# - Submit the packages to the OBS project defined in OBS_PROJECT_RELEASE variable +# at GitHub (in the original repository it is set to systemsmanagement:Agama:Release, +# see https://github.com/agama-project/agama/settings/variables/actions, +# you might change that in forks) # - Send submit requests name: Release @@ -11,7 +14,7 @@ on: - v[0-9]* jobs: - # Note: agama-integration-tests and the Live ISO are currently not submitted + # Note: the Live ISO is currently not submitted update_rust: uses: ./.github/workflows/obs-staging-shared.yml @@ -19,8 +22,8 @@ jobs: secrets: inherit with: install_packages: obs-service-cargo_audit obs-service-cargo_vendor - project_name: systemsmanagement:Agama:Devel package_name: agama + service_file: rust/package/_service update_web: uses: ./.github/workflows/obs-staging-shared.yml @@ -28,12 +31,18 @@ jobs: secrets: inherit with: install_packages: obs-service-node_modules - project_name: systemsmanagement:Agama:Devel - package_name: cockpit-agama + package_name: agama-web-ui + service_file: web/package/_service update_service: uses: ./.github/workflows/obs-service-shared.yml # pass all secrets secrets: inherit + + update_products: + uses: ./.github/workflows/obs-staging-shared.yml + # pass all secrets + secrets: inherit with: - project_name: systemsmanagement:Agama:Devel + package_name: agama-products + service_file: products.d/_service diff --git a/.github/workflows/obs-staging-integration-tests.yml b/.github/workflows/obs-staging-integration-tests.yml deleted file mode 100644 index 63c26e2461..0000000000 --- a/.github/workflows/obs-staging-integration-tests.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Submit agama-integration-tests - -on: - # runs on pushes targeting the default branch - push: - branches: - - master - - release - 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: - install_packages: obs-service-node_modules - package_name: agama-integration-tests - service_file: puppeteer/package/_service diff --git a/doc/answers_example.json b/doc/answers_example.json new file mode 100644 index 0000000000..266dbf2128 --- /dev/null +++ b/doc/answers_example.json @@ -0,0 +1,9 @@ +{ + "answers": [ + { + "answer": "decrypt", + "class": "storage.luks_activation", + "password": "my_password" + } + ] +} diff --git a/doc/answers_example.yaml b/doc/answers_example.yaml deleted file mode 100644 index 581f39ff1d..0000000000 --- a/doc/answers_example.yaml +++ /dev/null @@ -1,3 +0,0 @@ -answers: - - class: storage.luks_activation - answer: "skip" diff --git a/live/root/usr/lib/dracut/modules.d/99agama-cmdline/README b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/README new file mode 100644 index 0000000000..2787db46c2 --- /dev/null +++ b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/README @@ -0,0 +1,6 @@ +dracut agama-cmdline module +------------------------------- + +This module writes any agama configuration given through the kernel cmdline +to its own cmdline conf file copying it to the sysroot. + diff --git a/live/root/usr/lib/dracut/modules.d/99agama-cmdline/agama-cmdline-conf.sh b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/agama-cmdline-conf.sh new file mode 100755 index 0000000000..e1ec13f79b --- /dev/null +++ b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/agama-cmdline-conf.sh @@ -0,0 +1,31 @@ +#! /bin/sh + +[ -e /dracut-state.sh ] && . /dracut-state.sh + +. /lib/dracut-lib.sh + +get_agama_args() { + local _i _found + + for _i in $CMDLINE; do + case $_i in + LIBSTORAGE_* | YAST_* | agama* | Y2* | ZYPP_*) + _found=1 + ;; + esac + + if [ -n "$_found" ]; then + printf "Agama variable found ($_i)" + if ! strstr "$_i" "="; then + # Set the variable as a boolean if there is no assignation + _i="${_i}=1" + fi + echo $_i >>/etc/cmdline.d/99-agama-cmdline.conf + fi + unset _found + done + + return 0 +} + +get_agama_args diff --git a/live/root/usr/lib/dracut/modules.d/99agama-cmdline/module-setup.sh b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/module-setup.sh new file mode 100755 index 0000000000..24a6bcd321 --- /dev/null +++ b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/module-setup.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# called by dracut +check() { + return 0 +} + +# called by dracut +depends() { + return 0 +} + +installkernel() { + return 0 +} + +# called by dracut +install() { + inst_hook cmdline 99 "$moddir/agama-cmdline-conf.sh" + inst_hook pre-pivot 99 "$moddir/save-agama-conf.sh" +} diff --git a/live/root/usr/lib/dracut/modules.d/99agama-cmdline/save-agama-conf.sh b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/save-agama-conf.sh new file mode 100755 index 0000000000..612843ddd7 --- /dev/null +++ b/live/root/usr/lib/dracut/modules.d/99agama-cmdline/save-agama-conf.sh @@ -0,0 +1,11 @@ +#! /bin/sh + +[ -e /dracut-state.sh ] && . /dracut-state.sh + +. /lib/dracut-lib.sh + +if [ -e /etc/cmdline.d/99-agama-cmdline.conf ]; then + echo "Creating agama conf" + mkdir -p "$NEWROOT/etc/agama.d" + cp /etc/cmdline.d/99-agama-cmdline.conf "$NEWROOT/etc/agama.d/cmdline.conf" +fi diff --git a/live/src/agama-installer.changes b/live/src/agama-installer.changes index 8d573634a3..9eb42405ef 100644 --- a/live/src/agama-installer.changes +++ b/live/src/agama-installer.changes @@ -1,3 +1,20 @@ +------------------------------------------------------------------- +Fri Jan 10 21:22:03 UTC 2025 - Imobach Gonzalez Sosa + +- Version 11 + +------------------------------------------------------------------- +Fri Jan 10 09:03:06 UTC 2025 - Imobach Gonzalez Sosa + +- Depend on Ruby's default version (gh#agama-project/agama#1872). + +------------------------------------------------------------------- +Wed Jan 8 12:10:39 UTC 2025 - Knut Anderssen + +- Make agama kernel cmdline options available in the sysroot at + /etc/agama.d/cmdline.conf and set it as a EnvironmentFile + in Agama related services (gh#agama-project/agama#1866). + ------------------------------------------------------------------- Tue Dec 10 12:46:06 UTC 2024 - Michal Filka diff --git a/live/src/agama-installer.kiwi b/live/src/agama-installer.kiwi index 8a1dab1e4c..c42a5995b7 100644 --- a/live/src/agama-installer.kiwi +++ b/live/src/agama-installer.kiwi @@ -16,7 +16,7 @@ - 10.0.0 + 11.0.0 zypper en_US us @@ -144,6 +144,8 @@ + + @@ -166,8 +168,6 @@ - - @@ -178,8 +178,6 @@ - - @@ -189,8 +187,6 @@ - - diff --git a/live/src/config.sh b/live/src/config.sh index d326a0c76b..607fe25094 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -82,11 +82,11 @@ echo "root_disk=live:LABEL=$label" >>/etc/cmdline.d/10-liveroot.conf # if there's a default network location, add it here # echo "root_net=" >> /etc/cmdline.d/10-liveroot.conf echo 'install_items+=" /etc/cmdline.d/10-liveroot.conf "' >/etc/dracut.conf.d/10-liveroot-file.conf -echo 'add_dracutmodules+=" dracut-menu "' >>/etc/dracut.conf.d/10-liveroot-file.conf +echo 'add_dracutmodules+=" dracut-menu agama-cmdline "' >>/etc/dracut.conf.d/10-liveroot-file.conf -if [ "${arch}" = "s390x" ];then - # workaround for custom bootloader setting - touch /config.bootoptions +if [ "${arch}" = "s390x" ]; then + # workaround for custom bootloader setting + touch /config.bootoptions fi # replace the @@LIVE_MEDIUM_LABEL@@ with the real Live partition label name from KIWI @@ -111,7 +111,7 @@ 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 +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/ @@ -125,7 +125,7 @@ du -h -s /usr/{share,lib}/locale/ mkdir -p /etc/agama.d # emulate "localectl list-locales" call, it cannot be used here because it # insists on running systemd as PID 1 :-/ -ls -1 -d /usr/lib/locale/*.utf8 | sed -e "s#/usr/lib/locale/##" -e "s#utf8#UTF-8#" > /etc/agama.d/locales +ls -1 -d /usr/lib/locale/*.utf8 | sed -e "s#/usr/lib/locale/##" -e "s#utf8#UTF-8#" >/etc/agama.d/locales # delete translations and unusupported languages (makes ISO about 22MiB smaller) # build list of ignore options for "ls" with supported languages like "-I cs* -I de* -I es* ..." @@ -138,7 +138,7 @@ ls -1 "${IGNORE_OPTS[@]}" -I "en_US*" -I "C.*" /usr/lib/locale/ | xargs -I% sh - # delete unused translations (MO files) for t in zypper gettext-runtime p11-kit; do - rm -f /usr/share/locale/*/LC_MESSAGES/$t.mo + rm -f /usr/share/locale/*/LC_MESSAGES/$t.mo done du -h -s /usr/{share,lib}/locale/ @@ -180,7 +180,7 @@ du -h -s /lib/modules /lib/firmware # disable the services included by dependencies for s in purge-kernels; do - systemctl -f disable $s || true + systemctl -f disable $s || true done # Only used for OpenCL and X11 acceleration on vmwgfx (?), saves ~50MiB diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes index 0470f30118..780ff182b1 100644 --- a/products.d/agama-products.changes +++ b/products.d/agama-products.changes @@ -1,3 +1,38 @@ +------------------------------------------------------------------- +Mon Jan 13 09:04:21 UTC 2025 - Imobach Gonzalez Sosa + +- Update SLES4SAP ID to SLES_SAP (gh#agama-project/agama#1890). + +------------------------------------------------------------------- +Fri Jan 10 16:13:30 UTC 2025 - Imobach Gonzalez Sosa + +- Add the user selectable patterns list to the SLES product + (gh#agama-project/agama#1885). + +------------------------------------------------------------------- +Fri Jan 10 14:51:35 UTC 2025 - Imobach Gonzalez Sosa + +- Update SLES name (gh#agama-project/agama#1883). + +------------------------------------------------------------------- +Wed Jan 8 14:07:21 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + +------------------------------------------------------------------- +Tue Jan 7 12:57:13 UTC 2025 - Lubos Kocman + +- Drop yast from Leap 16.0 software selection + code-o-o#leap/features#173 + +------------------------------------------------------------------- +Mon Jan 6 14:41:28 UTC 2025 - Angela Briel + +- SLES for SAP Application product: + Change product description. + (bsc#1235023) + ------------------------------------------------------------------- Thu Dec 5 11:04:06 UTC 2024 - Angela Briel diff --git a/products.d/leap_160.yaml b/products.d/leap_160.yaml index 273b72ee9a..0c9563becc 100644 --- a/products.d/leap_160.yaml +++ b/products.d/leap_160.yaml @@ -50,9 +50,6 @@ software: - basic_desktop - gnome - kde - - yast2_basis - - yast2_desktop - - yast2_server - multimedia - office mandatory_packages: diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index 136d5daf2f..dd94294086 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -1,5 +1,7 @@ -id: SLES_16.0 -name: SUSE Linux Enterprise Server 16.0 Alpha +id: SLES +name: SUSE Linux Enterprise Server 16.0 Beta +registration: "mandatory" +version: "16-0" # ------------------------------------------------------------------------------ # WARNING: When changing the product description delete the translations located # at the at translations/description key below to avoid using obsolete @@ -50,23 +52,15 @@ translations: altyapı için güvenli ve uyarlanabilir işletim sistemidir. Şirket içinde, bulutta ve uçta iş açısından kritik iş yüklerini çalıştırır. software: - installation_repositories: - # Use plain HTTP repositories, HTTPS does not work without importing the SSL - # certificate. It is safe as the repository is GPG checked and you neeed VPN - # to reach the internal server anyway. - - 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:/TEST/product/repo/SLES-Packages-16.0-aarch64/ - archs: aarch64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-ppc64le/ - archs: ppc - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-Packages-16.0-s390x/ - archs: s390 + installation_repositories: [] mandatory_patterns: - base_traditional optional_patterns: null # no optional pattern shared - user_patterns: [] + user_patterns: + - kvm_host + - cockpit + - sles_enhanced_base mandatory_packages: - NetworkManager optional_packages: null diff --git a/products.d/sles_sap_160.yaml b/products.d/sles_sap_160.yaml index 8b8db18ea8..a11ad92401 100644 --- a/products.d/sles_sap_160.yaml +++ b/products.d/sles_sap_160.yaml @@ -1,67 +1,23 @@ -id: SLES_SAP_16.0 -name: SUSE Linux Enterprise Server for SAP Applications 16.0 Alpha +id: SLES_SAP +name: SUSE Linux Enterprise Server for SAP Applications 16.0 Beta +archs: x86_64,aarch64,ppc +registration: "mandatory" +version: "16-0" # ------------------------------------------------------------------------------ # WARNING: When changing the product description delete the translations located # at the at translations/description key below to avoid using obsolete # translations!! # ------------------------------------------------------------------------------ -description: "An 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: "The leading OS for a secure and reliable SAP platform. +Endorsed for SAP deployments, SUSE Linux Enterprise Server for SAP Applications +futureproofs the SAP project, offers uninterrupted business, and minimizes +operational risks and costs." icon: SUSE.svg # Do not manually change any translations! See README.md for more details. translations: description: - ca: Una opció de servidor de Linux oberta, fiable, compatible i a prova del - 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'última. - cs: Otevřená, spolehlivá, kompatibilní a perspektivní volba linuxového serveru, - která zajišťuje kontinuitu podnikání 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ě. - de: Ein offener, zuverlässiger, kompatibler und zukunftssicherer Linux-Server, - der die Geschäftskontinuität des Unternehmens gewährleistet. Es ist das - sichere und anpassungsfähige Betriebssystem für eine langfristig - unterstützte, innovationsbereite Infrastruktur, auf der geschäftskritische - Arbeitslasten vor Ort, in der Cloud und am Netzwerkrand ausgeführt werden. - es: Una opción de servidor Linux abierta, confiable, compatible y preparada para - el futuro que garantiza la continuidad del negocio de la empresa. Es el - sistema operativo seguro y adaptable para una infraestructura lista para - la innovación y con soporte a largo plazo que ejecuta cargas de trabajo - críticas para el negocio en las instalaciones, en la nube y en el borde. - ja: オープンで信頼性が高く、各種の標準にも準拠し、将来性とビジネスの継続性を支援する Linux - サーバです。長期のサポートが提供されていることから、安全性と順応性に優れ、オンプレミスからクラウド、エッジ環境に至るまで、様々な場所で重要なビジネス処理をこなすことのできる革新性の高いインフラストラクチャです。 - pt_BR: Uma escolha de servidor Linux aberta, confiável, compatível e à prova do - futuro que garante a continuidade dos negócios da empresa. É o SO seguro e - adaptável para infraestrutura com suporte de longo prazo e pronta para - inovação, executando cargas de trabalho críticas para os negócios no - local, na nuvem e na borda. - sv: Ett öppet, pålitligt, kompatibelt och framtidssäkert Linux-serverval som - säkerställer företagets affärskontinuitet. Det är det säkra och - anpassningsbara operativsystemet för långsiktigt stödd, innovationsfärdig - infrastruktur som kör affärskritiska arbetsbelastningar på plats, i molnet - och vid kanten. - tr: İşletmenin iş sürekliliğini garanti eden açık, güvenilir, uyumlu ve geleceğe - dönük bir Linux Sunucu seçeneği. Uzun vadeli desteklenen, inovasyona hazır - altyapı için güvenli ve uyarlanabilir işletim sistemidir. Şirket içinde, - bulutta ve uçta iş açısından kritik iş yüklerini çalıştırır. software: - installation_repositories: - # Use plain HTTP repositories, HTTPS does not work without importing the SSL - # certificate. It is safe as the repository is GPG checked and you neeed VPN - # to reach the internal server anyway. - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-x86_64/ - archs: x86_64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-aarch64/ - archs: aarch64 - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-ppc64le/ - archs: ppc - - url: http://download.suse.de/ibs/SUSE:/SLFO:/Products:/SLES:/16.0:/TEST/product/repo/SLES-SAP-Packages-16.0-s390x/ - archs: s390 + installation_repositories: [] mandatory_patterns: - base_traditional diff --git a/products.d/slowroll.yaml b/products.d/slowroll.yaml index 357fac7d80..e4d1b25d36 100644 --- a/products.d/slowroll.yaml +++ b/products.d/slowroll.yaml @@ -31,6 +31,10 @@ translations: entre paquetes "estables" y más nuevos. ja: 実験的なディストリビューションではありますが、 Tumbleweed よりは比較的ゆっくりした、かつ Leap よりは速いペースで公開される openSUSE ローリングリリース型ディストリビューションです。 "安定性" と最新パッケージの中間を目指しています。 + pt_BR: Uma versão experimental e um pouco mais lenta do openSUSE, projetada para + atualizar com menos frequência que o Tumbleweed, mas com mais frequência + que o Leap, sem forçar os usuários a escolher entre pacotes "estáveis" e + mais novos. sv: En experimentell och något långsammare rullande utgåva av openSUSE utformad för att få nya paketuppdateringar mer sällan än Tumbleweed men oftare än Leap utan att tvinga användarna att välja mellan "stabila" eller nyare diff --git a/puppeteer/.gitignore b/puppeteer/.gitignore deleted file mode 100644 index f2b93381e9..0000000000 --- a/puppeteer/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -log/ diff --git a/puppeteer/LICENSE b/puppeteer/LICENSE deleted file mode 100644 index d159169d10..0000000000 --- a/puppeteer/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/puppeteer/README.md b/puppeteer/README.md deleted file mode 100644 index 6b0ce8b431..0000000000 --- a/puppeteer/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# 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 deleted file mode 100755 index 8829b86005..0000000000 --- a/puppeteer/agama-integration-tests +++ /dev/null @@ -1,34 +0,0 @@ -#! /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 - 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 - 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 "${MOCHA_OPTIONS[@]}" "$@" -fi diff --git a/puppeteer/node-prune.sh b/puppeteer/node-prune.sh deleted file mode 100755 index 7770c806c3..0000000000 --- a/puppeteer/node-prune.sh +++ /dev/null @@ -1,159 +0,0 @@ -#! /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 deleted file mode 100755 index 1737f1c92e..0000000000 --- a/puppeteer/node-puppeteer-prune.sh +++ /dev/null @@ -1,9 +0,0 @@ -#! /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-lock.json b/puppeteer/package-lock.json deleted file mode 100644 index 9647e20115..0000000000 --- a/puppeteer/package-lock.json +++ /dev/null @@ -1,2794 +0,0 @@ -{ - "name": "puppeteer", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "chai": "^5.1.1", - "mocha": "^10.6.0", - "puppeteer-core": "22.13.0" - } - }, - "node_modules/@puppeteer/browsers": { - "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" - }, - "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/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", - "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/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.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", - "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/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/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/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-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-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/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/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/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/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-core": { - "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.3", - "chromium-bidi": "0.6.0", - "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/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.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" - }, - "engines": { - "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", - "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.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" - }, - "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/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", - "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": { - "@puppeteer/browsers": { - "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.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": { - "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" - } - }, - "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", - "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==" - }, - "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.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", - "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==" - }, - "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" - } - }, - "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==" - }, - "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-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-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==" - }, - "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" - } - }, - "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" - } - }, - "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==" - }, - "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-core": { - "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.3", - "chromium-bidi": "0.6.0", - "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==" - }, - "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.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", - "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.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", - "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==" - }, - "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", - "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 deleted file mode 100644 index 71c5178aa1..0000000000 --- a/puppeteer/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "module", - "dependencies": { - "chai": "^5.1.1", - "mocha": "^10.6.0", - "puppeteer-core": "22.13.0" - } -} diff --git a/puppeteer/package/_service b/puppeteer/package/_service deleted file mode 100644 index f11ca977ba..0000000000 --- a/puppeteer/package/_service +++ /dev/null @@ -1,30 +0,0 @@ - - - @PARENT_TAG@+@TAG_OFFSET@ - v(.*) - - https://github.com/agama-project/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 deleted file mode 100644 index 2a0a0330b5..0000000000 --- a/puppeteer/package/agama-integration-tests.changes +++ /dev/null @@ -1,22 +0,0 @@ -------------------------------------------------------------------- -Fri Sep 20 11:29:10 UTC 2024 - Imobach Gonzalez Sosa - -- Change the license to GPL-2.0-or-later (gh#openSUSE/agama#1621). - -------------------------------------------------------------------- -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 - -- 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 - -- Initial version - diff --git a/puppeteer/package/agama-integration-tests.spec b/puppeteer/package/agama-integration-tests.spec deleted file mode 100644 index 931f436f02..0000000000 --- a/puppeteer/package/agama-integration-tests.spec +++ /dev/null @@ -1,77 +0,0 @@ -# -# 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-or-later -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 -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 -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} - -rm %{buildroot}%{_datadir}/agama/integration-tests/node_modules/.package-lock.json - -# symlink duplicate files -%fdupes -s %{buildroot}/%{_datadir}/agama/integration-tests - -%files -%defattr(-,root,root,-) -%doc README.md -%license LICENSE -%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 deleted file mode 100644 index cdb7c5cc22..0000000000 --- a/puppeteer/tests/test_root_password.js +++ /dev/null @@ -1,197 +0,0 @@ -import fs from "fs"; -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]; - 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 () { - // Either the root password setting is displayed or there is - // the product selection page. - const productSelectionDisplayed = await Promise.any([ - page.waitForSelector("input#rootPassword").then((s) => { - s.dispose(); - return false; - }), - page.waitForSelector("button[form='productSelectionForm']").then((s) => { - s.dispose(); - return true; - }), - ]); - - if (productSelectionDisplayed) { - const product = await page.locator("label[for='opensuse-tumbleweed']").waitHandle(); - // scroll the page so the product is visible - await product.scrollIntoView(); - await product.click(); - - await page - .locator("button[form='productSelectionForm']") - // wait until the button is enabled - .setWaitForEnabled(true) - .click(); - } else { - // no product selection displayed, mark the test as skipped - this.skip(); - } - }); - - it("should require setting the root password", async function () { - // increase the timeout for the whole test - this.timeout(60000); - await page - .locator("input#rootPassword") - // refreshing the repositories before showing the password configuration might take long time - .setTimeout(60000) - .waitHandle() - // type the new password - .then((h) => h.type("test")); - await page.locator("button[type='submit']").setWaitForEnabled(true).click(); - }); - - 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(); - - const 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(); - }); -}); diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 8517ddb9e9..4e6080b4f0 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -19,7 +19,9 @@ // find current contact information at www.suse.com. use std::collections::HashMap; +use std::str::FromStr; +use crate::dbus::get_property; use crate::error::ServiceError; use crate::software::model::RegistrationRequirement; use crate::software::proxies::SoftwareProductProxy; @@ -39,6 +41,8 @@ pub struct Product { pub description: String, /// Product icon (e.g., "default.svg") pub icon: String, + /// Registration requirement + pub registration: RegistrationRequirement, } /// D-Bus client for the software service @@ -72,11 +76,17 @@ impl<'a> ProductClient<'a> { Some(value) => value.try_into().unwrap(), None => "default.svg", }; + + let registration = get_property::(&data, "registration") + .map(|r| RegistrationRequirement::from_str(&r).unwrap_or_default()) + .unwrap_or_default(); + Product { id, name, description: description.to_string(), icon: icon.to_string(), + registration, } }) .collect(); @@ -114,13 +124,6 @@ impl<'a> ProductClient<'a> { Ok(self.registration_proxy.email().await?) } - pub async fn registration_requirement(&self) -> Result { - let requirement = self.registration_proxy.requirement().await?; - // unknown number can happen only if we do programmer mistake - let result: RegistrationRequirement = requirement.try_into().unwrap(); - Ok(result) - } - /// register product pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { let mut options: HashMap<&str, &zbus::zvariant::Value> = HashMap::new(); diff --git a/rust/agama-lib/src/product/http_client.rs b/rust/agama-lib/src/product/http_client.rs index 424a8b49f9..29a36f3b3a 100644 --- a/rust/agama-lib/src/product/http_client.rs +++ b/rust/agama-lib/src/product/http_client.rs @@ -18,6 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +use crate::software::model::RegistrationError; use crate::software::model::RegistrationInfo; use crate::software::model::RegistrationParams; use crate::software::model::SoftwareConfig; @@ -64,13 +65,29 @@ impl ProductHTTPClient { } /// register product - pub async fn register(&self, key: &str, email: &str) -> Result<(u32, String), ServiceError> { + pub async fn register(&self, key: &str, email: &str) -> Result<(), ServiceError> { // note RegistrationParams != RegistrationInfo, fun! let params = RegistrationParams { key: key.to_owned(), email: email.to_owned(), }; + let result = self + .client + .post_void("/software/registration", ¶ms) + .await; - self.client.post("/software/registration", ¶ms).await + let Err(error) = result else { + return Ok(()); + }; + + let message = match error { + ServiceError::BackendError(_, details) => { + let details: RegistrationError = serde_json::from_str(&details).unwrap(); + format!("{} (error code: {})", details.message, details.id) + } + _ => format!("Could not register the product: #{error:?}"), + }; + + Err(ServiceError::FailedRegistration(message)) } } diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs index d43a6166c4..c1a3f42297 100644 --- a/rust/agama-lib/src/product/store.rs +++ b/rust/agama-lib/src/product/store.rs @@ -68,17 +68,8 @@ impl ProductStore { } } if let Some(reg_code) = &settings.registration_code { - let (result, message); - if let Some(email) = &settings.registration_email { - (result, message) = self.product_client.register(reg_code, email).await?; - } else { - (result, message) = self.product_client.register(reg_code, "").await?; - } - // FIXME: name the magic numbers. 3 is Registration not required - // FIXME: well don't register when not required (no regcode in profile) - if result != 0 && result != 3 { - return Err(ServiceError::FailedRegistration(message)); - } + let email = settings.registration_email.as_deref().unwrap_or(""); + self.product_client.register(reg_code, email).await?; probe = true; } diff --git a/rust/agama-lib/src/software/model.rs b/rust/agama-lib/src/software/model.rs index 31f81fbcaf..8556dae11d 100644 --- a/rust/agama-lib/src/software/model.rs +++ b/rust/agama-lib/src/software/model.rs @@ -47,38 +47,36 @@ pub struct RegistrationInfo { pub key: String, /// Registration email. Empty value mean email not used or not registered. pub email: String, - /// if registration is required, optional or not needed for current product. - /// Change only if selected product is changed. - pub requirement: RegistrationRequirement, } -#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] +#[derive( + Clone, + Default, + Debug, + Serialize, + Deserialize, + strum::Display, + strum::EnumString, + utoipa::ToSchema, +)] +#[strum(serialize_all = "camelCase")] +#[serde(rename_all = "camelCase")] pub enum RegistrationRequirement { /// Product does not require registration - NotRequired = 0, + #[default] + No = 0, /// Product has optional registration Optional = 1, /// It is mandatory to register the product Mandatory = 2, } -impl TryFrom for RegistrationRequirement { - type Error = (); - - fn try_from(v: u32) -> Result { - match v { - x if x == RegistrationRequirement::NotRequired as u32 => { - Ok(RegistrationRequirement::NotRequired) - } - x if x == RegistrationRequirement::Optional as u32 => { - Ok(RegistrationRequirement::Optional) - } - x if x == RegistrationRequirement::Mandatory as u32 => { - Ok(RegistrationRequirement::Mandatory) - } - _ => Err(()), - } - } +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RegistrationError { + /// ID of error. See dbus API for possible values + pub id: u32, + /// human readable error string intended to be displayed to user + pub message: String, } /// Software resolvable type (package or pattern). diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index a365595124..e8735546c6 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -24,7 +24,12 @@ macaddr = { version = "1.0", features = ["serde_std"] } async-trait = "0.1.83" axum = { version = "0.7.7", features = ["ws"] } serde_json = "1.0.128" -tower-http = { version = "0.5.2", features = ["compression-br", "fs", "trace"] } +tower-http = { version = "0.5.2", features = [ + "compression-br", + "fs", + "trace", + "set-header", +] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-journald = "0.3.0" tracing = "0.1.40" diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index f4a0532349..40d49be018 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -37,7 +37,10 @@ use agama_lib::{ error::ServiceError, product::{proxies::RegistrationProxy, Product, ProductClient}, software::{ - model::{RegistrationInfo, RegistrationParams, ResolvableParams, SoftwareConfig}, + model::{ + RegistrationError, RegistrationInfo, RegistrationParams, ResolvableParams, + SoftwareConfig, + }, proxies::{Software1Proxy, SoftwareProductProxy}, Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy, }, @@ -49,7 +52,7 @@ use axum::{ routing::{get, post, put}, Json, Router, }; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::collections::HashMap; use tokio_stream::{Stream, StreamExt}; @@ -74,10 +77,6 @@ pub async fn software_streams(dbus: zbus::Connection) -> Result Result, Error> { - // TODO: move registration requirement to product in dbus and so just one event will be needed. - let proxy = RegistrationProxy::new(&dbus).await?; - let stream = proxy - .receive_requirement_changed() - .await - .then(|change| async move { - if let Ok(id) = change.get().await { - // unwrap is safe as possible numbers is send by our controlled dbus - return Some(Event::RegistrationRequirementChanged { - requirement: id.try_into().unwrap(), - }); - } - None - }) - .filter_map(|e| e); - Ok(stream) -} - async fn registration_email_changed_stream( dbus: zbus::Connection, ) -> Result, Error> { @@ -269,19 +247,10 @@ async fn get_registration( let result = RegistrationInfo { key: state.product.registration_code().await?, email: state.product.email().await?, - requirement: state.product.registration_requirement().await?, }; Ok(Json(result)) } -#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] -pub struct FailureDetails { - /// ID of error. See dbus API for possible values - id: u32, - /// human readable error string intended to be displayed to user - message: String, -} - /// Register product /// /// * `state`: service state. @@ -291,7 +260,7 @@ pub struct FailureDetails { context_path = "/api/software", responses( (status = 204, description = "registration successfull"), - (status = 422, description = "Registration failed. Details are in body", body = FailureDetails), + (status = 422, description = "Registration failed. Details are in body", body = RegistrationError), (status = 400, description = "The D-Bus service could not perform the action") ) )] @@ -300,10 +269,10 @@ async fn register( Json(config): Json, ) -> Result { let (id, message) = state.product.register(&config.key, &config.email).await?; - let details = FailureDetails { id, message }; if id == 0 { Ok((StatusCode::NO_CONTENT, ().into_response())) } else { + let details = RegistrationError { id, message }; Ok(( StatusCode::UNPROCESSABLE_ENTITY, Json(details).into_response(), @@ -320,13 +289,13 @@ async fn register( context_path = "/api/software", responses( (status = 200, description = "deregistration successfull"), - (status = 422, description = "De-registration failed. Details are in body", body = FailureDetails), + (status = 422, description = "De-registration failed. Details are in body", body = RegistrationError), (status = 400, description = "The D-Bus service could not perform the action") ) )] async fn deregister(State(state): State>) -> Result { let (id, message) = state.product.deregister().await?; - let details = FailureDetails { id, message }; + let details = RegistrationError { id, message }; if id == 0 { Ok((StatusCode::NO_CONTENT, ().into_response())) } else { diff --git a/rust/agama-server/src/web/service.rs b/rust/agama-server/src/web/service.rs index fb01cd5b20..1e9213a04f 100644 --- a/rust/agama-server/src/web/service.rs +++ b/rust/agama-server/src/web/service.rs @@ -21,6 +21,7 @@ use super::http::{login, login_from_query, logout, session}; use super::{config::ServiceConfig, state::ServiceState, EventsSender}; use agama_lib::auth::TokenClaims; +use axum::http::HeaderValue; use axum::{ body::Body, extract::Request, @@ -29,12 +30,14 @@ use axum::{ routing::{get, post}, Router, }; +use hyper::header::CACHE_CONTROL; use std::time::Duration; use std::{ convert::Infallible, path::{Path, PathBuf}, }; use tower::Service; +use tower_http::set_header::SetResponseHeaderLayer; use tower_http::{compression::CompressionLayer, services::ServeDir, trace::TraceLayer}; use tracing::Span; @@ -128,6 +131,10 @@ impl MainServiceBuilder { ), ) .layer(CompressionLayer::new().br(true)) + .layer(SetResponseHeaderLayer::if_not_present( + CACHE_CONTROL, + HeaderValue::from_static("no-store"), + )) .with_state(state) } } diff --git a/rust/package/_constraints b/rust/package/_constraints index 2a52ed7f2b..bf98067011 100644 --- a/rust/package/_constraints +++ b/rust/package/_constraints @@ -2,7 +2,7 @@ 4 - 20 + 25 8 diff --git a/rust/package/agama.changes b/rust/package/agama.changes index d2b6e937da..33c3ad983b 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,26 @@ +------------------------------------------------------------------- +Fri Jan 10 21:22:01 UTC 2025 - Imobach Gonzalez Sosa + +- Version 11 + +------------------------------------------------------------------- +Fri Jan 10 08:58:29 UTC 2025 - Imobach Gonzalez Sosa + +- Disable the browser cache setting the "Cache-Control" header to + "no-store" (gh#agama-project/agama#1880). + +------------------------------------------------------------------- +Thu Jan 9 12:52:05 UTC 2025 - Josef Reidinger + +- Increase disk size in _constraints to fix build on ppc + (gh#agama-project/agama#1876). + +------------------------------------------------------------------- +Wed Jan 8 14:05:34 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + ------------------------------------------------------------------- Fri Dec 20 12:17:26 UTC 2024 - Josef Reidinger diff --git a/rust/share/agama-web-server.service b/rust/share/agama-web-server.service index 0c8b27a4d8..3c02754d6c 100644 --- a/rust/share/agama-web-server.service +++ b/rust/share/agama-web-server.service @@ -6,6 +6,7 @@ After=network-online.target agama.service agama-hostname.service BindsTo=agama.service [Service] +EnvironmentFile=-/etc/agama.d/cmdline.conf Environment="AGAMA_LOG=debug,zbus=info" Type=notify ExecStart=/usr/bin/agama-web-server serve --address :::80 --address2 :::443 diff --git a/service/Gemfile.lock b/service/Gemfile.lock index 6c311cc834..1c0f0732f5 100755 --- a/service/Gemfile.lock +++ b/service/Gemfile.lock @@ -1,13 +1,17 @@ PATH remote: . specs: - agama-yast (10) + agama-yast (11.devel4) cfa (~> 1.0.2) cfa_grub2 (~> 2.0.0) cheetah (~> 1.0.0) + csv (~> 3.3) eventmachine (~> 1.2.7) fast_gettext (~> 2.3.0) + logger (~> 1.6) nokogiri (~> 1.15) + ostruct (~> 0.6.1) + prime (~> 0.1.3) rexml (~> 3.2) ruby-dbus (>= 0.23.1, < 1.0) @@ -22,14 +26,23 @@ GEM cfa (~> 1.0) cheetah (1.0.0) abstract_method (~> 1.2) + csv (3.3.2) diff-lcs (1.5.1) docile (1.4.1) eventmachine (1.2.7) fast_gettext (2.3.0) - nokogiri (1.16.7-x86_64-linux) + forwardable (1.3.3) + logger (1.6.5) + mini_portile2 (2.8.8) + nokogiri (1.16.7) + mini_portile2 (~> 2.8.2) racc (~> 1.4) + ostruct (0.6.1) packaging_rake_tasks (1.5.4) rake + prime (0.1.3) + forwardable + singleton racc (1.8.1) rake (13.0.6) rexml (3.2.9) @@ -57,6 +70,7 @@ GEM simplecov-html (0.12.3) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) + singleton (0.3.0) strscan (3.1.0) yard (0.9.36) diff --git a/service/agama-yast.gemspec b/service/agama-yast.gemspec index 1b5cf2966c..38d3043835 100644 --- a/service/agama-yast.gemspec +++ b/service/agama-yast.gemspec @@ -59,4 +59,8 @@ Gem::Specification.new do |spec| spec.add_dependency "nokogiri", "~> 1.15" spec.add_dependency "rexml", "~> 3.2" spec.add_dependency "ruby-dbus", ">= 0.23.1", "< 1.0" + spec.add_dependency "csv", "~> 3.3" + spec.add_dependency "prime", "~> 0.1.3" + spec.add_dependency "logger", "~> 1.6" + spec.add_dependency "ostruct", "~> 0.6.1" end diff --git a/service/lib/agama/dbus/clients/storage.rb b/service/lib/agama/dbus/clients/storage.rb index 094b2eaccb..c2d4363f65 100644 --- a/service/lib/agama/dbus/clients/storage.rb +++ b/service/lib/agama/dbus/clients/storage.rb @@ -65,7 +65,7 @@ def finish # Gets the current storage config. # - # @return [Hash] + # @return [Hash, nil] nil if there is no config yet. def config # Use storage iface to avoid collision with bootloader iface serialized_config = dbus_object[STORAGE_IFACE].GetConfig diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index 53209ff0a3..40b7d2b1fd 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -58,8 +58,9 @@ def available_products product.id, product.display_name, { - "description" => product.localized_description, - "icon" => product.icon + "description" => product.localized_description, + "icon" => product.icon, + "registration" => product.registration } ] end @@ -177,7 +178,7 @@ def register(reg_code, email: nil) [1, "Product not selected yet"] elsif backend.registration.reg_code [2, "Product already registered"] - elsif backend.registration.requirement == Agama::Registration::Requirement::NOT_REQUIRED + elsif backend.registration.requirement == Agama::Registration::Requirement::NO [3, "Product does not require registration"] else connect_result(first_error_code: 4) do diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 1aa0393492..2554e8fd29 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -88,7 +88,13 @@ def issues private_constant :STORAGE_INTERFACE def probe - busy_while { backend.probe } + busy_while do + # Clean trees in advance to avoid having old objects exported in D-Bus. + system_devices_tree.clean + staging_devices_tree.clean + + backend.probe + end end # Applies the given serialized config according to the JSON schema. diff --git a/service/lib/agama/manager.rb b/service/lib/agama/manager.rb index 69ef425dd8..da80a76ae0 100644 --- a/service/lib/agama/manager.rb +++ b/service/lib/agama/manager.rb @@ -282,7 +282,7 @@ def iguana? def probe_and_recover_storage storage_config = storage.config storage.probe - storage.config = storage_config unless storage_config.empty? + storage.config = storage_config if storage_config end end end diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 3466147215..7eb4ef60be 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -30,6 +30,16 @@ module Agama # Handles everything related to registration of system to SCC, RMT or similar. class Registration + # NOTE: identical and keep in sync with Software::Manager::TARGET_DIR + TARGET_DIR = "/run/agama/zypp" + private_constant :TARGET_DIR + + # FIXME: it should use TARGET_DIR instead of "/", but connect failed to read it even + # if fs_root passed as client params. Check with SCC guys why. + GLOBAL_CREDENTIALS_PATH = File.join("/", + SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) + private_constant :GLOBAL_CREDENTIALS_PATH + # Code used for registering the product. # # @return [String, nil] nil if the product is not registered yet. @@ -41,7 +51,7 @@ class Registration attr_reader :email module Requirement - NOT_REQUIRED = :not_required + NO = :no OPTIONAL = :optional MANDATORY = :mandatory end @@ -74,7 +84,7 @@ def register(code, email: "") login, password = SUSE::Connect::YaST.announce_system(connect_params, target_distro) # write the global credentials # TODO: check if we can do it in memory for libzypp - SUSE::Connect::YaST.create_credentials_file(login, password) + SUSE::Connect::YaST.create_credentials_file(login, password, GLOBAL_CREDENTIALS_PATH) target_product = OpenStruct.new( arch: Yast::Arch.rpm_arch, @@ -86,7 +96,8 @@ def register(code, email: "") # if service require specific credentials file, store it @credentials_file = credentials_from_url(@service.url) if @credentials_file - SUSE::Connect::YaST.create_credentials_file(login, password, @credentials_file) + SUSE::Connect::YaST.create_credentials_file(login, password, + File.join(TARGET_DIR, credentials_path(@credentials_file))) end Y2Packager::NewRepositorySetup.instance.add_service(@service.name) @software.add_service(@service) @@ -116,9 +127,9 @@ def deregister email: email } SUSE::Connect::YaST.deactivate_system(connect_params) - FileUtils.rm(SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE) # connect does not remove it itself + FileUtils.rm(GLOBAL_CREDENTIALS_PATH) # connect does not remove it itself if @credentials_file - FileUtils.rm(credentials_path(@credentials_file)) + FileUtils.rm(File.join(TARGET_DIR, credentials_path(@credentials_file))) @credentials_file = nil end @@ -131,10 +142,18 @@ def deregister def finish return unless reg_code - files = [credentials_path(@credentials_file), SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE] - files.each do |file| - dest = File.join(Yast::Installation.destdir, file) - FileUtils.cp(file, dest) + files = [[ + GLOBAL_CREDENTIALS_PATH, File.join(Yast::Installation.destdir, GLOBAL_CREDENTIALS_PATH) + ]] + if @credentials_file + files << [ + File.join(TARGET_DIR, credentials_path(@credentials_file)), + File.join(Yast::Installation.destdir, credentials_path(@credentials_file)) + ] + end + + files.each do |src_dest| + FileUtils.cp(*src_dest) end end @@ -142,10 +161,10 @@ def finish # # @return [Symbol] See {Requirement}. def requirement - return Requirement::NOT_REQUIRED unless product + return Requirement::NO unless product return Requirement::MANDATORY if product.repositories.none? - Requirement::NOT_REQUIRED + Requirement::NO end # Callbacks to be called when registration changes (e.g., a different product is selected). diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index 63f4675af5..3312d5138f 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -338,8 +338,6 @@ def registration # code is based on https://github.com/yast/yast-registration/blob/master/src/lib/registration/sw_mgmt.rb#L365 # rubocop:disable Metrics/AbcSize def add_service(service) - # init repos, so we are sure we operate on "/" and have GPG imported - initialize_target_repos # save repositories before refreshing added services (otherwise # pkg-bindings will treat them as removed by the service refresh and # unload them) diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb index 3fe27489a7..01b219bf09 100644 --- a/service/lib/agama/software/product.rb +++ b/service/lib/agama/software/product.rb @@ -19,6 +19,8 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/registration" + module Agama module Software # Represents a product that Agama can install. @@ -90,6 +92,12 @@ class Product # @return [Array] attr_accessor :user_patterns + # Determines if the product should be registered. + # + # @see Agama::Registration::Requirement + # @return [String] + attr_accessor :registration + # Product translations. # # @example @@ -115,6 +123,7 @@ def initialize(id) @optional_patterns = [] # nil = display all visible patterns, [] = display no patterns @user_patterns = nil + @registration = Agama::Registration::Requirement::NO @translations = {} end diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb index a9ff1d655b..a2507507bd 100644 --- a/service/lib/agama/software/product_builder.rb +++ b/service/lib/agama/software/product_builder.rb @@ -65,6 +65,8 @@ def initialize_product(id, data, attrs) product.name = data[:name] product.version = data[:version] product.icon = attrs["icon"] if attrs["icon"] + product.registration = attrs["registration"] if attrs["registration"] + product.version = attrs["version"] if attrs["version"] end end @@ -98,7 +100,6 @@ def set_translations(product, attrs) def product_data_from_config(id) { name: config.products.dig(id, "software", "base_product"), - version: config.products.dig(id, "software", "version"), icon: config.products.dig(id, "software", "icon"), labels: config.arch_elements_from( id, "software", "installation_labels", property: :label diff --git a/service/lib/agama/storage/callbacks/activate_multipath.rb b/service/lib/agama/storage/callbacks/activate_multipath.rb index 7009204447..6aeea247ad 100644 --- a/service/lib/agama/storage/callbacks/activate_multipath.rb +++ b/service/lib/agama/storage/callbacks/activate_multipath.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require "agama/question" +require "y2storage" module Agama module Storage @@ -42,6 +43,7 @@ def initialize(questions_client, logger) # @param looks_like_real_multipath [Boolean] see {Callbacks::Activate#multipath}. # @return [Boolean] def call(looks_like_real_multipath) + return true if Y2Storage::StorageEnv.instance.forced_multipath? return false unless looks_like_real_multipath questions_client.ask(question) do |question_client| diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 293259c874..a65ba97a13 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,33 @@ +------------------------------------------------------------------- +Wed Jan 15 14:26:11 UTC 2025 - José Iván López González + + - Add missing gems to the gemspec file + (gh#agama-project/agama#1899). + +------------------------------------------------------------------- +Fri Jan 10 21:22:00 UTC 2025 - Imobach Gonzalez Sosa + +- Version 11 + +------------------------------------------------------------------- +Fri Jan 10 15:44:30 UTC 2025 - José Iván López González + +- Objects from the D-Bus trees representing the storage devices are + removed before performing the probing. It prevents a segmentation + fault by accessing to old objects (gh#agama-project/agama#1884). + +------------------------------------------------------------------- +Thu Jan 9 12:21:40 UTC 2025 - Knut Anderssen + +- Activate multipath in case it is forced by the user + (gh#agama-project/agama#1875). + +------------------------------------------------------------------- +Wed Jan 8 14:05:53 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + ------------------------------------------------------------------- Mon Dec 23 18:40:01 UTC 2024 - Josef Reidinger diff --git a/service/po/ca.po b/service/po/ca.po index 681ec7c81c..20b0882326 100644 --- a/service/po/ca.po +++ b/service/po/ca.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-10-30 13:48+0000\n" "Last-Translator: David Medina \n" "Language-Team: Catalan ] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "No s'ha pogut llegir el repositori %s." @@ -113,14 +113,14 @@ msgstr "No s'ha pogut llegir el repositori %s." #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Encara no s'ha seleccionat el producte." #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "El producte ha d'estar registrat." @@ -246,37 +246,37 @@ msgid "Shrinking is not supported by this device" msgstr "Aquest dispositiu no admet l'encongiment." #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Activant els dispositius d'emmagatzematge" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Sondant els dispositius d'emmagatzematge" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Calculant la proposta d'emmagatzematge" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Seleccionant els mòduls de seguretat de Linux" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Preparant la proposta de carregador d'arrencada" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Afegint paquets relacionats amb l'emmagatzematge" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Preparant els dispositius d'emmagatzematge" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Escrivint la configuració de sistema del carregador d'arrencada" diff --git a/service/po/cs.po b/service/po/cs.po index 96beb204b3..717c83494e 100644 --- a/service/po/cs.po +++ b/service/po/cs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-11-13 18:48+0000\n" "Last-Translator: Jan Papež \n" "Language-Team: Czech =2 && n<=4) ? 1 : 2;\n" "X-Generator: Weblate 5.8.3\n" -#. Runs the startup phase -#: service/lib/agama/manager.rb:91 -msgid "Load software translations" -msgstr "Načíst překlady softwaru" - -#: service/lib/agama/manager.rb:92 -msgid "Load storage translations" -msgstr "Načíst překlady paměti" - #. Runs the config phase -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Analyze disks" msgstr "Analyzovat disky" -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Configure software" msgstr "Konfigurovat software" #. Runs the install phase #. rubocop:disable Metrics/AbcSize -#: service/lib/agama/manager.rb:127 +#: service/lib/agama/manager.rb:116 msgid "Prepare disks" msgstr "Připravit disky" -#: service/lib/agama/manager.rb:128 +#: service/lib/agama/manager.rb:117 msgid "Install software" msgstr "Instalovat software" -#: service/lib/agama/manager.rb:129 +#: service/lib/agama/manager.rb:118 msgid "Configure the system" msgstr "Konfigurovat systém" +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "Načíst překlady softwaru" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "Načíst překlady paměti" + #. Callback to handle unsigned files #. #. @param filename [String] File name @@ -105,7 +105,7 @@ msgstr "Vypočítávání návrhu softwaru" #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Nelze načíst repozitář \"%s\"" @@ -113,14 +113,14 @@ msgstr "Nelze načíst repozitář \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Není vybrán žádný produkt" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Produkt musí být zaregistrován" @@ -236,37 +236,37 @@ msgid "Shrinking is not supported by this device" msgstr "Toto zařízení nepodporuje zmenšování" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Aktivuji úložná zařízení" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Sonduji úložná zařízení" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Vypočítávání návrhu úložiště" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Vybírám bezpečnostní moduly Linuxu" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Připravuji návrh boot zavaděče" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Přidávám balíčky související s úložištěm" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Připravuji úložná zařízení" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Zapisuji konfiguraci boot zavaděče v sysconfig" diff --git a/service/po/de.po b/service/po/de.po index 970aa0a5ab..07d10e07c3 100644 --- a/service/po/de.po +++ b/service/po/de.po @@ -7,11 +7,11 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-12-12 06:48+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" @@ -19,38 +19,38 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.8.4\n" -#. Runs the startup phase -#: service/lib/agama/manager.rb:91 -msgid "Load software translations" -msgstr "Softwareübersetzungen laden" - -#: service/lib/agama/manager.rb:92 -msgid "Load storage translations" -msgstr "" - #. Runs the config phase -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Analyze disks" msgstr "Festplatten analysieren" -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Configure software" msgstr "Software konfigurieren" #. Runs the install phase #. rubocop:disable Metrics/AbcSize -#: service/lib/agama/manager.rb:127 +#: service/lib/agama/manager.rb:116 msgid "Prepare disks" msgstr "Festplatten vorbereiten" -#: service/lib/agama/manager.rb:128 +#: service/lib/agama/manager.rb:117 msgid "Install software" msgstr "Software installieren" -#: service/lib/agama/manager.rb:129 +#: service/lib/agama/manager.rb:118 msgid "Configure the system" msgstr "System konfigurieren" +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "Softwareübersetzungen laden" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "" + #. Callback to handle unsigned files #. #. @param filename [String] File name @@ -105,7 +105,7 @@ msgstr "Software-Vorschlag wird berechnet" #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Repositorium „%s“ konnte nicht gelesen werden" @@ -113,14 +113,14 @@ msgstr "Repositorium „%s“ konnte nicht gelesen werden" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Produkt noch nicht ausgewählt" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Produkt muss registriert sein" @@ -243,37 +243,37 @@ msgid "Shrinking is not supported by this device" msgstr "Verkleinern wird von diesem Gerät nicht unterstützt" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Speichergeräte werden aktiviert" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Speichergeräte werden untersucht" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Speichervorschlag wird berechnet" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Linux-Sicherheitsmodule werden ausgewählt" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Bootloader-Vorschlag wird vorbereitet" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Speicherbezogene Pakete werden hinzugefügt" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Speichergeräte werden vorbereitet" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Bootloader-Systemkonfiguration wird geschrieben" diff --git a/service/po/es.po b/service/po/es.po index 639cf64b95..e426912482 100644 --- a/service/po/es.po +++ b/service/po/es.po @@ -7,11 +7,11 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-12-11 16:48+0000\n" "Last-Translator: \"Marina J. Donis\" \n" -"Language-Team: Spanish \n" +"Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -19,38 +19,38 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.8.4\n" -#. Runs the startup phase -#: service/lib/agama/manager.rb:91 -msgid "Load software translations" -msgstr "Cargar traducciones de software" - -#: service/lib/agama/manager.rb:92 -msgid "Load storage translations" -msgstr "Cargar traducciones de almacenamiento" - #. Runs the config phase -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Analyze disks" msgstr "Analizar discos" -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Configure software" msgstr "Configurar software" #. Runs the install phase #. rubocop:disable Metrics/AbcSize -#: service/lib/agama/manager.rb:127 +#: service/lib/agama/manager.rb:116 msgid "Prepare disks" msgstr "Preparar discos" -#: service/lib/agama/manager.rb:128 +#: service/lib/agama/manager.rb:117 msgid "Install software" msgstr "Instalar software" -#: service/lib/agama/manager.rb:129 +#: service/lib/agama/manager.rb:118 msgid "Configure the system" msgstr "Configurar el sistema" +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "Cargar traducciones de software" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "Cargar traducciones de almacenamiento" + #. Callback to handle unsigned files #. #. @param filename [String] File name @@ -105,7 +105,7 @@ msgstr "Calcular la propuesta de software" #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "No se pudo leer el repositorio \"%s\"" @@ -113,14 +113,14 @@ msgstr "No se pudo leer el repositorio \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Producto aún no seleccionado" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "El producto debe estar registrado" @@ -156,8 +156,8 @@ msgstr "Falta el tipo de sistema de archivos para \"%s\"" #, perl-brace-format msgid "The file system type '%{filesystem}' is not suitable for '%{path}'" msgstr "" -"El tipo de sistema de archivos para \"%{filesystem}\" no es adecuado para \"" -"%{path}\"" +"El tipo de sistema de archivos para \"%{filesystem}\" no es adecuado para " +"\"%{path}\"" #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1', 'random_swap'). @@ -244,37 +244,37 @@ msgid "Shrinking is not supported by this device" msgstr "Este dispositivo no admite la reducción" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Activar dispositivos de almacenamiento" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Probando los dispositivos de almacenamiento" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Calcular la propuesta de almacenamiento" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Seleccionar módulos de seguridad de Linux" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Preparando la propuesta del gestor de arranque" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Agregar paquetes relacionados con el almacenamiento" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Preparando los dispositivos de almacenamiento" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Escribiendo el gestor de arranque sysconfig" diff --git a/service/po/fr.po b/service/po/fr.po index 988764c3e6..5adff6ff64 100644 --- a/service/po/fr.po +++ b/service/po/fr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-04-19 23:43+0000\n" "Last-Translator: faila fail \n" "Language-Team: French 1;\n" "X-Generator: Weblate 4.9.1\n" -#. Runs the startup phase -#: service/lib/agama/manager.rb:91 -msgid "Load software translations" -msgstr "" - -#: service/lib/agama/manager.rb:92 -msgid "Load storage translations" -msgstr "" - #. Runs the config phase -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Analyze disks" msgstr "" -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 #, fuzzy msgid "Configure software" msgstr "Sonde les logiciels" #. Runs the install phase #. rubocop:disable Metrics/AbcSize -#: service/lib/agama/manager.rb:127 +#: service/lib/agama/manager.rb:116 msgid "Prepare disks" msgstr "" -#: service/lib/agama/manager.rb:128 +#: service/lib/agama/manager.rb:117 #, fuzzy msgid "Install software" msgstr "Installation des logiciels" -#: service/lib/agama/manager.rb:129 +#: service/lib/agama/manager.rb:118 msgid "Configure the system" msgstr "" +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "" + #. Callback to handle unsigned files #. #. @param filename [String] File name @@ -107,7 +107,7 @@ msgstr "Calcul de l'offre de logiciel" #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Impossible de lire le dépôt \"%s\"" @@ -115,14 +115,14 @@ msgstr "Impossible de lire le dépôt \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Le produit n'est pas encore sélectionné" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Le produit doit être enregistré" @@ -233,37 +233,37 @@ msgid "Shrinking is not supported by this device" msgstr "" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Activation des périphériques de stockage" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Sonde les périphériques de stockage" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Calcul de la proposition de stockage" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Sélection des modules de sécurité Linux" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Préparation du chargeur d'amorçage envisagé" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Ajout des paquets relatifs au stockage" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Préparation des périphériques de stockage" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Écriture du sysconfig du chargeur d'amorçage" diff --git a/service/po/id.po b/service/po/id.po index 1e03bd596d..ab77922471 100644 --- a/service/po/id.po +++ b/service/po/id.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" -"PO-Revision-Date: 2023-12-28 21:02+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" +"PO-Revision-Date: 2024-12-25 18:50+0000\n" "Last-Translator: Arif Budiman \n" "Language-Team: Indonesian \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" - -#. Runs the startup phase -#: service/lib/agama/manager.rb:91 -msgid "Load software translations" -msgstr "" - -#: service/lib/agama/manager.rb:92 -msgid "Load storage translations" -msgstr "" +"X-Generator: Weblate 5.9.2\n" #. Runs the config phase -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Analyze disks" -msgstr "" +msgstr "Menganalisis disk" -#: service/lib/agama/manager.rb:107 -#, fuzzy +#: service/lib/agama/manager.rb:93 msgid "Configure software" -msgstr "Memeriksa Perangkat Lunak" +msgstr "Mengkonfigurasi perangkat lunak" #. Runs the install phase #. rubocop:disable Metrics/AbcSize -#: service/lib/agama/manager.rb:127 +#: service/lib/agama/manager.rb:116 msgid "Prepare disks" -msgstr "" +msgstr "Siapkan disk" -#: service/lib/agama/manager.rb:128 -#, fuzzy +#: service/lib/agama/manager.rb:117 msgid "Install software" -msgstr "Menginstal Perangkat Lunak" +msgstr "Menginstal perangkat lunak" -#: service/lib/agama/manager.rb:129 +#: service/lib/agama/manager.rb:118 msgid "Configure the system" -msgstr "" +msgstr "Mengkonfigurasi sistem" + +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "Memuat terjemahan perangkat lunak" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "Memuat terjemahan penyimpanan" #. Callback to handle unsigned files #. @@ -60,12 +58,12 @@ msgstr "" #: service/lib/agama/software/callbacks/signature.rb:63 #, perl-brace-format msgid "The file %{filename} from repository %{repo_name} (%{repo_url})" -msgstr "File %{filename} dari repositori %{repo_name} (%{repo_url})" +msgstr "Berkas %{filename} dari repositori %{repo_name} (%{repo_url})" #: service/lib/agama/software/callbacks/signature.rb:67 #, perl-brace-format msgid "The file %{filename}" -msgstr "File %{filename}" +msgstr "Berkas %{filename}" #: service/lib/agama/software/callbacks/signature.rb:71 #, perl-brace-format @@ -73,7 +71,7 @@ msgid "" "%{source} is not digitally signed. The origin and integrity of the file " "cannot be verified. Use it anyway?" msgstr "" -"%{source} tidak ditandatangani secara digital. Asal dan integritas file " +"%{source} tidak ditandatangani secara digital. Asal dan integritas berkas " "tidak dapat diverifikasi. Tetap menggunakannya?" #. Callback to handle signature verification failures @@ -107,7 +105,7 @@ msgstr "Menghitung proposal perangkat lunak" #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Tidak dapat membaca repositori \"%s\"" @@ -115,14 +113,14 @@ msgstr "Tidak dapat membaca repositori \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Produk belum dipilih" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Produk harus didaftarkan" @@ -140,24 +138,24 @@ msgstr "Ditemukan %s masalah ketergantungan." #. @return [Agama::Issue] #: service/lib/agama/storage/config_checker.rb:87 msgid "No device found for a mandatory drive" -msgstr "" +msgstr "Perangkat tidak ditemukan untuk drive wajib" #: service/lib/agama/storage/config_checker.rb:89 msgid "No device found for a mandatory partition" -msgstr "" +msgstr "Perangkat tidak ditemukan untuk partisi wajib" #. TRANSLATORS: %s is the replaced by a mount path (e.g., "/home"). #: service/lib/agama/storage/config_checker.rb:118 #, c-format msgid "Missing file system type for '%s'" -msgstr "" +msgstr "Tidak ada tipe sistem berkas untuk '%s'" #. TRANSLATORS: %{filesystem} is replaced by a file system type (e.g., "Btrfs") and #. %{path} is replaced by a mount path (e.g., "/home"). #: service/lib/agama/storage/config_checker.rb:145 #, perl-brace-format msgid "The file system type '%{filesystem}' is not suitable for '%{path}'" -msgstr "" +msgstr "Tipe sistem berkas '%{filesystem}' tidak cocok untuk '%{path}'" #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1', 'random_swap'). @@ -166,13 +164,15 @@ msgstr "" msgid "" "No passphrase provided (required for using the method '%{crypt_method}')." msgstr "" +"Kata sandi tidak disediakan (diperlukan untuk menggunakan metode " +"'%{crypt_method}')." #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1', 'random_swap'). #: service/lib/agama/storage/config_checker.rb:196 #, perl-brace-format msgid "Encryption method '%{crypt_method}' is not available in this system." -msgstr "" +msgstr "Metode enkripsi '%{crypt_method}' tidak tersedia di sistem ini." #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1', 'random_swap'). @@ -180,6 +180,7 @@ msgstr "" #, perl-brace-format msgid "'%{crypt_method}' is not a suitable method to encrypt the device." msgstr "" +"'%{crypt_method}' bukan metode yang cocok untuk mengenkripsi perangkat." #. TRANSLATORS: %s is the replaced by a device alias (e.g., "disk1"). #: service/lib/agama/storage/config_checker.rb:276 @@ -187,24 +188,26 @@ msgstr "" msgid "" "The device '%s' is used several times as target device for physical volumes" msgstr "" +"Perangkat '%s' digunakan beberapa kali sebagai perangkat target untuk volume " +"fisik" #. TRANSLATORS: %s is the replaced by a device alias (e.g., "pv1"). #: service/lib/agama/storage/config_checker.rb:350 #, c-format msgid "There is no LVM thin pool volume with alias '%s'" -msgstr "" +msgstr "Tidak ada volume pool tipis LVM dengan alias '%s'" #. TRANSLATORS: %s is the replaced by a device alias (e.g., "pv1"). #: service/lib/agama/storage/config_checker.rb:375 #, c-format msgid "There is no LVM physical volume with alias '%s'" -msgstr "" +msgstr "Tidak ada volume fisik LVM dengan alias '%s'" #. TRANSLATORS: %s is the replaced by a device alias (e.g., "disk1"). #: service/lib/agama/storage/config_checker.rb:401 #, c-format msgid "There is no target device for LVM physical volumes with alias '%s'" -msgstr "" +msgstr "Tidak ada perangkat target untuk volume fisik LVM dengan alias '%s'" #. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device #. (e.g., 'luks1'). @@ -213,6 +216,7 @@ msgstr "" msgid "" "'%{crypt_method}' is not a suitable method to encrypt the physical volumes." msgstr "" +"'%{crypt_method}' bukan metode yang cocok untuk mengenkripsi volume fisik." #. Text of the reason preventing to shrink because there is no content. #. @@ -223,46 +227,50 @@ msgid "" "case the device does contain a file system or a storage system that is not " "supported, resizing will most likely cause data loss." msgstr "" +"Baik sistem berkas maupun sistem penyimpanan tidak terdeteksi pada " +"perangkat. Jika perangkat memiliki sistem berkas atau sistem penyimpanan " +"yang tidak didukung, mengubah ukuran kemungkinan besar akan menyebabkan " +"hilangnya data." #. 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. #: service/lib/agama/storage/device_shrinking.rb:162 msgid "Shrinking is not supported by this device" -msgstr "" +msgstr "Penyusutan tidak didukung oleh perangkat ini" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Mengaktifkan perangkat penyimpanan" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Memeriksa perangkat penyimpanan" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Menghitung proposal penyimpanan" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Memilih Modul Keamanan Linux" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Mempersiapkan proposal bootloader" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Menambahkan paket terkait penyimpanan" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Mempersiapkan perangkat penyimpanan" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Menulis sysconfig bootloader" @@ -272,20 +280,21 @@ msgstr "Menulis sysconfig bootloader" #: service/lib/agama/storage/proposal.rb:287 msgid "Cannot accommodate the required file systems for installation" msgstr "" +"Tidak dapat mengakomodasi sistem berkas yang diperlukan untuk instalasi" #. Issue to communicate a generic Y2Storage error. #. #. @return [Issue] #: service/lib/agama/storage/proposal.rb:298 msgid "A problem ocurred while calculating the storage setup" -msgstr "" +msgstr "Terjadi masalah saat menghitung pengaturan penyimpanan" #. Returns an issue if there is no target device. #. #. @return [Issue, nil] #: service/lib/agama/storage/proposal_strategies/guided.rb:127 msgid "No device selected for installation" -msgstr "" +msgstr "Tidak ada perangkat yang dipilih untuk pemasangan" #. Returns an issue if any of the devices required for the proposal is not found #. @@ -296,12 +305,14 @@ msgid "The following selected device is not found in the system: %{devices}" msgid_plural "" "The following selected devices are not found in the system: %{devices}" msgstr[0] "" +"Perangkat yang dipilih berikut ini tidak ditemukan dalam sistem: %{devices}" #. Recalculates the list of issues #: service/lib/agama/users.rb:154 msgid "" "Defining a user, setting the root password or a SSH public key is required" msgstr "" +"Wajib menentukan pengguna, mengatur kata sandi root atau kunci publik SSH" #~ msgid "Probing Storage" #~ msgstr "Memeriksa Penyimpanan" diff --git a/service/po/ja.po b/service/po/ja.po index a984394dc0..123bbc7485 100644 --- a/service/po/ja.po +++ b/service/po/ja.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-10-30 00:48+0000\n" "Last-Translator: Yasuhiko Kamata \n" "Language-Team: Japanese ] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "リポジトリ\"%s\" を読み込むことができませんでした" @@ -113,14 +113,14 @@ msgstr "リポジトリ\"%s\" を読み込むことができませんでした" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "まだ製品を選択していません" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "製品を登録しなければなりません" @@ -236,37 +236,37 @@ msgid "Shrinking is not supported by this device" msgstr "このデバイスはサイズ縮小に対応していません" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "ストレージデバイスを有効化しています" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "ストレージデバイスを検出しています" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "ストレージの提案内容を作成しています" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Linux セキュリティモジュールを選択しています" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "ブートローダの提案内容を準備しています" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "ストレージ関連のパッケージを追加しています" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "ストレージデバイスを準備しています" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "ブートローダの sysconfig を書き込んでいます" diff --git a/service/po/ka.po b/service/po/ka.po index 1466a72b56..ffbaee16ac 100644 --- a/service/po/ka.po +++ b/service/po/ka.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-10-31 13:48+0000\n" "Last-Translator: Temuri Doghonadze \n" "Language-Team: Georgian ] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "" @@ -109,14 +109,14 @@ msgstr "" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "" @@ -226,37 +226,37 @@ msgid "Shrinking is not supported by this device" msgstr "" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "" diff --git a/service/po/nb_NO.po b/service/po/nb_NO.po index 80a9c74617..1c25c9616b 100644 --- a/service/po/nb_NO.po +++ b/service/po/nb_NO.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-06-29 13:46+0000\n" "Last-Translator: Martin Hansen \n" "Language-Team: Norwegian Bokmål ] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Kunne ikke lese pakkebrønn \"%s\"" @@ -113,14 +113,14 @@ msgstr "Kunne ikke lese pakkebrønn \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Produkt har ikke blitt valgt ennå" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Produkt må bli registrert" @@ -231,37 +231,37 @@ msgid "Shrinking is not supported by this device" msgstr "" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Aktiverer lagringsenheter" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Undersøker lagringsenheter" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Kalkulerer lagringsforslaget" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Velger Linux Sikkerhetsmoduler" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Forbereder forslag til oppstartslaster" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Legger til lagringsrelaterte pakker" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Forbereder lagringsenheter" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Skriver oppstartslasterens sysconfig" diff --git a/service/po/nl.po b/service/po/nl.po new file mode 100644 index 0000000000..8afd7c32f1 --- /dev/null +++ b/service/po/nl.po @@ -0,0 +1,323 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR SuSE Linux Products GmbH, Nuernberg +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" +"PO-Revision-Date: 2025-01-02 16:50+0000\n" +"Last-Translator: Natasha Ament \n" +"Language-Team: Dutch \n" +"Language: nl\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.9.2\n" + +#. Runs the config phase +#: service/lib/agama/manager.rb:93 +msgid "Analyze disks" +msgstr "Analyseer schijven" + +#: service/lib/agama/manager.rb:93 +msgid "Configure software" +msgstr "Configureer software" + +#. Runs the install phase +#. rubocop:disable Metrics/AbcSize +#: service/lib/agama/manager.rb:116 +msgid "Prepare disks" +msgstr "Prepareer schijven" + +#: service/lib/agama/manager.rb:117 +msgid "Install software" +msgstr "Installeer software" + +#: service/lib/agama/manager.rb:118 +msgid "Configure the system" +msgstr "Configureer het systeem" + +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "Laad software vertalingen" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "Laad opslag vertalingen" + +#. Callback to handle unsigned files +#. +#. @param filename [String] File name +#. @param repo_id [Integer] Repository ID. It might be -1 if there is not an associated repo. +#: service/lib/agama/software/callbacks/signature.rb:63 +#, perl-brace-format +msgid "The file %{filename} from repository %{repo_name} (%{repo_url})" +msgstr "Het bestand %{filename} van opslagruimte %{repo_name} (%{repo_url})" + +#: service/lib/agama/software/callbacks/signature.rb:67 +#, perl-brace-format +msgid "The file %{filename}" +msgstr "Het bestand %{filename}" + +#: service/lib/agama/software/callbacks/signature.rb:71 +#, perl-brace-format +msgid "" +"%{source} is not digitally signed. The origin and integrity of the file " +"cannot be verified. Use it anyway?" +msgstr "" +"%{source} is niet digitaal ondertekend. De herkomst en integriteit van het " +"bestand kan niet worden gecontroleerd. Toch gebruiken?" + +#. Callback to handle signature verification failures +#. +#. @param key [Hash] GPG key data (id, name, fingerprint, etc.) +#. @param _repo_id [Integer] Repository ID +#: service/lib/agama/software/callbacks/signature.rb:94 +#, perl-brace-format +msgid "" +"The key %{id} (%{name}) with fingerprint %{fingerprint} is unknown. Do you " +"want to trust this key?" +msgstr "" +"De sleutel {id} (%{name}) met vingerafdruk %{fingerprint} is onbekend. " +"Vertrouwt u deze sleutel?" + +#. Should an error be raised? +#: service/lib/agama/software/manager.rb:141 +msgid "Initializing sources" +msgstr "Bronnen initialiseren" + +#: service/lib/agama/software/manager.rb:146 +msgid "Refreshing repositories metadata" +msgstr "Metadata van opslagruimtes verversen" + +#: service/lib/agama/software/manager.rb:147 +msgid "Calculating the software proposal" +msgstr "Berekenen van het softwarevoorstel" + +#. Issues related to the software proposal. +#. +#. Repositories that could not be probed are reported as errors. +#. +#. @return [Array] +#: service/lib/agama/software/manager.rb:561 +#, c-format +msgid "Could not read repository \"%s\"" +msgstr "Kan opslagruimte \"%s\" niet lezen" + +#. Issue when a product is missing +#. +#. @return [Agama::Issue] +#: service/lib/agama/software/manager.rb:571 +msgid "Product not selected yet" +msgstr "Nog geen product geselecteerd" + +#. Issue when a product requires registration but it is not registered yet. +#. +#. @return [Agama::Issue] +#: service/lib/agama/software/manager.rb:580 +msgid "Product must be registered" +msgstr "Product moet geregistreerd zijn" + +#. Returns solver error messages from the last attempt +#. +#. @return [Array] Error messages +#: service/lib/agama/software/proposal.rb:223 +#, c-format +msgid "Found %s dependency issues." +msgstr "%s afhankelijkheidsissues gevonden." + +#. Issue for not found device. +#. +#. @param config [Configs::Drive, Configs::Partition] +#. @return [Agama::Issue] +#: service/lib/agama/storage/config_checker.rb:87 +msgid "No device found for a mandatory drive" +msgstr "Geen apparaat gevonden voor een vereiste schijf" + +#: service/lib/agama/storage/config_checker.rb:89 +msgid "No device found for a mandatory partition" +msgstr "Geen apparaat gevonden voor een vereiste partitie" + +#. TRANSLATORS: %s is the replaced by a mount path (e.g., "/home"). +#: service/lib/agama/storage/config_checker.rb:118 +#, c-format +msgid "Missing file system type for '%s'" +msgstr "Ontbrekend bestandssysteem voor '%s'" + +#. TRANSLATORS: %{filesystem} is replaced by a file system type (e.g., "Btrfs") and +#. %{path} is replaced by a mount path (e.g., "/home"). +#: service/lib/agama/storage/config_checker.rb:145 +#, perl-brace-format +msgid "The file system type '%{filesystem}' is not suitable for '%{path}'" +msgstr "" +"Het bestandssysteemtype '%{filesystem}' is niet geschikt voor '%{path}'" + +#. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device +#. (e.g., 'luks1', 'random_swap'). +#: service/lib/agama/storage/config_checker.rb:178 +#, perl-brace-format +msgid "" +"No passphrase provided (required for using the method '%{crypt_method}')." +msgstr "" +"Geen wachtwoordzin opgegeven (vereist bij gebruik van methode " +"'%{crypt_method}')." + +#. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device +#. (e.g., 'luks1', 'random_swap'). +#: service/lib/agama/storage/config_checker.rb:196 +#, perl-brace-format +msgid "Encryption method '%{crypt_method}' is not available in this system." +msgstr "" +"Encryptie methode '%{crypt_method}' is niet beschikbaar op dit systeem." + +#. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device +#. (e.g., 'luks1', 'random_swap'). +#: service/lib/agama/storage/config_checker.rb:226 +#, perl-brace-format +msgid "'%{crypt_method}' is not a suitable method to encrypt the device." +msgstr "" +"'%{crypt_method}' is een niet geschikte methode om het apparaat te " +"versleutelen." + +#. TRANSLATORS: %s is the replaced by a device alias (e.g., "disk1"). +#: service/lib/agama/storage/config_checker.rb:276 +#, c-format +msgid "" +"The device '%s' is used several times as target device for physical volumes" +msgstr "" +"Het apparaat '%s' is meerdere keren gebruikt al doel apparaat voor fysieke " +"volumes" + +#. TRANSLATORS: %s is the replaced by a device alias (e.g., "pv1"). +#: service/lib/agama/storage/config_checker.rb:350 +#, c-format +msgid "There is no LVM thin pool volume with alias '%s'" +msgstr "Er is geen LVM thin pool volume met alias '%s'" + +#. TRANSLATORS: %s is the replaced by a device alias (e.g., "pv1"). +#: service/lib/agama/storage/config_checker.rb:375 +#, c-format +msgid "There is no LVM physical volume with alias '%s'" +msgstr "Er is geen fysiek LVM volume met alias '%s'" + +#. TRANSLATORS: %s is the replaced by a device alias (e.g., "disk1"). +#: service/lib/agama/storage/config_checker.rb:401 +#, c-format +msgid "There is no target device for LVM physical volumes with alias '%s'" +msgstr "Er is geen doelapparaat voor fysieke LVM volumes met alias '%s'" + +#. TRANSLATORS: 'crypt_method' is the identifier of the method to encrypt the device +#. (e.g., 'luks1'). +#: service/lib/agama/storage/config_checker.rb:434 +#, perl-brace-format +msgid "" +"'%{crypt_method}' is not a suitable method to encrypt the physical volumes." +msgstr "" +"'%{crypt_method}' is geen geschikte methode voor het versleutelen van " +"fysieke volumes." + +#. 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. +#: service/lib/agama/storage/device_shrinking.rb:151 +msgid "" +"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." +msgstr "" +"Er is geen bestandssysteem of opslag systeem gedetecteerd op het apparaat. " +"In het geval dat het apparaat wel een bestandssysteem of opslagsysteem dat " +"niet wordt ondersteund, zal het aanpassen van de grootte zeer waarschijnlijk " +"leiden tot gegevensverlies." + +#. 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. +#: service/lib/agama/storage/device_shrinking.rb:162 +msgid "Shrinking is not supported by this device" +msgstr "Verkleinen wordt niet ondersteund door dit apparaat" + +#. Probes storage devices and performs an initial proposal +#: service/lib/agama/storage/manager.rb:120 +msgid "Activating storage devices" +msgstr "Activeren van opslag apparaten" + +#: service/lib/agama/storage/manager.rb:121 +msgid "Probing storage devices" +msgstr "Onderzoeken van opslag apparaten" + +#: service/lib/agama/storage/manager.rb:122 +msgid "Calculating the storage proposal" +msgstr "Berekenen van het opslagvoorstel" + +#: service/lib/agama/storage/manager.rb:123 +msgid "Selecting Linux Security Modules" +msgstr "Selecteren van Linux beveiligingsmodules" + +#. Prepares the partitioning to install the system +#: service/lib/agama/storage/manager.rb:131 +msgid "Preparing bootloader proposal" +msgstr "Voorbereiden bootloader voorstel" + +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 +msgid "Adding storage-related packages" +msgstr "Toevoegen opslag gerelateerde pakketten" + +#: service/lib/agama/storage/manager.rb:139 +msgid "Preparing the storage devices" +msgstr "Voorbereiden van de opslag apparaten" + +#: service/lib/agama/storage/manager.rb:140 +msgid "Writing bootloader sysconfig" +msgstr "Schrijven van de bootloader sysconfig" + +#. Issue representing the proposal is not valid. +#. +#. @return [Issue] +#: service/lib/agama/storage/proposal.rb:287 +msgid "Cannot accommodate the required file systems for installation" +msgstr "Kan de vereiste bestandssystemen voor installatie niet toepassen" + +#. Issue to communicate a generic Y2Storage error. +#. +#. @return [Issue] +#: service/lib/agama/storage/proposal.rb:298 +msgid "A problem ocurred while calculating the storage setup" +msgstr "Een probleem is ontstaan bij het berekenen van de opslag opstelling" + +#. Returns an issue if there is no target device. +#. +#. @return [Issue, nil] +#: service/lib/agama/storage/proposal_strategies/guided.rb:127 +msgid "No device selected for installation" +msgstr "Geen apparaat geselecteerd voor installatie" + +#. Returns an issue if any of the devices required for the proposal is not found +#. +#. @return [Issue, nil] +#: service/lib/agama/storage/proposal_strategies/guided.rb:143 +#, perl-brace-format +msgid "The following selected device is not found in the system: %{devices}" +msgid_plural "" +"The following selected devices are not found in the system: %{devices}" +msgstr[0] "" +"Het volgende, geselecteerde apparaat is niet gevonden in het systeem: " +"%{devices}" +msgstr[1] "" +"De volgende, geselecteerde apparaten zij niet gevonden in het systeem: " +"%{devices}" + +#. Recalculates the list of issues +#: service/lib/agama/users.rb:154 +msgid "" +"Defining a user, setting the root password or a SSH public key is required" +msgstr "" +"Het aanmaken van een gebruiker, het instellen van een root password of een " +"publieke SSH sleutel is vereist" diff --git a/service/po/pt_BR.po b/service/po/pt_BR.po index 59e89232bf..56a7e99af1 100644 --- a/service/po/pt_BR.po +++ b/service/po/pt_BR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-11-14 15:48+0000\n" "Last-Translator: Rodrigo Macedo \n" "Language-Team: Portuguese (Brazil) 1;\n" "X-Generator: Weblate 5.8.3\n" -#. Runs the startup phase -#: service/lib/agama/manager.rb:91 -msgid "Load software translations" -msgstr "Carregar traduções de software" - -#: service/lib/agama/manager.rb:92 -msgid "Load storage translations" -msgstr "Traduções de armazenamento de carga" - #. Runs the config phase -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Analyze disks" msgstr "Analizar discos" -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Configure software" msgstr "Configurar software" #. Runs the install phase #. rubocop:disable Metrics/AbcSize -#: service/lib/agama/manager.rb:127 +#: service/lib/agama/manager.rb:116 msgid "Prepare disks" msgstr "Preparar discos" -#: service/lib/agama/manager.rb:128 +#: service/lib/agama/manager.rb:117 msgid "Install software" msgstr "Instalar software" -#: service/lib/agama/manager.rb:129 +#: service/lib/agama/manager.rb:118 msgid "Configure the system" msgstr "Configurar o sistema" +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "Carregar traduções de software" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "Traduções de armazenamento de carga" + #. Callback to handle unsigned files #. #. @param filename [String] File name @@ -105,7 +105,7 @@ msgstr "Calculando a proposta de software" #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Não foi possível ler o repositório \"%s\"" @@ -113,14 +113,14 @@ msgstr "Não foi possível ler o repositório \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Produto ainda não selecionado" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "O produto deve ser registrado" @@ -241,37 +241,37 @@ msgid "Shrinking is not supported by this device" msgstr "O encolhimento não é suportado por este dispositivo" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Ativando dispositivos de armazenamento" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Sondando dispositivos de armazenamento" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Calculando a proposta de armazenamento" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Selecionando módulos de segurança do Linux" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Preparando proposta de bootloader" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Adicionando pacotes relacionados ao armazenamento" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Preparando os dispositivos de armazenamento" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Escrevendo sysconfig do bootloader" diff --git a/service/po/ru.po b/service/po/ru.po index 30a34af926..6007f791a4 100644 --- a/service/po/ru.po +++ b/service/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-06-26 10:46+0000\n" "Last-Translator: Aleksey Fedorov \n" "Language-Team: Russian =20) ? 1 : 2;\n" "X-Generator: Weblate 5.6\n" -#. Runs the startup phase -#: service/lib/agama/manager.rb:91 -msgid "Load software translations" -msgstr "" - -#: service/lib/agama/manager.rb:92 -msgid "Load storage translations" -msgstr "" - #. Runs the config phase -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Analyze disks" msgstr "Анализ дисков" -#: service/lib/agama/manager.rb:107 +#: service/lib/agama/manager.rb:93 msgid "Configure software" msgstr "Настройка программного обеспечения" #. Runs the install phase #. rubocop:disable Metrics/AbcSize -#: service/lib/agama/manager.rb:127 +#: service/lib/agama/manager.rb:116 msgid "Prepare disks" msgstr "Подготовка дисков" -#: service/lib/agama/manager.rb:128 +#: service/lib/agama/manager.rb:117 msgid "Install software" msgstr "Установка программного обеспечения" -#: service/lib/agama/manager.rb:129 +#: service/lib/agama/manager.rb:118 msgid "Configure the system" msgstr "Настройка системы" +#. rubocop:enable Metrics/AbcSize +#: service/lib/agama/manager.rb:156 +msgid "Load software translations" +msgstr "" + +#: service/lib/agama/manager.rb:157 +msgid "Load storage translations" +msgstr "" + #. Callback to handle unsigned files #. #. @param filename [String] File name @@ -106,7 +106,7 @@ msgstr "Расчет предложения по программному обе #. Repositories that could not be probed are reported as errors. #. #. @return [Array] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Не удалось прочитать репозиторий \"%s\"" @@ -114,14 +114,14 @@ msgstr "Не удалось прочитать репозиторий \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Продукт еще не выбран" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Продукт должен быть зарегистрирован" @@ -232,37 +232,37 @@ msgid "Shrinking is not supported by this device" msgstr "" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Активация устройств хранения" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Поиск устройств хранения" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Расчет предложения по хранению" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Выбор модулей безопасности Linux" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Подготовка предложения по загрузчику" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Добавление пакетов, связанных с хранилищем" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Подготовка устройств хранения" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Запись системной конфигурации загрузчика" diff --git a/service/po/sv.po b/service/po/sv.po index c546697be5..9181b4674b 100644 --- a/service/po/sv.po +++ b/service/po/sv.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-10-29 12:48+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish ] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "Kunde inte läsa förråd \"%s\"" @@ -113,14 +113,14 @@ msgstr "Kunde inte läsa förråd \"%s\"" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Produkt har inte valts ännu" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Produkt måste registreras" @@ -238,37 +238,37 @@ msgid "Shrinking is not supported by this device" msgstr "Krympning stöds inte på den här enheten" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Aktiverar lagringsenheter" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Undersöker lagringsenheter" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Beräknar lagringsförslag" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Väljer Linux säkerhetsmoduler" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Förbereder starthanterare förslag" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Lägger till lagrings-relaterade paket" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Förbereder lagringsenheter" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "Skriver starthanterarens sysconfig" diff --git a/service/po/tr.po b/service/po/tr.po index 2d705b6f6b..27aaecdde9 100644 --- a/service/po/tr.po +++ b/service/po/tr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-11-07 21:48+0000\n" "Last-Translator: yok4 \n" "Language-Team: Turkish ] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "\"%s\" deposu okunamadı" @@ -113,14 +113,14 @@ msgstr "\"%s\" deposu okunamadı" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "Ürün henüz seçilmedi" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "Ürün kayıtlı olmalı" @@ -237,37 +237,37 @@ msgid "Shrinking is not supported by this device" msgstr "Bu cihaz küçültmeyi desteklemiyor" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "Depolama aygıtlarını etkinleştiriliyor" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "Depolama aygıtları araştırılıyor" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "Depolama önerisi hesaplanıyor" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "Linux Güvenlik Modüllerini Seçme" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "Bootloader bölümü hazırlanıyor" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "Depolamayla ilgili paketler ekleme" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "Depolama cihazları hazırlanıyor" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "bootloader sysconfig yazılıyor" diff --git a/service/po/zh_Hans.po b/service/po/zh_Hans.po index 36265f4a08..f128a76d0d 100644 --- a/service/po/zh_Hans.po +++ b/service/po/zh_Hans.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-10 02:55+0000\n" +"POT-Creation-Date: 2025-01-12 02:47+0000\n" "PO-Revision-Date: 2024-07-03 14:46+0000\n" "Last-Translator: Monstorix \n" "Language-Team: Chinese (Simplified) ] -#: service/lib/agama/software/manager.rb:563 +#: service/lib/agama/software/manager.rb:561 #, c-format msgid "Could not read repository \"%s\"" msgstr "无法读取仓库 “ %s”" @@ -111,14 +111,14 @@ msgstr "无法读取仓库 “ %s”" #. Issue when a product is missing #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:573 +#: service/lib/agama/software/manager.rb:571 msgid "Product not selected yet" msgstr "尚未选择产品" #. Issue when a product requires registration but it is not registered yet. #. #. @return [Agama::Issue] -#: service/lib/agama/software/manager.rb:582 +#: service/lib/agama/software/manager.rb:580 msgid "Product must be registered" msgstr "产品必须注册" @@ -229,37 +229,37 @@ msgid "Shrinking is not supported by this device" msgstr "" #. Probes storage devices and performs an initial proposal -#: service/lib/agama/storage/manager.rb:115 +#: service/lib/agama/storage/manager.rb:120 msgid "Activating storage devices" msgstr "正在激活存储设备" -#: service/lib/agama/storage/manager.rb:116 +#: service/lib/agama/storage/manager.rb:121 msgid "Probing storage devices" msgstr "正在探测存储设备" -#: service/lib/agama/storage/manager.rb:117 +#: service/lib/agama/storage/manager.rb:122 msgid "Calculating the storage proposal" msgstr "正在计算存储提案" -#: service/lib/agama/storage/manager.rb:118 +#: service/lib/agama/storage/manager.rb:123 msgid "Selecting Linux Security Modules" msgstr "正在选择 Linux 安全模块" #. Prepares the partitioning to install the system -#: service/lib/agama/storage/manager.rb:126 +#: service/lib/agama/storage/manager.rb:131 msgid "Preparing bootloader proposal" msgstr "正在准备引导加载程序提案" -#. first make bootloader proposal to be sure that required packages are installed -#: service/lib/agama/storage/manager.rb:131 +#. then also apply changes to that proposal +#: service/lib/agama/storage/manager.rb:138 msgid "Adding storage-related packages" msgstr "正在添加存储相关软件包" -#: service/lib/agama/storage/manager.rb:132 +#: service/lib/agama/storage/manager.rb:139 msgid "Preparing the storage devices" msgstr "正在准备存储设备" -#: service/lib/agama/storage/manager.rb:133 +#: service/lib/agama/storage/manager.rb:140 msgid "Writing bootloader sysconfig" msgstr "正在写入引导加载程序 sysconfig" diff --git a/service/share/agama.service b/service/share/agama.service index f9b9bcfeb3..dff0d75ea3 100644 --- a/service/share/agama.service +++ b/service/share/agama.service @@ -5,6 +5,7 @@ After=network-online.target [Service] Type=forking ExecStart=/usr/bin/agamactl --daemon +EnvironmentFile=-/etc/agama.d/cmdline.conf PIDFile=/run/agama/bus.pid User=root TimeoutStopSec=5 diff --git a/service/test/agama/dbus/software/product_test.rb b/service/test/agama/dbus/software/product_test.rb index 8ca3d45787..cadcdf8dae 100644 --- a/service/test/agama/dbus/software/product_test.rb +++ b/service/test/agama/dbus/software/product_test.rb @@ -158,7 +158,7 @@ end context "if the registration is not required" do - let(:requirement) { Agama::Registration::Requirement::NOT_REQUIRED } + let(:requirement) { Agama::Registration::Requirement::NO } it "returns 0" do expect(subject.requirement).to eq(0) diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index a2dd6731b8..1efb802ab5 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -33,6 +33,7 @@ subject { described_class.new(manager, logger) } let(:manager) { instance_double(Agama::Software::Manager) } + let(:product) { Agama::Software::Product.new("test").tap { |p| p.version = "5.0" } } let(:logger) { Logger.new($stdout, level: :warn) } @@ -90,7 +91,9 @@ it "creates credentials file" do expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345") + .with("test-user", "12345", "/etc/zypp/credentials.d/SCCcredentials") + # TODO: when fixing suse-connect read of fsroot + # .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/SCCcredentials") subject.register("11112222", email: "test@test.com") end @@ -117,13 +120,14 @@ before do allow(subject).to receive(:credentials_from_url) - .with("https://credentials/file").and_return("credentials") + .with("https://credentials/file") + .and_return("productA") end it "creates the credentials file" do expect(SUSE::Connect::YaST).to receive(:create_credentials_file) expect(SUSE::Connect::YaST).to receive(:create_credentials_file) - .with("test-user", "12345", "credentials") + .with("test-user", "12345", "/run/agama/zypp/etc/zypp/credentials.d/productA") subject.register("11112222", email: "test@test.com") end @@ -346,7 +350,7 @@ let(:product) { nil } it "returns not required" do - expect(subject.requirement).to eq(Agama::Registration::Requirement::NOT_REQUIRED) + expect(subject.requirement).to eq(Agama::Registration::Requirement::NO) end end @@ -359,7 +363,7 @@ let(:repositories) { ["https://repo"] } it "returns not required" do - expect(subject.requirement).to eq(Agama::Registration::Requirement::NOT_REQUIRED) + expect(subject.requirement).to eq(Agama::Registration::Requirement::NO) end end @@ -372,4 +376,41 @@ end end end + + describe "#finish" do + context "system is not registered" do + before do + subject.instance_variable_set(:@reg_code, nil) + end + + it "do nothing" do + expect(::FileUtils).to_not receive(:cp) + + subject.finish + end + end + + context "system is registered" do + before do + subject.instance_variable_set(:@reg_code, "test") + subject.instance_variable_set(:@credentials_file, "test") + Yast::Installation.destdir = "/mnt" + allow(::FileUtils).to receive(:cp) + end + + it "copies global credentials file" do + expect(::FileUtils).to receive(:cp).with("/etc/zypp/credentials.d/SCCcredentials", + "/mnt/etc/zypp/credentials.d/SCCcredentials") + + subject.finish + end + + it "copies product credentials file" do + expect(::FileUtils).to receive(:cp).with("/run/agama/zypp/etc/zypp/credentials.d/test", + "/mnt/etc/zypp/credentials.d/test") + + subject.finish + end + end + end end diff --git a/service/test/agama/software/product_builder_test.rb b/service/test/agama/software/product_builder_test.rb index a48bc87ceb..cf67c02b0f 100644 --- a/service/test/agama/software/product_builder_test.rb +++ b/service/test/agama/software/product_builder_test.rb @@ -41,6 +41,8 @@ "id" => "Test1", "name" => "Product Test 1", "description" => "This is a test product named Test 1", + "version" => "1.0", + "registration" => "mandatory", "translations" => { "description" => { "cs" => "Czech", @@ -84,19 +86,19 @@ "archs" => "aarch64" } ], - "base_product" => "Test1", - "version" => "1.0" + "base_product" => "Test1" } }, { - "id" => "Test2", - "name" => "Product Test 2", - "description" => "This is a test product named Test 2", - "archs" => "x86_64,aarch64", - "software" => { + "id" => "Test2", + "name" => "Product Test 2", + "description" => "This is a test product named Test 2", + "archs" => "x86_64,aarch64", + "version" => "2.0", + "registration" => "optional", + "software" => { "mandatory_patterns" => ["pattern2-1"], - "base_product" => "Test2", - "version" => "2.0" + "base_product" => "Test2" } }, { @@ -143,6 +145,7 @@ description: "This is a test product named Test 1", name: "Test1", version: "1.0", + registration: "mandatory", repositories: ["https://repos/test1/x86_64/product/"], mandatory_patterns: ["pattern1-1", "pattern1-2"], optional_patterns: ["pattern1-3"], @@ -200,6 +203,7 @@ description: "This is a test product named Test 2", name: "Test2", version: "2.0", + registration: "optional", repositories: [], mandatory_patterns: ["pattern2-1"], optional_patterns: [], diff --git a/web/jest.config.js b/web/jest.config.js index 670ff97b57..728e681612 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -135,7 +135,7 @@ module.exports = { // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], - setupFilesAfterEnv: ["/src/setupTests.js"], + setupFilesAfterEnv: ["/src/setupTests.ts"], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 398cf686d1..b856e7cb7c 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,51 @@ +------------------------------------------------------------------- +Mon Jan 13 11:11:49 UTC 2025 - David Diaz + +- Do not allow changing selected product after registering one + (related to gh#agama-project/agama#1891). + +------------------------------------------------------------------- +Fri Jan 10 21:22:02 UTC 2025 - Imobach Gonzalez Sosa + +- Version 11 + +------------------------------------------------------------------- +Fri Jan 10 15:56:35 UTC 2025 - José Iván López González + +- Add storage reprobing and recalculate proposal when going back to + either the proposal page or the devices selector if the system + is deprecated (gh#agama-project/agama#1884). + +------------------------------------------------------------------- +Fri Jan 10 13:46:42 UTC 2025 - David Diaz + +- Drop the feature for deregistering a product + (gh#agama-project/agama#1882). +------------------------------------------------------------------- + +Fri Jan 10 13:22:27 UTC 2025 - Imobach Gonzalez Sosa + +- Do not allow changing the storage setup when Agama is using the + new storage settings (gh#agama-project/agama#1881). + +------------------------------------------------------------------- +Wed Jan 8 16:07:13 UTC 2025 - Imobach Gonzalez Sosa + +- Add support for products registration (jsc#PED-11192, + gh#agama-project/agama#1809). + +------------------------------------------------------------------- +Wed Jan 8 15:16:51 UTC 2025 - David Diaz + +- Use product ID instead of slug to link labels and descriptions to + their corresponding radio inputs (gh#agama-project/agama#1873). + +------------------------------------------------------------------- +Wed Jan 8 13:12:44 UTC 2025 - David Diaz + +- Show the cancel action at product selection page only when + a product is already selected (gh#agama-project/agama#1871). + ------------------------------------------------------------------- Fri Dec 20 12:53:41 UTC 2024 - David Diaz diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index a7a58a5e8d..b106985e13 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -40,8 +40,8 @@ jest.mock("~/api/l10n", () => ({ updateConfig: jest.fn(), })); -const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed" }; -const microos: Product = { id: "Leap Micro", name: "openSUSE Micro" }; +const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: "no" }; +const microos: Product = { id: "Leap Micro", name: "openSUSE Micro", registration: "no" }; // list of available products let mockProducts: Product[]; diff --git a/web/src/App.tsx b/web/src/App.tsx index 74f37609e2..06b45ba55d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -43,7 +43,7 @@ function App() { const location = useLocation(); const { isBusy, phase } = useInstallerStatus({ suspense: true }); const { connected, error } = useInstallerClientStatus(); - const { selectedProduct, products } = useProduct(); + const { selectedProduct, products } = useProduct({ suspense: true }); const { language } = useInstallerL10n(); const { password: isRootPasswordDefined, sshkey: rootSSHKey } = useRootUser(); useL10nConfigChanges(); diff --git a/web/src/Protected.jsx b/web/src/Protected.tsx similarity index 100% rename from web/src/Protected.jsx rename to web/src/Protected.tsx diff --git a/web/src/agama.js b/web/src/agama.js deleted file mode 100644 index 45851db41d..0000000000 --- a/web/src/agama.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -/** - * This module provides a global "agama" object which can be use from other - * scripts like "po.js". - */ - -const agama = { - // the current language - language: "en", -}; - -// mapping with the current translations -let translations = {}; -// function used for computing the plural form index -let plural_fn; - -// set the current translations, called from po..js -agama.locale = function locale(po) { - if (po) { - Object.assign(translations, po); - - const header = po[""]; - if (header) { - if (header["plural-forms"]) plural_fn = header["plural-forms"]; - if (header.language) agama.language = header.language; - } - } else if (po === null) { - translations = {}; - plural_fn = undefined; - agama.language = "en"; - } -}; - -/** - * get a translation for a singular text - * @param {string} str input text - * @return translated text or the original text if the translation is not found - */ -agama.gettext = function gettext(str) { - if (translations) { - const translated = translations[str]; - if (translated?.[0]) return translated[0]; - } - - // fallback, return the original text - return str; -}; - -/** - * get a translation for a plural text - * @param {string} str1 input singular text - * @param {string} strN input plural text - * @param {number} n the actual number which decides whether to use the - * singular or plural form (of which plural form if there are several of them) - * @return translated text or the original text if the translation is not found - */ -agama.ngettext = function ngettext(str1, strN, n) { - if (translations && plural_fn) { - // plural form translations are indexed by the singular variant - const translation = translations[str1]; - - if (translation) { - const plural_index = plural_fn(n); - - // the plural function either returns direct index (integer) in the plural - // translations or a boolean indicating simple plural form which - // needs to be converted to index 0 (singular) or 1 (plural) - const index = plural_index === true ? 1 : plural_index || 0; - - if (translation[index]) return translation[index]; - } - } - - // fallback, return the original text - return n === 1 ? str1 : strN; -}; - -/** - * Wrapper around Intl.ListFormat to get a language-specific representation of the given list of - * strings. - * - * @param {string[]} list iterable list of strings to represent - * @param {object} options passed to the Intl.ListFormat constructor - * @return {string} concatenation of the original strings with the correct language-specific - * separators according to the currently selected language for the Agama UI - */ -agama.formatList = function formatList(list, options) { - const formatter = new Intl.ListFormat(agama.language, options); - return formatter.format(list); -}; - -export default agama; diff --git a/web/src/agama.ts b/web/src/agama.ts new file mode 100644 index 0000000000..061444bf0a --- /dev/null +++ b/web/src/agama.ts @@ -0,0 +1,112 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +/** + * This module provides a global "agama" object which can be use from other + * scripts like "po.js". + */ + +// mapping with the current translations +let translations = {}; +// function used for computing the plural form index +let plural_fn: (n: number) => boolean; + +const agama = { + // the current language + language: "en", + + // set the current translations, called from po..js + locale: (po) => { + if (po) { + Object.assign(translations, po); + + const header = po[""]; + if (header) { + if (header["plural-forms"]) plural_fn = header["plural-forms"]; + if (header.language) agama.language = header.language; + } + } else if (po === null) { + translations = {}; + plural_fn = undefined; + agama.language = "en"; + } + }, + + /** + * Get a translation for a singular text + * @param str input text + * @return translated text or the original text if the translation is not found + */ + gettext: (str: string): string => { + if (translations) { + const translated = translations[str]; + if (translated?.[0]) return translated[0]; + } + + // fallback, return the original text + return str; + }, + + /** + * get a translation for a plural text + * @param str1 input singular text + * @param strN input plural text + * @param n the actual number which decides whether to use the + * singular or plural form (of which plural form if there are several of them) + * @return translated text or the original text if the translation is not found + */ + ngettext: (str1: string, strN: string, n: number) => { + if (translations && plural_fn) { + // plural form translations are indexed by the singular variant + const translation = translations[str1]; + + if (translation) { + const plural_index = plural_fn(n); + + // the plural function either returns direct index (integer) in the plural + // translations or a boolean indicating simple plural form which + // needs to be converted to index 0 (singular) or 1 (plural) + const index = plural_index === true ? 1 : plural_index || 0; + + if (translation[index]) return translation[index]; + } + } + + // fallback, return the original text + return n === 1 ? str1 : strN; + }, + + /** + * Wrapper around Intl.ListFormat to get a language-specific representation of the given list of + * strings. Returns the concatenation of the original strings with the correct language-specific + * separators according to the currently selected language for the Agama UI + * + * @param list iterable list of strings to represent + * @param options passed to the Intl.ListFormat constructor + */ + formatList: (list: string[], options: object): string => { + const formatter = new Intl.ListFormat(agama.language, options); + return formatter.format(list); + }, +}; + +export default agama; diff --git a/web/src/api/software.ts b/web/src/api/software.ts index 3be92e58ff..38a6f72bd9 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -20,8 +20,14 @@ * find current contact information at www.suse.com. */ -import { Pattern, Product, SoftwareConfig, SoftwareProposal } from "~/types/software"; -import { get, put } from "~/api/http"; +import { + Pattern, + Product, + SoftwareConfig, + RegistrationInfo, + SoftwareProposal, +} from "~/types/software"; +import { get, post, put } from "~/api/http"; /** * Returns the software configuration @@ -38,6 +44,11 @@ const fetchProposal = (): Promise => get("/api/software/propos */ const fetchProducts = (): Promise => get("/api/software/products"); +/** + * Returns an object with the registration info + */ +const fetchRegistration = (): Promise => get("/api/software/registration"); + /** * Returns the list of patterns for the selected product */ @@ -50,4 +61,18 @@ const fetchPatterns = (): Promise => get("/api/software/patterns"); */ const updateConfig = (config: SoftwareConfig) => put("/api/software/config", config); -export { fetchConfig, fetchPatterns, fetchProposal, fetchProducts, updateConfig }; +/** + * Request registration of selected product with given key + */ +const register = ({ key, email }: { key: string; email?: string }) => + post("/api/software/registration", { key, email }); + +export { + fetchConfig, + fetchPatterns, + fetchProposal, + fetchProducts, + fetchRegistration, + updateConfig, + register, +}; diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts index 2c8ebedd13..b09a0b64f2 100644 --- a/web/src/api/storage.ts +++ b/web/src/api/storage.ts @@ -62,8 +62,8 @@ const findStorageJob = (id: string): Promise => */ const refresh = async (): Promise => { const settings = await fetchSettings(); - await probe(); - await calculate(settings); + await probe().catch(console.log); + await calculate(settings).catch(console.log); }; export { diff --git a/web/src/api/storage/proposal.ts b/web/src/api/storage/proposal.ts index 727da24e77..22ce49f066 100644 --- a/web/src/api/storage/proposal.ts +++ b/web/src/api/storage/proposal.ts @@ -38,7 +38,9 @@ const fetchDefaultVolume = (mountPath: string): Promise => { return get(`/api/storage/product/volume_for?mount_path=${path}`); }; -const fetchSettings = (): Promise => get("/api/storage/proposal/settings"); +// NOTE: the settings might not exist. +const fetchSettings = (): Promise => + get("/api/storage/proposal/settings").catch(() => null); const fetchActions = (): Promise => get("/api/storage/devices/actions"); diff --git a/web/src/client/index.js b/web/src/client/index.ts similarity index 55% rename from web/src/client/index.js rename to web/src/client/index.ts index 5e161ef5f3..97826e5cd5 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2021-2023] SUSE LLC + * Copyright (c) [2021-2024] SUSE LLC * * All Rights Reserved. * @@ -20,28 +20,40 @@ * find current contact information at www.suse.com. */ -// @ts-check - import { WSClient } from "./ws"; -/** - * @typedef {object} InstallerClient - * @property {() => boolean} isConnected - determines whether the client is connected - * @property {() => boolean} isRecoverable - determines whether the client is recoverable after disconnected - * @property {(handler: () => void) => (() => void)} onConnect - registers a handler to run - * @property {(handler: () => void) => (() => void)} onDisconnect - registers a handler to run - * when the connection is lost. It returns a function to deregister the - * handler. - * @property {(handler: (any) => void) => (() => void)} onEvent - registers a handler to run on events - */ +type VoidFn = () => void; +type BooleanFn = () => boolean; +type EventHandlerFn = (event) => void; + +export type InstallerClient = { + /** Whether the client is connected. */ + isConnected: BooleanFn; + /** Whether the client is recoverable after disconnecting. */ + isRecoverable: BooleanFn; + /** + * Registers a handler to run when connection is set. It returns a function + * for deregistering the handler. + */ + onConnect: (handler: VoidFn) => VoidFn; + /** + * Registers a handler to run when connection is lost. It returns a function + * for deregistering the handler. + */ + onDisconnect: (handler: VoidFn) => VoidFn; + /** + * Registers a handler to run on events. It returns a function for + * deregistering the handler. + */ + onEvent: (handler: EventHandlerFn) => VoidFn; +}; /** * Creates the Agama client * - * @param {URL} url - URL of the HTTP API. - * @return {InstallerClient} + * @param url - URL of the HTTP API. */ -const createClient = (url) => { +const createClient = (url: URL): InstallerClient => { url.hash = ""; url.pathname = url.pathname.concat("api/ws"); url.protocol = url.protocol === "http:" ? "ws" : "wss"; @@ -53,9 +65,9 @@ const createClient = (url) => { return { isConnected, isRecoverable, - onConnect: (handler) => ws.onOpen(handler), - onDisconnect: (handler) => ws.onClose(handler), - onEvent: (handler) => ws.onEvent(handler), + onConnect: (handler: VoidFn) => ws.onOpen(handler), + onDisconnect: (handler: VoidFn) => ws.onClose(handler), + onEvent: (handler: EventHandlerFn) => ws.onEvent(handler), }; }; diff --git a/web/src/client/ws.js b/web/src/client/ws.ts similarity index 85% rename from web/src/client/ws.js rename to web/src/client/ws.ts index b63c8e2a51..608001f2cd 100644 --- a/web/src/client/ws.js +++ b/web/src/client/ws.ts @@ -20,19 +20,13 @@ * find current contact information at www.suse.com. */ -// @ts-check - -/** - * @callback RemoveFn - * @return {void} - */ +type RemoveFn = () => void; +type BaseHandlerFn = () => void; +type EventHandlerFn = (event) => void; /** * Enum for the WebSocket states. - * - * */ - const SocketStates = Object.freeze({ CONNECTED: 0, CONNECTING: 1, @@ -52,10 +46,25 @@ const ATTEMPT_INTERVAL = 1000; * HTTPClient API. */ class WSClient { + url: string; + + client: WebSocket; + + handlers: { + open: Array; + close: Array; + error: Array; + events: Array; + }; + + reconnectAttempts: number; + + timeout: ReturnType; + /** - * @param {URL} url - Websocket URL. + * @param url - Websocket URL. */ - constructor(url) { + constructor(url: URL) { this.url = url.toString(); this.handlers = { @@ -126,13 +135,10 @@ class WSClient { /** * Registers a handler for events. * - * The handler is executed for all the events. It is up to the callback to - * filter the relevant events. - * - * @param {(object) => void} func - Handler function to register. - * @return {RemoveFn} + * The handler is executed for all events. It is up to the callback to + * filter the relevant ones for it. */ - onEvent(func) { + onEvent(func: EventHandlerFn): RemoveFn { this.handlers.events.push(func); return () => { const position = this.handlers.events.indexOf(func); @@ -144,11 +150,8 @@ class WSClient { * Registers a handler for close socket. * * The handler is executed when the socket is close. - * - * @param {(object) => void} func - Handler function to register. - * @return {RemoveFn} */ - onClose(func) { + onClose(func: BaseHandlerFn): RemoveFn { this.handlers.close.push(func); return () => { @@ -161,10 +164,8 @@ class WSClient { * Registers a handler for open socket. * * The handler is executed when the socket is open. - * @param {(object) => void} func - Handler function to register. - * @return {RemoveFn} */ - onOpen(func) { + onOpen(func: BaseHandlerFn): RemoveFn { this.handlers.open.push(func); return () => { @@ -177,11 +178,8 @@ class WSClient { * Registers a handler for socket errors. * * The handler is executed when an error is reported by the socket. - * - * @param {(object) => void} func - Handler function to register. - * @return {RemoveFn} */ - onError(func) { + onError(func: BaseHandlerFn): RemoveFn { this.handlers.error.push(func); return () => { @@ -195,9 +193,9 @@ class WSClient { * * Dispatchs an event by running all the handlers. * - * @param {object} event - Event object, which is basically a websocket message. + * @param event - Event object, which is basically a websocket message. */ - dispatchEvent(event) { + dispatchEvent(event: MessageEvent) { const eventObject = JSON.parse(event.data); this.handlers.events.forEach((f) => f(eventObject)); } diff --git a/web/src/components/core/ChangeProductLink.test.tsx b/web/src/components/core/ChangeProductLink.test.tsx index ddbdd369d1..da97510ccd 100644 --- a/web/src/components/core/ChangeProductLink.test.tsx +++ b/web/src/components/core/ChangeProductLink.test.tsx @@ -24,26 +24,31 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { PRODUCT as PATHS } from "~/routes/paths"; -import { Product } from "~/types/software"; +import { Product, RegistrationInfo } from "~/types/software"; import ChangeProductLink from "./ChangeProductLink"; +import { useRegistration } from "~/queries/software"; const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", icon: "tumbleweed.svg", description: "Tumbleweed description...", + registration: "no", }; const microos: Product = { id: "MicroOS", name: "openSUSE MicroOS", icon: "MicroOS.svg", description: "MicroOS description", + registration: "no", }; let mockUseProduct: { products: Product[]; selectedProduct?: Product }; +let registrationInfoMock: RegistrationInfo; jest.mock("~/queries/software", () => ({ useProduct: () => mockUseProduct, + useRegistration: (): ReturnType => registrationInfoMock, })); describe("ChangeProductLink", () => { @@ -57,6 +62,17 @@ describe("ChangeProductLink", () => { const link = screen.getByRole("link", { name: "Change product" }); expect(link).toHaveAttribute("href", PATHS.changeProduct); }); + + describe("but a product is registered", () => { + beforeEach(() => { + registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" }; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); }); describe("when there is only one product available", () => { diff --git a/web/src/components/core/ChangeProductLink.tsx b/web/src/components/core/ChangeProductLink.tsx index d5fd67b8fb..40f7b9d4cb 100644 --- a/web/src/components/core/ChangeProductLink.tsx +++ b/web/src/components/core/ChangeProductLink.tsx @@ -22,17 +22,20 @@ import React from "react"; import { Link, LinkProps } from "react-router-dom"; -import { useProduct } from "~/queries/software"; +import { useProduct, useRegistration } from "~/queries/software"; import { PRODUCT as PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; +import { isEmpty } from "~/utils"; /** * Link for navigating to the selection product. */ export default function ChangeProductLink({ children, ...props }: Omit) { const { products } = useProduct(); + const registration = useRegistration(); if (products.length <= 1) return null; + if (!isEmpty(registration?.key)) return null; return ( diff --git a/web/src/components/core/EmailInput.test.jsx b/web/src/components/core/EmailInput.test.tsx similarity index 74% rename from web/src/components/core/EmailInput.test.jsx rename to web/src/components/core/EmailInput.test.tsx index b4572709f3..1a050e9f15 100644 --- a/web/src/components/core/EmailInput.test.jsx +++ b/web/src/components/core/EmailInput.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2024] SUSE LLC * * All Rights Reserved. * @@ -23,9 +23,35 @@ import React, { useState } from "react"; import { screen } from "@testing-library/react"; -import EmailInput from "./EmailInput"; +import EmailInput, { EmailInputProps } from "./EmailInput"; import { plainRender } from "~/test-utils"; +/** + * Controlled component for testing the EmailInputProps + * + * Instead of testing if given callbacks are called, below tests are going to + * check the rendered result to be more aligned with the React Testing Library + * principles, https://testing-library.com/docs/guiding-principles/ + * + */ +const EmailInputTest = (props: EmailInputProps) => { + const [email, setEmail] = useState(""); + const [isValid, setIsValid] = useState(true); + + return ( + <> + setEmail(v)} + onValidate={setIsValid} + /> + {email &&

Email value updated!

} + {isValid === false &&

Email is not valid!

} + + ); +}; + describe("EmailInput component", () => { it("renders an email input", () => { plainRender( @@ -36,27 +62,6 @@ describe("EmailInput component", () => { expect(inputField).toHaveAttribute("type", "email"); }); - // Using a controlled component for testing the rendered result instead of testing if - // the given onChange callback is called. The former is more aligned with the - // React Testing Library principles, https://testing-library.com/docs/guiding-principles/ - const EmailInputTest = (props) => { - const [email, setEmail] = useState(""); - const [isValid, setIsValid] = useState(true); - - return ( - <> - setEmail(v)} - onValidate={setIsValid} - /> - {email &&

Email value updated!

} - {isValid === false &&

Email is not valid!

} - - ); - }; - it("triggers onChange callback", async () => { const { user } = plainRender(); const emailInput = screen.getByRole("textbox", { name: "Test email" }); diff --git a/web/src/components/core/EmailInput.jsx b/web/src/components/core/EmailInput.tsx similarity index 67% rename from web/src/components/core/EmailInput.jsx rename to web/src/components/core/EmailInput.tsx index 6bda2447b5..5d3bf09335 100644 --- a/web/src/components/core/EmailInput.jsx +++ b/web/src/components/core/EmailInput.tsx @@ -21,28 +21,25 @@ */ import React, { useEffect, useState } from "react"; -import { InputGroup, TextInput } from "@patternfly/react-core"; -import { noop } from "~/utils"; +import { InputGroup, TextInput, TextInputProps } from "@patternfly/react-core"; +import { isEmpty, noop } from "~/utils"; /** * Email validation. * * Code inspired by https://github.com/manishsaraan/email-validator/blob/master/index.js - * - * @param {string} email - * @returns {boolean} */ -const validateEmail = (email) => { +const validateEmail = (email: string) => { const regexp = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; - const validateFormat = (email) => { + const validateFormat = (email: string) => { const parts = email.split("@"); return parts.length === 2 && regexp.test(email); }; - const validateSizes = (email) => { + const validateSizes = (email: string) => { const [account, address] = email.split("@"); if (account.length > 64) return false; @@ -58,27 +55,29 @@ const validateEmail = (email) => { return validateFormat(email) && validateSizes(email); }; +export type EmailInputProps = TextInputProps & { onValidate?: (isValid: boolean) => void }; + /** * Renders an email input field which validates its value. * @component * - * @param {(boolean) => void} onValidate - Callback to be called every time the input value is + * @param onValidate - Callback to be called every time the input value is * validated. - * @param {Object} props - Props matching the {@link https://www.patternfly.org/components/forms/text-input PF/TextInput}, + * @param props - Props matching the {@link https://www.patternfly.org/components/forms/text-input PF/TextInput}, * except `type` and `validated` which are managed by the component. */ -export default function EmailInput({ onValidate = noop, ...props }) { +export default function EmailInput({ onValidate = noop, value, ...props }: EmailInputProps) { const [isValid, setIsValid] = useState(true); useEffect(() => { - const isValid = props.value.length === 0 || validateEmail(props.value); + const isValid = typeof value === "string" && (isEmpty(value) || validateEmail(value)); setIsValid(isValid); - onValidate(isValid); - }, [onValidate, props.value, setIsValid]); + typeof onValidate === "function" && onValidate(isValid); + }, [onValidate, value, setIsValid]); return ( - + ); } diff --git a/web/src/components/core/FormValidationError.test.jsx b/web/src/components/core/FormValidationError.test.jsx index 3ee1651071..e1b6b7e3bd 100644 --- a/web/src/components/core/FormValidationError.test.jsx +++ b/web/src/components/core/FormValidationError.test.jsx @@ -22,26 +22,30 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import { FormValidationError } from "~/components/core"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + it("renders nothing when message is null", () => { - const { container } = plainRender(); + const { container } = installerRender(); expect(container).toBeEmptyDOMElement(); }); it("renders nothing when message is empty", () => { - const { container } = plainRender(); + const { container } = installerRender(); expect(container).toBeEmptyDOMElement(); }); it("renders nothing when message is not defined", () => { - const { container } = plainRender(); + const { container } = installerRender(); expect(container).toBeEmptyDOMElement(); }); it("renders a PatternFly error with given message", () => { - plainRender(); + installerRender(); const node = screen.getByText("Invalid input"); expect(node.parentNode.classList.contains("pf-m-error")).toBe(true); }); diff --git a/web/src/components/core/InstallButton.tsx b/web/src/components/core/InstallButton.tsx index 5a6d44333d..93826fe188 100644 --- a/web/src/components/core/InstallButton.tsx +++ b/web/src/components/core/InstallButton.tsx @@ -26,7 +26,7 @@ import { Popup } from "~/components/core"; import { startInstallation } from "~/api/manager"; import { useAllIssues } from "~/queries/issues"; import { useLocation } from "react-router-dom"; -import { PRODUCT, ROOT, USER } from "~/routes/paths"; +import { SUPPORTIVE_PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; import { Icon } from "../layout"; @@ -38,14 +38,6 @@ import { Icon } from "../layout"; * defining the root authentication for the fisrt time, nor when the installer * is setting the chosen product. * */ -const EXCLUDED_FROM = [ - ROOT.login, - PRODUCT.changeProduct, - PRODUCT.progress, - ROOT.installationProgress, - ROOT.installationFinished, - USER.rootUser.edit, -]; const InstallConfirmationPopup = ({ onAccept, onClose }) => { return ( @@ -90,7 +82,7 @@ const InstallButton = ( const location = useLocation(); const hasIssues = !issues.isEmpty; - if (EXCLUDED_FROM.includes(location.pathname)) return; + if (SUPPORTIVE_PATHS.includes(location.pathname)) return; const { onClickWithIssues, ...buttonProps } = props; const open = async () => setIsOpen(true); diff --git a/web/src/components/core/IssuesDrawer.test.tsx b/web/src/components/core/IssuesDrawer.test.tsx index 4d4c9bd195..4dfb14245a 100644 --- a/web/src/components/core/IssuesDrawer.test.tsx +++ b/web/src/components/core/IssuesDrawer.test.tsx @@ -60,7 +60,14 @@ describe("IssuesDrawer", () => { describe("when there are installation issues", () => { beforeEach(() => { mockIssuesList = new IssuesList( - [], + [ + { + description: "Registration Fake Issue", + source: 0, + severity: 0, + details: "Registration Fake Issue details", + }, + ], [ { description: "Software Fake Issue", @@ -97,6 +104,7 @@ describe("IssuesDrawer", () => { it("renders the drawer with categorized issues linking to their scope", async () => { const { user } = installerRender(); + const registrationIssues = screen.getByRole("region", { name: "Registration" }); const softwareIssues = screen.getByRole("region", { name: "Software" }); const storageIssues = screen.getByRole("region", { name: "Storage" }); const usersIssues = screen.getByRole("region", { name: "Users" }); @@ -114,6 +122,14 @@ describe("IssuesDrawer", () => { expect(usersLink).toHaveAttribute("href", "/users"); within(usersIssues).getByText("Users Fake Issue"); + // Regression test: right now, registration issues comes under product + // scope. Check that it links to registration section anyway. + const registrationLink = within(registrationIssues).getByRole("link", { + name: "Registration", + }); + expect(registrationLink).toHaveAttribute("href", "/registration"); + within(registrationIssues).getByText("Registration Fake Issue"); + // onClose should be called when user clicks on a section too for ensuring // drawer gets closed even when navigation is not needed. await user.click(usersLink); diff --git a/web/src/components/core/IssuesDrawer.tsx b/web/src/components/core/IssuesDrawer.tsx index 38b205d2c7..8fcef222e9 100644 --- a/web/src/components/core/IssuesDrawer.tsx +++ b/web/src/components/core/IssuesDrawer.tsx @@ -49,6 +49,7 @@ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => { users: _("Users"), storage: _("Storage"), software: _("Software"), + product: _("Registration"), }; if (issues.isEmpty || phase === InstallationPhase.Install) return; @@ -65,13 +66,16 @@ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => {

{Object.entries(issuesByScope).map(([scope, issues], idx) => { if (issues.length === 0) return null; + // FIXME: address this better or use the /product(s)? namespace instead of + // /registration. + const section = scope === "product" ? "registration" : scope; const ariaLabelId = `${scope}-issues-section`; return (

- + {scopeHeaders[scope]}

diff --git a/web/src/components/core/IssuesHint.test.jsx b/web/src/components/core/IssuesHint.test.tsx similarity index 97% rename from web/src/components/core/IssuesHint.test.jsx rename to web/src/components/core/IssuesHint.test.tsx index a4a0d5a19e..6bead73a40 100644 --- a/web/src/components/core/IssuesHint.test.jsx +++ b/web/src/components/core/IssuesHint.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/core/IssuesHint.jsx b/web/src/components/core/IssuesHint.tsx similarity index 91% rename from web/src/components/core/IssuesHint.jsx rename to web/src/components/core/IssuesHint.tsx index 9a9c92a6d9..86ee00cad7 100644 --- a/web/src/components/core/IssuesHint.jsx +++ b/web/src/components/core/IssuesHint.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * @@ -23,6 +23,7 @@ import React from "react"; import { Hint, HintBody, List, ListItem, Stack } from "@patternfly/react-core"; import { _ } from "~/i18n"; +import { Issue } from "~/types/issues"; export default function IssuesHint({ issues }) { if (issues === undefined || issues.length === 0) return; @@ -35,7 +36,7 @@ export default function IssuesHint({ issues }) { {_("Before starting the installation, you need to address the following problems:")}

- {issues.map((i, idx) => ( + {issues.map((i: Issue, idx: number) => ( {i.description} ))} diff --git a/web/src/components/core/ListSearch.test.jsx b/web/src/components/core/ListSearch.test.tsx similarity index 98% rename from web/src/components/core/ListSearch.test.jsx rename to web/src/components/core/ListSearch.test.tsx index 1baf97cfea..d29e368dfb 100644 --- a/web/src/components/core/ListSearch.test.jsx +++ b/web/src/components/core/ListSearch.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2024] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/core/ListSearch.jsx b/web/src/components/core/ListSearch.tsx similarity index 76% rename from web/src/components/core/ListSearch.jsx rename to web/src/components/core/ListSearch.tsx index 1b3467f1f0..780feb574b 100644 --- a/web/src/components/core/ListSearch.jsx +++ b/web/src/components/core/ListSearch.tsx @@ -25,44 +25,47 @@ import { SearchInput } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { noop, useDebounce } from "~/utils"; -const search = (elements, term) => { +type ListSearchProps = { + /** Text to display as placeholder for the search input. */ + placeholder?: string; + /** List of elements in which to search. */ + elements: T[]; + /** Callback to be called with the filtered list of elements. */ + onChange: (elements: T[]) => void; +}; + +function search(elements: T[], term: string): T[] { const value = term.toLowerCase(); - const match = (element) => { + const match = (element: T) => { return Object.values(element).join("").toLowerCase().includes(value); }; return elements.filter(match); -}; +} /** - * TODO: Rename and/or refactor? * Input field for searching in a given list of elements. * @component - * - * @param {object} props - * @param {string} [props.placeholder] - * @param {object[]} [props.elements] - List of elements in which to search. - * @param {(elements: object[]) => void} [props.onChange] - Callback to be called with the filtered list of elements. */ -export default function ListSearch({ +export default function ListSearch({ placeholder = _("Search"), elements = [], onChange: onChangeProp = noop, -}) { +}: ListSearchProps) { const [value, setValue] = useState(""); const [resultSize, setResultSize] = useState(elements.length); - const updateResult = (result) => { + const updateResult = (result: T[]) => { setResultSize(result.length); onChangeProp(result); }; - const searchHandler = useDebounce((term) => { + const searchHandler = useDebounce((term: string) => { updateResult(search(elements, term)); }, 500); - const onChange = (value) => { + const onChange = (value: string) => { setValue(value); searchHandler(value); }; diff --git a/web/src/components/core/LoginPage.test.tsx b/web/src/components/core/LoginPage.test.tsx index 91ea58652a..8256357ee8 100644 --- a/web/src/components/core/LoginPage.test.tsx +++ b/web/src/components/core/LoginPage.test.tsx @@ -22,7 +22,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { installerRender, mockRoutes, plainRender } from "~/test-utils"; +import { installerRender, mockRoutes } from "~/test-utils"; import { LoginPage } from "~/components/core"; import { AuthErrors } from "~/context/auth"; import { PlainLayout } from "../layout"; @@ -36,6 +36,10 @@ const mockLoginFn = jest.fn(); const phase: InstallationPhase = InstallationPhase.Startup; const isBusy: boolean = false; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/status", () => ({ useInstallerStatus: () => ({ phase, @@ -86,12 +90,12 @@ describe("LoginPage", () => { describe("when user is not authenticated", () => { it("renders reference to root", () => { - plainRender(); + installerRender(); screen.getAllByText(/root/); }); it("allows entering a password", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const form = screen.getByRole("form", { name: "Login form" }); const passwordInput = within(form).getByLabelText("Password input"); const loginButton = within(form).getByRole("button", { name: "Log in" }); @@ -109,7 +113,7 @@ describe("LoginPage", () => { }); it("renders an authentication error", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const form = screen.getByRole("form", { name: "Login form" }); const passwordInput = within(form).getByLabelText("Password input"); const loginButton = within(form).getByRole("button", { name: "Log in" }); @@ -130,7 +134,7 @@ describe("LoginPage", () => { }); it("renders a server error text", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const form = screen.getByRole("form", { name: "Login form" }); const passwordInput = within(form).getByLabelText("Password input"); const loginButton = within(form).getByRole("button", { name: "Log in" }); diff --git a/web/src/components/core/NumericTextInput.test.jsx b/web/src/components/core/NumericTextInput.test.tsx similarity index 95% rename from web/src/components/core/NumericTextInput.test.jsx rename to web/src/components/core/NumericTextInput.test.tsx index 2078a24e5c..760aac843e 100644 --- a/web/src/components/core/NumericTextInput.test.jsx +++ b/web/src/components/core/NumericTextInput.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2023-2024] SUSE LLC * * All Rights Reserved. * @@ -29,7 +29,7 @@ import { NumericTextInput } from "~/components/core"; // the given onChange callback is called. The former is more aligned with the // React Testing Library principles, https://testing-library.com/docs/guiding-principles const Input = ({ value: initialValue = "" }) => { - const [value, setValue] = useState(initialValue); + const [value, setValue] = useState(initialValue); return ; }; diff --git a/web/src/components/core/NumericTextInput.jsx b/web/src/components/core/NumericTextInput.tsx similarity index 66% rename from web/src/components/core/NumericTextInput.jsx rename to web/src/components/core/NumericTextInput.tsx index 1ecd2aa984..50302af7b2 100644 --- a/web/src/components/core/NumericTextInput.jsx +++ b/web/src/components/core/NumericTextInput.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2024] SUSE LLC * * All Rights Reserved. * @@ -20,19 +20,14 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; -import { TextInput } from "@patternfly/react-core"; +import { TextInput, TextInputProps } from "@patternfly/react-core"; import { noop } from "~/utils"; -/** - * Callback function for notifying a valid input change - * - * @callback onChangeFn - * @param {string|number} the input value - * @return {void} - */ +type NumericTextInputProps = { + value: string | number; + onChange: (value: string | number) => void; +} & Omit; /** * Helper component for having an input text limited to not signed numbers @@ -41,17 +36,16 @@ import { noop } from "~/utils"; * Based on {@link https://www.patternfly.org/components/forms/text-input PF/TextInput} * * @note It allows empty value too. - * - * @param {object} props - * @param {string|number} props.value - the input value - * @param {onChangeFn} props.onChange - the callback to be called when the entered value match the input pattern - * @param {import("@patternfly/react-core").TextInputProps} props.textInputProps */ -export default function NumericTextInput({ value = "", onChange = noop, ...textInputProps }) { +export default function NumericTextInput({ + value = "", + onChange = noop, + ...textInputProps +}: NumericTextInputProps) { // NOTE: Using \d* instead of \d+ at the beginning to allow empty const pattern = /^\d*\.?\d*$/; - const handleOnChange = (_, value) => { + const handleOnChange: TextInputProps["onChange"] = (_, value) => { if (pattern.test(value)) { onChange(value); } diff --git a/web/src/components/core/Page.test.tsx b/web/src/components/core/Page.test.tsx index be2515d2a3..5c90132aa1 100644 --- a/web/src/components/core/Page.test.tsx +++ b/web/src/components/core/Page.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023-2024] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * @@ -22,12 +22,17 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { plainRender, mockNavigateFn } from "~/test-utils"; +import { plainRender, mockNavigateFn, mockRoutes, installerRender } from "~/test-utils"; import { Page } from "~/components/core"; import { _ } from "~/i18n"; +import { PRODUCT, ROOT, USER } from "~/routes/paths"; let consoleErrorSpy: jest.SpyInstance; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlertMock
+)); + describe("Page", () => { beforeAll(() => { consoleErrorSpy = jest.spyOn(console, "error"); @@ -41,7 +46,7 @@ describe("Page", () => { it("renders given children", () => { plainRender( -

{_("The Page Component")}

+

The Page Component

, ); screen.getByRole("heading", { name: "The Page Component" }); @@ -93,10 +98,33 @@ describe("Page", () => { describe("Page.Content", () => { it("renders a node that fills all the available space", () => { - plainRender({_("The Content")}); + installerRender({_("The Content")}); const content = screen.getByText("The Content"); expect(content.classList.contains("pf-m-fill")).toBe(true); }); + + it("mounts a ProductRegistrationAlert", () => { + installerRender(); + screen.getByText("ProductRegistrationAlertMock"); + }); + + describe.each([ + ["login", ROOT.login], + ["product selection", PRODUCT.changeProduct], + ["product selection progress", PRODUCT.progress], + ["installation progress", ROOT.installationProgress], + ["installation finished", ROOT.installationFinished], + ["root authentication", USER.rootUser.edit], + ])(`but at %s path`, (_, path) => { + beforeEach(() => { + mockRoutes(path); + }); + + it("does not mount ProductRegistrationAlert", () => { + installerRender(); + expect(screen.queryByText("ProductRegistrationAlertMock")).toBeNull(); + }); + }); }); describe("Page.Cancel", () => { @@ -149,7 +177,7 @@ describe("Page", () => { }); describe("Page.Header", () => { it("renders a node that sticks to top", () => { - plainRender({_("The Header")}); + plainRender(The Header); const content = screen.getByText("The Header"); const container = content.parentNode as HTMLElement; expect(container.classList.contains("pf-m-sticky-top")).toBe(true); @@ -158,19 +186,19 @@ describe("Page", () => { describe("Page.Section", () => { it("outputs to console.error if both are missing, title and aria-label", () => { - plainRender({_("Content")}); + plainRender(Content); expect(console.error).toHaveBeenCalledWith(expect.stringContaining("must have either")); }); it("renders a section node", () => { - plainRender({_("The Content")}); + plainRender(The Content); const section = screen.getByRole("region"); within(section).getByText("The Content"); }); it("adds the aria-labelledby attribute when title is given but aria-label is not", () => { const { rerender } = plainRender( - {_("The Content")}, + The Content, ); const section = screen.getByRole("region"); expect(section).toHaveAttribute("aria-labelledby"); @@ -178,7 +206,7 @@ describe("Page", () => { // aria-label is given through Page.Section props rerender( - {_("The Content")} + The Content , ); expect(section).not.toHaveAttribute("aria-labelledby"); @@ -186,25 +214,25 @@ describe("Page", () => { // aria-label is given through pfCardProps rerender( - {_("The Content")} + The Content , ); expect(section).not.toHaveAttribute("aria-labelledby"); // None was given, title nor aria-label - rerender({_("The Content")}); + rerender(The Content); expect(section).not.toHaveAttribute("aria-labelledby"); }); it("renders given content props (title, value, description, actions, and children (content)", () => { plainRender( {_("Disable")}} + title="A section" + value="Enabled" + description="Testing section with title, value, description, content, and actions" + actions={Disable} > - {_("The Content")} + The Content , ); const section = screen.getByRole("region"); diff --git a/web/src/components/core/Page.tsx b/web/src/components/core/Page.tsx index 799f2413eb..f72d6ba171 100644 --- a/web/src/components/core/Page.tsx +++ b/web/src/components/core/Page.tsx @@ -40,11 +40,13 @@ import { TitleProps, } from "@patternfly/react-core"; import { Flex } from "~/components/layout"; +import { ProductRegistrationAlert } from "~/components/product"; import { _ } from "~/i18n"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import flexStyles from "@patternfly/react-styles/css/utilities/Flex/flex"; -import { To, useNavigate } from "react-router-dom"; +import { To, useLocation, useNavigate } from "react-router-dom"; import { isEmpty, isObject } from "~/utils"; +import { SUPPORTIVE_PATHS } from "~/routes/paths"; /** * Props accepted by Page.Section @@ -278,11 +280,19 @@ const Submit = ({ children, ...props }: SubmitActionProps) => { * * @see [Patternfly Page/PageSection](https://www.patternfly.org/components/page#pagesection) */ -const Content = ({ children, ...pageSectionProps }: React.PropsWithChildren) => ( - - {children} - -); +const Content = ({ children, ...pageSectionProps }: React.PropsWithChildren) => { + const location = useLocation(); + const mountRegistrationAlert = !SUPPORTIVE_PATHS.includes(location.pathname); + + return ( + <> + {mountRegistrationAlert && } + + {children} + + + ); +}; /** * Component for structuring an Agama page, built on top of PF/Page/PageGroup. diff --git a/web/src/components/core/PasswordInput.test.tsx b/web/src/components/core/PasswordInput.test.tsx index 8437498107..ad4f5aca27 100644 --- a/web/src/components/core/PasswordInput.test.tsx +++ b/web/src/components/core/PasswordInput.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023-2024] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * @@ -25,18 +25,17 @@ import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import userEvent from "@testing-library/user-event"; import PasswordInput, { PasswordInputProps } from "./PasswordInput"; -import { _ } from "~/i18n"; describe("PasswordInput Component", () => { it("renders a password input", () => { - plainRender(); + plainRender(); const inputField = screen.getByLabelText("User password"); expect(inputField).toHaveAttribute("type", "password"); }); it("allows revealing the password", async () => { - plainRender(); + plainRender(); const passwordInput = screen.getByLabelText("User password"); const button = screen.getByRole("button"); @@ -48,7 +47,7 @@ describe("PasswordInput Component", () => { it("applies autoFocus behavior correctly", () => { plainRender( - , + , ); const inputField = screen.getByLabelText("User password"); diff --git a/web/src/components/core/Popup.tsx b/web/src/components/core/Popup.tsx index 9135bfcb6c..ec2ba7f6a8 100644 --- a/web/src/components/core/Popup.tsx +++ b/web/src/components/core/Popup.tsx @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import React from "react"; +import React, { isValidElement } from "react"; import { Button, ButtonProps, Modal, ModalProps } from "@patternfly/react-core"; import { Loading } from "~/components/layout"; import { _ } from "~/i18n"; @@ -202,9 +202,8 @@ const Popup = ({ children, ...props }: PopupProps) => { - const [actions, content] = partition( - React.Children.toArray(children), - (child) => child.type === Actions, + const [actions, content] = partition(React.Children.toArray(children), (child) => + isValidElement(child) ? child.type === Actions : false, ); return ( diff --git a/web/src/components/core/ProgressText.test.jsx b/web/src/components/core/ProgressText.test.tsx similarity index 97% rename from web/src/components/core/ProgressText.test.jsx rename to web/src/components/core/ProgressText.test.tsx index 0c7e49034b..8fc5f21206 100644 --- a/web/src/components/core/ProgressText.test.jsx +++ b/web/src/components/core/ProgressText.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/core/ProgressText.jsx b/web/src/components/core/ProgressText.tsx similarity index 82% rename from web/src/components/core/ProgressText.jsx rename to web/src/components/core/ProgressText.tsx index 5ac7dcac57..8a31346e2a 100644 --- a/web/src/components/core/ProgressText.jsx +++ b/web/src/components/core/ProgressText.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * @@ -20,22 +20,22 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; import { Split, Text } from "@patternfly/react-core"; +type ProgressTextProps = { + /** Progress message. */ + message: string; + /** Current step. */ + current: number; + /** Total steps. */ + total: number; +}; + /** * Progress description - * - * @component - * - * @param {object} props - * @param {string} [props.message] Progress message - * @param {number} [props.current] Current step - * @param {number} [props.total] Number of steps */ -export default function ProgressText({ message, current, total }) { +export default function ProgressText({ message, current, total }: ProgressTextProps) { const text = current === 0 ? message : `${message} (${current}/${total})`; return ( diff --git a/web/src/components/core/RowActions.test.tsx b/web/src/components/core/RowActions.test.tsx new file mode 100644 index 0000000000..9f5fc525da --- /dev/null +++ b/web/src/components/core/RowActions.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { IAction } from "@patternfly/react-table"; +import { screen } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { RowActions } from "~/components/core"; +import { Icon } from "../layout"; + +const mockEditFn = jest.fn(); +const mockDeleteFn = jest.fn(); + +const actions: IAction[] = [ + { + title: "Edit", + role: "link", + "aria-label": "Dummy edit action", + onClick: mockEditFn, + }, + { + title: "Delete", + "aria-label": "Dummy delete action", + icon: , + onClick: mockDeleteFn, + isDanger: true, + }, +]; + +describe("RowActions", () => { + it("allows interacting with given actions from a dropdown menu", async () => { + const { user } = plainRender( + , + ); + + const button = screen.getByRole("button", { name: "Actions for testing" }); + await user.click(button); + screen.getByRole("menu"); + const editAction = screen.getByRole("menuitem", { name: "Dummy edit action" }); + await user.click(editAction); + expect(mockEditFn).toHaveBeenCalled(); + await user.click(button); + const deleteAction = screen.getByRole("menuitem", { name: "Dummy delete action" }); + await user.click(deleteAction); + expect(mockDeleteFn).toHaveBeenCalled(); + }); +}); diff --git a/web/src/components/core/RowActions.jsx b/web/src/components/core/RowActions.tsx similarity index 77% rename from web/src/components/core/RowActions.jsx rename to web/src/components/core/RowActions.tsx index 8a6321bdc4..27f2be1cd7 100644 --- a/web/src/components/core/RowActions.jsx +++ b/web/src/components/core/RowActions.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2024] SUSE LLC * * All Rights Reserved. * @@ -22,13 +22,21 @@ import React from "react"; import { MenuToggle } from "@patternfly/react-core"; -import { ActionsColumn } from "@patternfly/react-table"; - +import { + ActionsColumn, + ActionsColumnProps, + CustomActionsToggleProps, +} from "@patternfly/react-table"; import { Icon } from "~/components/layout"; import { _ } from "~/i18n"; +type RowActionsProps = { + id: string; + actions: ActionsColumnProps["items"]; +} & Omit; + /** - * Renders icon for selecting the options of a row in a table + * Renders available options for a row in a table * @component * * @example @@ -46,16 +54,14 @@ import { _ } from "~/i18n"; * } * ]} * /> - * - * @param {object} props - * @param {string} props.id - * @param {Action[]} props.actions - * @param {object} [props.rest] - * - * @typedef {import("@patternfly/react-table").IAction} Action */ -export default function RowActions({ id, actions, "aria-label": toggleAriaLabel, ...rest }) { - const actionsToggle = (props) => ( +export default function RowActions({ + id, + actions, + "aria-label": toggleAriaLabel, + ...rest +}: RowActionsProps) { + const actionsToggle = (props: CustomActionsToggleProps) => ( () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/components/layout/Header", () => () =>
Header Mock
); jest.mock("~/components/layout/Sidebar", () => () =>
Sidebar Mock
); jest.mock("~/components/layout/Layout", () => { diff --git a/web/src/components/core/index.js b/web/src/components/core/index.ts similarity index 97% rename from web/src/components/core/index.js rename to web/src/components/core/index.ts index 0345080fd4..dd18a31925 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.ts @@ -32,7 +32,6 @@ export { default as InstallationFinished } from "./InstallationFinished"; export { default as InstallationProgress } from "./InstallationProgress"; export { default as InstallButton } from "./InstallButton"; export { default as IssuesHint } from "./IssuesHint"; -export { default as SectionSkeleton } from "./SectionSkeleton"; export { default as ListSearch } from "./ListSearch"; export { default as LoginPage } from "./LoginPage"; export { default as RowActions } from "./RowActions"; diff --git a/web/src/components/l10n/KeyboardSelection.test.tsx b/web/src/components/l10n/KeyboardSelection.test.tsx index 10c85d6f8a..c22c3c5894 100644 --- a/web/src/components/l10n/KeyboardSelection.test.tsx +++ b/web/src/components/l10n/KeyboardSelection.test.tsx @@ -24,7 +24,7 @@ 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"; +import { mockNavigateFn, installerRender } from "~/test-utils"; const keymaps = [ { id: "us", name: "English" }, @@ -35,6 +35,10 @@ const mockConfigMutation = { mutate: jest.fn(), }; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), useConfigMutation: () => mockConfigMutation, @@ -47,7 +51,7 @@ jest.mock("react-router-dom", () => ({ })); it("allows changing the keyboard", async () => { - plainRender(); + installerRender(); const option = await screen.findByText("Spanish"); await userEvent.click(option); diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.tsx similarity index 89% rename from web/src/components/l10n/L10nPage.test.jsx rename to web/src/components/l10n/L10nPage.test.tsx index 7229c51903..d9bbb380be 100644 --- a/web/src/components/l10n/L10nPage.test.jsx +++ b/web/src/components/l10n/L10nPage.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2024] SUSE LLC + * Copyright (c) [2022-2025] SUSE LLC * * All Rights Reserved. * @@ -22,9 +22,13 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import L10nPage from "~/components/l10n/L10nPage"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + let mockLoadedData; const locales = [ @@ -58,7 +62,7 @@ beforeEach(() => { }); it("renders a section for configuring the language", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Language" }); within(region).getByText("English - United States"); within(region).getByText("Change"); @@ -70,7 +74,7 @@ describe("if there is no selected language", () => { }); it("renders a button for selecting a language", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Language" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); @@ -78,7 +82,7 @@ describe("if there is no selected language", () => { }); it("renders a section for configuring the keyboard", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Keyboard" }); within(region).getByText("English"); within(region).getByText("Change"); @@ -90,7 +94,7 @@ describe("if there is no selected keyboard", () => { }); it("renders a button for selecting a keyboard", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Keyboard" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); @@ -98,7 +102,7 @@ describe("if there is no selected keyboard", () => { }); it("renders a section for configuring the time zone", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Time zone" }); within(region).getByText("Europe - Berlin"); within(region).getByText("Change"); @@ -110,7 +114,7 @@ describe("if there is no selected time zone", () => { }); it("renders a button for selecting a time zone", () => { - plainRender(); + installerRender(); const region = screen.getByRole("region", { name: "Time zone" }); within(region).getByText("Not selected yet"); within(region).getByText("Select"); diff --git a/web/src/components/l10n/L10nPage.jsx b/web/src/components/l10n/L10nPage.tsx similarity index 98% rename from web/src/components/l10n/L10nPage.jsx rename to web/src/components/l10n/L10nPage.tsx index 5ead5d01f6..4e0121f788 100644 --- a/web/src/components/l10n/L10nPage.jsx +++ b/web/src/components/l10n/L10nPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2025] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/l10n/LocaleSelection.test.tsx b/web/src/components/l10n/LocaleSelection.test.tsx index d00cb87769..b2d4a98aa5 100644 --- a/web/src/components/l10n/LocaleSelection.test.tsx +++ b/web/src/components/l10n/LocaleSelection.test.tsx @@ -24,7 +24,7 @@ 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"; +import { mockNavigateFn, installerRender } from "~/test-utils"; const locales = [ { id: "en_US.UTF-8", name: "English", territory: "United States" }, @@ -35,6 +35,10 @@ const mockConfigMutation = { mutate: jest.fn(), }; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/l10n", () => ({ ...jest.requireActual("~/queries/l10n"), useL10n: () => ({ locales, selectedLocale: locales[0] }), @@ -47,7 +51,7 @@ jest.mock("react-router-dom", () => ({ })); it("allows changing the keyboard", async () => { - plainRender(); + installerRender(); const option = await screen.findByText("Spanish"); await userEvent.click(option); diff --git a/web/src/components/l10n/TimezoneSelection.test.tsx b/web/src/components/l10n/TimezoneSelection.test.tsx index 201595f4e7..1077a34cef 100644 --- a/web/src/components/l10n/TimezoneSelection.test.tsx +++ b/web/src/components/l10n/TimezoneSelection.test.tsx @@ -24,7 +24,11 @@ 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"; +import { mockNavigateFn, installerRender } from "~/test-utils"; + +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); const timezones = [ { id: "Europe/Berlin", parts: ["Europe", "Berlin"], country: "Germany", utcOffset: 120 }, @@ -70,7 +74,7 @@ afterEach(() => { }); it("allows changing the timezone", async () => { - plainRender(); + installerRender(); const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); const option = await screen.findByText("Europe-Madrid"); @@ -82,7 +86,7 @@ it("allows changing the timezone", async () => { }); it("displays the UTC offset", () => { - plainRender(); + installerRender(); expect(screen.getByText("Australia/Adelaide UTC+9:30")).toBeInTheDocument(); expect(screen.getByText("Europe/Madrid UTC+2")).toBeInTheDocument(); diff --git a/web/src/components/l10n/TimezoneSelection.tsx b/web/src/components/l10n/TimezoneSelection.tsx index 0ea385a2ba..614ba8dc5a 100644 --- a/web/src/components/l10n/TimezoneSelection.tsx +++ b/web/src/components/l10n/TimezoneSelection.tsx @@ -98,7 +98,7 @@ export default function TimezoneSelection() { } description={ - {timezoneTime(id, { date }) || ""} + {timezoneTime(id, date) || ""}
{details}
diff --git a/web/src/components/l10n/index.js b/web/src/components/l10n/index.ts similarity index 100% rename from web/src/components/l10n/index.js rename to web/src/components/l10n/index.ts diff --git a/web/src/components/layout/Center.jsx b/web/src/components/layout/Center.tsx similarity index 91% rename from web/src/components/layout/Center.jsx rename to web/src/components/layout/Center.tsx index 2f3046ec3a..48c4a53512 100644 --- a/web/src/components/layout/Center.jsx +++ b/web/src/components/layout/Center.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -20,8 +20,6 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; /** @@ -44,11 +42,8 @@ import React from "react"; * To know more, read * - https://www.w3.org/TR/selectors-4/#relational * - https://ishadeed.com/article/css-has-parent-selector/ - * - * @param {object} props - * @param {React.ReactNode} props.children */ -const Center = ({ children }) => ( +const Center = ({ children }: React.PropsWithChildren) => (
{children}
diff --git a/web/src/components/layout/Header.test.tsx b/web/src/components/layout/Header.test.tsx index 1ae19803fc..bd34ec37b8 100644 --- a/web/src/components/layout/Header.test.tsx +++ b/web/src/components/layout/Header.test.tsx @@ -25,17 +25,20 @@ import { screen, within } from "@testing-library/react"; import { installerRender, mockRoutes } from "~/test-utils"; import Header from "./Header"; import { InstallationPhase } from "~/types/status"; +import { Product } from "~/types/software"; -const tumbleweed = { +const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", description: "Tumbleweed description...", + registration: "no", }; -const microos = { +const microos: Product = { id: "MicroOS", name: "openSUSE MicroOS", description: "MicroOS description", + registration: "no", }; let phase: InstallationPhase; diff --git a/web/src/components/layout/Icon.tsx b/web/src/components/layout/Icon.tsx index 9c3d46a969..2a0b5992ae 100644 --- a/web/src/components/layout/Icon.tsx +++ b/web/src/components/layout/Icon.tsx @@ -26,6 +26,7 @@ import React from "react"; // icons location. Check the tsconfig.json file to see its value. import AddAPhoto from "@icons/add_a_photo.svg?component"; import Apps from "@icons/apps.svg?component"; +import AppRegistration from "@icons/app_registration.svg?component"; import Badge from "@icons/badge.svg?component"; import Backspace from "@icons/backspace.svg?component"; import CheckCircle from "@icons/check_circle.svg?component"; @@ -92,6 +93,7 @@ import { SiLinux } from "@icons-pack/react-simple-icons"; const icons = { add_a_photo: AddAPhoto, apps: Apps, + app_registration: AppRegistration, badge: Badge, backspace: Backspace, check_circle: CheckCircle, diff --git a/web/src/components/layout/Sidebar.test.tsx b/web/src/components/layout/Sidebar.test.tsx index fa3d3c7495..fa094548cc 100644 --- a/web/src/components/layout/Sidebar.test.tsx +++ b/web/src/components/layout/Sidebar.test.tsx @@ -24,25 +24,77 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import Sidebar from "./Sidebar"; +import { Product } from "~/types/software"; +import { useProduct } from "~/queries/software"; jest.mock("~/components/core/ChangeProductLink", () => () =>
ChangeProductLink Mock
); +const tw: Product = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + registration: "no", +}; + +const sle: Product = { + id: "sle", + name: "SLE", + registration: "mandatory", +}; + +let selectedProduct: Product; + +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useProduct: (): ReturnType => { + return { + products: [tw, sle], + selectedProduct, + }; + }, +})); + jest.mock("~/router", () => ({ rootRoutes: () => [ { path: "/", handle: { name: "Main" } }, { path: "/l10n", handle: { name: "L10n" } }, { path: "/hidden" }, + { + path: "/registration", + handle: { name: "Registration", needsRegistrableProduct: true }, + }, ], })); 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" }); + describe("when product is registrable", () => { + beforeEach(() => { + selectedProduct = sle; + }); + + it("renders a navigation including all root routes with handle object", () => { + installerRender(); + const mainNavigation = screen.getByRole("navigation"); + const mainNavigationLinks = within(mainNavigation).getAllByRole("link"); + expect(mainNavigationLinks.length).toBe(3); + screen.getByRole("link", { name: "Main" }); + screen.getByRole("link", { name: "L10n" }); + screen.getByRole("link", { name: "Registration" }); + }); + }); + + describe("when product is not registrable", () => { + beforeEach(() => { + selectedProduct = tw; + }); + + it("renders a navigation including all root routes with handle object, except ones set as needsRegistrableProduct", () => { + 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/ChangeProductLink component", () => { diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index aa768d0218..5d446130d9 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -27,10 +27,14 @@ import { Icon } from "~/components/layout"; import { ChangeProductLink } from "~/components/core"; import { rootRoutes } from "~/router"; import { _ } from "~/i18n"; +import { useProduct } from "~/queries/software"; const MainNavigation = (): React.ReactNode => { + const { selectedProduct: product } = useProduct(); + const links = rootRoutes().map((r) => { if (!r.handle) return null; + if (r.handle.needsRegistrableProduct && product.registration === "no") return null; // eslint-disable-next-line agama-i18n/string-literals const name = _(r.handle.name); diff --git a/web/src/components/layout/index.js b/web/src/components/layout/index.ts similarity index 100% rename from web/src/components/layout/index.js rename to web/src/components/layout/index.ts diff --git a/web/src/components/network/NetworkPage.test.tsx b/web/src/components/network/NetworkPage.test.tsx index e6f0bfae52..841bb245cb 100644 --- a/web/src/components/network/NetworkPage.test.tsx +++ b/web/src/components/network/NetworkPage.test.tsx @@ -26,6 +26,10 @@ import { installerRender } from "~/test-utils"; import NetworkPage from "~/components/network/NetworkPage"; import { Connection, ConnectionMethod, ConnectionStatus, ConnectionType } from "~/types/network"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + const wiredConnection = new Connection("eth0", { iface: "eth0", method4: ConnectionMethod.MANUAL, diff --git a/web/src/components/network/NetworkPage.tsx b/web/src/components/network/NetworkPage.tsx index f28015222a..83c3a16166 100644 --- a/web/src/components/network/NetworkPage.tsx +++ b/web/src/components/network/NetworkPage.tsx @@ -98,7 +98,7 @@ const NoWifiAvailable = () => ( export default function NetworkPage() { useNetworkConfigChanges(); const { connections, devices, settings } = useNetwork(); - const [wifiConnections, wiredConnections] = partition(connections, (c) => c.wireless); + const [wifiConnections, wiredConnections] = partition(connections, (c) => !!c.wireless); return ( diff --git a/web/src/components/network/index.js b/web/src/components/network/index.ts similarity index 100% rename from web/src/components/network/index.js rename to web/src/components/network/index.ts diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.tsx similarity index 97% rename from web/src/components/overview/L10nSection.test.jsx rename to web/src/components/overview/L10nSection.test.tsx index c89155fd5a..efb9a41ff0 100644 --- a/web/src/components/overview/L10nSection.test.jsx +++ b/web/src/components/overview/L10nSection.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/overview/L10nSection.jsx b/web/src/components/overview/L10nSection.tsx similarity index 97% rename from web/src/components/overview/L10nSection.jsx rename to web/src/components/overview/L10nSection.tsx index a82d72cb8d..37308e410d 100644 --- a/web/src/components/overview/L10nSection.jsx +++ b/web/src/components/overview/L10nSection.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/overview/OverviewPage.test.tsx b/web/src/components/overview/OverviewPage.test.tsx index 2cef9ae6b6..d37d118670 100644 --- a/web/src/components/overview/OverviewPage.test.tsx +++ b/web/src/components/overview/OverviewPage.test.tsx @@ -22,16 +22,19 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import { OverviewPage } from "~/components/overview"; jest.mock("~/components/overview/L10nSection", () => () =>
Localization Section
); jest.mock("~/components/overview/StorageSection", () => () =>
Storage Section
); jest.mock("~/components/overview/SoftwareSection", () => () =>
Software Section
); +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert
+)); describe("when a product is selected", () => { it("renders the overview page content", async () => { - plainRender(); + installerRender(); await screen.findByText("Localization Section"); await screen.findByText("Storage Section"); await screen.findByText("Software Section"); diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx index ecbe0c5951..17f0efadc9 100644 --- a/web/src/components/overview/OverviewPage.tsx +++ b/web/src/components/overview/OverviewPage.tsx @@ -21,44 +21,35 @@ */ import React from "react"; -import { Grid, GridItem, Hint, HintBody, Stack } from "@patternfly/react-core"; +import { Grid, GridItem, Stack } from "@patternfly/react-core"; import { Page } from "~/components/core"; import L10nSection from "./L10nSection"; import StorageSection from "./StorageSection"; import SoftwareSection from "./SoftwareSection"; import { _ } from "~/i18n"; -const OverviewSection = () => ( - - - - - - - -); - export default function OverviewPage() { return ( + +

{_("Overview")}

+
+ - - - {_( - "Take your time to check your configuration before starting the installation process.", - )} - - - - - + + + + + + + diff --git a/web/src/components/overview/StorageSection.tsx b/web/src/components/overview/StorageSection.tsx index 7c02aae9ca..d070a07093 100644 --- a/web/src/components/overview/StorageSection.tsx +++ b/web/src/components/overview/StorageSection.tsx @@ -45,7 +45,6 @@ const Content = ({ children }) => ( export default function StorageSection() { const configModel = useConfigModel(); const devices = useDevices("system", { suspense: true }); - const drives = configModel?.drives || []; const label = (drive) => { diff --git a/web/src/components/overview/index.js b/web/src/components/overview/index.ts similarity index 100% rename from web/src/components/overview/index.js rename to web/src/components/overview/index.ts diff --git a/web/src/components/product/ProductRegistrationAlert.test.tsx b/web/src/components/product/ProductRegistrationAlert.test.tsx new file mode 100644 index 0000000000..856e47737a --- /dev/null +++ b/web/src/components/product/ProductRegistrationAlert.test.tsx @@ -0,0 +1,134 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender, mockRoutes } from "~/test-utils"; +import ProductRegistrationAlert from "./ProductRegistrationAlert"; +import { Product, RegistrationInfo } from "~/types/software"; +import { useProduct, useRegistration } from "~/queries/software"; +import { PRODUCT, REGISTRATION, ROOT, USER } from "~/routes/paths"; + +jest.mock("~/components/core/ChangeProductLink", () => () =>
ChangeProductLink Mock
); + +const tw: Product = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + registration: "no", +}; + +const sle: Product = { + id: "sle", + name: "SLE", + registration: "mandatory", +}; + +let selectedProduct: Product; +let registrationInfoMock: RegistrationInfo; + +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useRegistration: (): ReturnType => registrationInfoMock, + useProduct: (): ReturnType => { + return { + products: [tw, sle], + selectedProduct, + }; + }, +})); + +const rendersNothingInSomePaths = () => { + describe.each([ + ["login", ROOT.login], + ["product selection", PRODUCT.changeProduct], + ["product selection progress", PRODUCT.progress], + ["installation progress", ROOT.installationProgress], + ["installation finished", ROOT.installationFinished], + ["root authentication", USER.rootUser.edit], + ])(`but at %s path`, (_, path) => { + beforeEach(() => { + mockRoutes(path); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); +}; + +describe("ProductRegistrationAlert", () => { + describe("when product is registrable and registration code is not set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "", email: "" }; + }); + + rendersNothingInSomePaths(); + + it("renders an alert warning about registration required", () => { + installerRender(); + screen.getByRole("heading", { + name: /Warning alert:.*must be registered/, + }); + const link = screen.getByRole("link", { name: "Register it now" }); + expect(link).toHaveAttribute("href", REGISTRATION.root); + }); + + describe("but at registration path already", () => { + beforeEach(() => { + mockRoutes(REGISTRATION.root); + }); + + it("does not render the link to registration", () => { + installerRender(); + screen.getByRole("heading", { + name: /Warning alert:.*must be registered/, + }); + expect(screen.queryAllByRole("link")).toEqual([]); + }); + }); + }); + + describe("when product is registrable and registration code is already set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" }; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("when product is not registrable", () => { + beforeEach(() => { + selectedProduct = tw; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx new file mode 100644 index 0000000000..4693a969db --- /dev/null +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -0,0 +1,60 @@ +/* + * Copyright (c) [2023-2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { Alert } from "@patternfly/react-core"; +import { useLocation } from "react-router-dom"; +import { Link } from "~/components/core"; +import { useProduct, useRegistration } from "~/queries/software"; +import { REGISTRATION, SUPPORTIVE_PATHS } from "~/routes/paths"; +import { isEmpty } from "~/utils"; +import { _ } from "~/i18n"; +import { sprintf } from "sprintf-js"; + +const LinkToRegistration = () => { + const location = useLocation(); + + if (location.pathname === REGISTRATION.root) return; + + return ( + + {_("Register it now")} + + ); +}; + +export default function ProductRegistrationAlert() { + const location = useLocation(); + const { selectedProduct: product } = useProduct(); + const registration = useRegistration(); + + // NOTE: it shouldn't be mounted in these paths, but let's prevent rendering + // if so just in case. + if (SUPPORTIVE_PATHS.includes(location.pathname)) return; + if (["no", undefined].includes(product.registration) || !isEmpty(registration.key)) return; + + return ( + + + + ); +} diff --git a/web/src/components/product/ProductRegistrationPage.test.tsx b/web/src/components/product/ProductRegistrationPage.test.tsx new file mode 100644 index 0000000000..c83d411d35 --- /dev/null +++ b/web/src/components/product/ProductRegistrationPage.test.tsx @@ -0,0 +1,166 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender } from "~/test-utils"; +import ProductRegistrationPage from "./ProductRegistrationPage"; +import { Product, RegistrationInfo } from "~/types/software"; +import { useProduct, useRegistration } from "~/queries/software"; + +const tw: Product = { + id: "Tumbleweed", + name: "openSUSE Tumbleweed", + registration: "no", +}; + +const sle: Product = { + id: "sle", + name: "SLE", + registration: "mandatory", +}; + +let selectedProduct: Product; +let registrationInfoMock: RegistrationInfo; +const registerMutationMock = jest.fn(); + +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useRegisterMutation: () => ({ mutate: registerMutationMock }), + useRegistration: (): ReturnType => registrationInfoMock, + useProduct: (): ReturnType => { + return { + products: [tw, sle], + selectedProduct, + }; + }, +})); + +describe("ProductRegistrationPage", () => { + describe("when selected product is not registrable", () => { + beforeEach(() => { + selectedProduct = tw; + registrationInfoMock = { key: "", email: "" }; + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("when selected product is registrable and registration code is not set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "", email: "" }; + }); + + it("renders ProductRegistrationAlert component", () => { + installerRender(); + screen.getByText("Warning alert:"); + }); + + it("renders a form to allow user registering the product", async () => { + const { user } = installerRender(); + const registrationCodeInput = screen.getByLabelText("Registration code"); + const emailInput = screen.getByRole("textbox", { name: /Email/ }); + const submitButton = screen.getByRole("button", { name: "Register" }); + + await user.type(registrationCodeInput, "INTERNAL-USE-ONLY-1234-5678"); + await user.type(emailInput, "example@company.test"); + await user.click(submitButton); + + expect(registerMutationMock).toHaveBeenCalledWith( + { + email: "example@company.test", + key: "INTERNAL-USE-ONLY-1234-5678", + }, + expect.anything(), + ); + }); + + it.todo("handles and renders errors from server, if any"); + }); + + describe("when selected product is registrable and registration code is set", () => { + beforeEach(() => { + selectedProduct = sle; + registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "example@company.test" }; + }); + + it("does not render ProductRegistrationAlert component", () => { + installerRender(); + expect(screen.queryByText("Warning alert:")).toBeNull(); + }); + + it("renders registration information with code partially hidden", async () => { + const { user } = installerRender(); + const visibilityCodeToggler = screen.getByRole("button", { name: "Show" }); + screen.getByText(/\*?5678/); + expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull(); + expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull(); + screen.getByText("example@company.test"); + await user.click(visibilityCodeToggler); + screen.getByText("INTERNAL-USE-ONLY-1234-5678"); + await user.click(visibilityCodeToggler); + expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull(); + screen.getByText(/\*?5678/); + }); + + // describe("but at registration path already", () => { + // beforeEach(() => { + // mockRoutes(REGISTRATION.root); + // }); + // + // it("does not render the link to registration", () => { + // installerRender(); + // screen.getByRole("heading", { + // name: /Warning alert:.*must be registered/, + // }); + // expect(screen.queryAllByRole("link")).toEqual([]); + // }); + // }); + // }); + // + // describe("when product is registrable and registration code is already set", () => { + // beforeEach(() => { + // selectedProduct = sle; + // registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" }; + // }); + // + // it("renders nothing", () => { + // const { container } = installerRender(); + // expect(container).toBeEmptyDOMElement(); + // }); + // }); + // + // describe("when product is not registrable", () => { + // beforeEach(() => { + // selectedProduct = tw; + // }); + // + // it("renders nothing", () => { + // const { container } = installerRender(); + // expect(container).toBeEmptyDOMElement(); + // }); + }); +}); diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx new file mode 100644 index 0000000000..2aa23f4f8e --- /dev/null +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -0,0 +1,166 @@ +/* + * Copyright (c) [2023-2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; +import { + ActionGroup, + Alert, + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Flex, + Form, + FormGroup, + Grid, + Stack, + TextInput, +} from "@patternfly/react-core"; +import { Page, PasswordInput } from "~/components/core"; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; +import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import { useProduct, useRegistration, useRegisterMutation } from "~/queries/software"; +import { isEmpty, mask } from "~/utils"; +import { _ } from "~/i18n"; +import { sprintf } from "sprintf-js"; + +const FORM_ID = "productRegistration"; +const KEY_LABEL = _("Registration code"); +const EMAIL_LABEL = "Email"; + +const RegisteredProductSection = () => { + const { selectedProduct: product } = useProduct(); + const registration = useRegistration(); + const [showCode, setShowCode] = useState(false); + const toggleCodeVisibility = () => setShowCode(!showCode); + + return ( + + + + {KEY_LABEL} + + + {showCode ? registration.key : mask(registration.key)} + + + + {!isEmpty(registration.email) && ( + <> + {EMAIL_LABEL} + {registration.email} + + )} + + + + ); +}; + +const RegistrationFormSection = () => { + const { mutate: register } = useRegisterMutation(); + const [key, setKey] = useState(""); + const [email, setEmail] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + // FIXME: use the right type for AxiosResponse + const onRegisterError = ({ response }) => { + const originalMessage = response.data.message; + const from = originalMessage.indexOf(":") + 1; + setError(originalMessage.slice(from).trim()); + }; + + const submit = async (e: React.SyntheticEvent) => { + e.preventDefault(); + setError(null); + setLoading(true); + // @ts-ignore + register({ key, email }, { onError: onRegisterError, onSettled: () => setLoading(false) }); + }; + + // TODO: adjust texts based of registration "type", mandatory or optional + + return ( + +
+ {error && } + + setKey(v)} /> + + + {EMAIL_LABEL} {_("(optional)")} + + } + > + setEmail(v)} /> + + + + + + +
+ ); +}; + +export default function ProductRegistrationPage() { + const { selectedProduct: product } = useProduct(); + const registration = useRegistration(); + + // TODO: render something meaningful instead? "Product not registrable"? + if (product.registration === "no") return; + + return ( + + +

{_("Registration")}

+
+ + + + + {isEmpty(registration.key) ? : } + + + +
+ ); +} diff --git a/web/src/components/product/ProductSelectionPage.test.tsx b/web/src/components/product/ProductSelectionPage.test.tsx index 4209e2da58..da2b4d9d67 100644 --- a/web/src/components/product/ProductSelectionPage.test.tsx +++ b/web/src/components/product/ProductSelectionPage.test.tsx @@ -24,15 +24,21 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, mockNavigateFn } from "~/test-utils"; import { ProductSelectionPage } from "~/components/product"; -import { Product } from "~/types/software"; -import { useProduct } from "~/queries/software"; +import { Product, RegistrationInfo } from "~/types/software"; +import { useProduct, useRegistration } from "~/queries/software"; + +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); const mockConfigMutation = jest.fn(); + const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", icon: "tumbleweed.svg", description: "Tumbleweed description...", + registration: "no", }; const microOs: Product = { @@ -40,39 +46,80 @@ const microOs: Product = { name: "openSUSE MicroOS", icon: "microos.svg", description: "MicroOS description", + registration: "no", }; +let mockSelectedProduct: Product; +let registrationInfoMock: RegistrationInfo; + jest.mock("~/queries/software", () => ({ ...jest.requireActual("~/queries/software"), useProduct: (): ReturnType => { return { products: [tumbleweed, microOs], - selectedProduct: tumbleweed, + selectedProduct: mockSelectedProduct, }; }, useProductChanges: () => jest.fn(), useConfigMutation: () => ({ mutate: mockConfigMutation }), + useRegistration: (): ReturnType => registrationInfoMock, })); -describe("when the user chooses a product and hits the confirmation button", () => { - it("triggers the product selection", async () => { - const { user } = installerRender(); - const productOption = screen.getByRole("radio", { name: microOs.name }); - const selectButton = screen.getByRole("button", { name: "Select" }); - await user.click(productOption); - await user.click(selectButton); - expect(mockConfigMutation).toHaveBeenCalledWith({ product: microOs.id }); +describe("ProductSelectionPage", () => { + beforeEach(() => { + mockSelectedProduct = tumbleweed; + registrationInfoMock = { key: "", email: "" }; + }); + + describe("when there is a registration code set", () => { + beforeEach(() => { + registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "" }; + }); + + it("navigates to root path", async () => { + installerRender(); + await screen.findByText("Navigating to /"); + }); + }); + + describe("when there is a product already selected", () => { + it("renders the Cancel button", () => { + installerRender(); + screen.getByRole("button", { name: "Cancel" }); + }); + }); + + describe("when there is not a product selected yet", () => { + beforeEach(() => { + mockSelectedProduct = undefined; + }); + + it("does not render the Cancel button", () => { + installerRender(); + expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull(); + }); + }); + + describe("when the user chooses a product and hits the confirmation button", () => { + it("triggers the product selection", async () => { + const { user } = installerRender(); + const productOption = screen.getByRole("radio", { name: microOs.name }); + const selectButton = screen.getByRole("button", { name: "Select" }); + await user.click(productOption); + await user.click(selectButton); + expect(mockConfigMutation).toHaveBeenCalledWith({ product: microOs.id }); + }); }); -}); -describe("when the user chooses a product but hits the cancel button", () => { - it("does not trigger the product selection and goes back", async () => { - const { user } = installerRender(); - const productOption = screen.getByRole("radio", { name: microOs.name }); - const cancelButton = screen.getByRole("button", { name: "Cancel" }); - await user.click(productOption); - await user.click(cancelButton); - expect(mockConfigMutation).not.toHaveBeenCalled(); - expect(mockNavigateFn).toHaveBeenCalledWith("/"); + describe("when the user chooses a product but hits the cancel button", () => { + it("does not trigger the product selection and goes back", async () => { + const { user } = installerRender(); + const productOption = screen.getByRole("radio", { name: microOs.name }); + const cancelButton = screen.getByRole("button", { name: "Cancel" }); + await user.click(productOption); + await user.click(cancelButton); + expect(mockConfigMutation).not.toHaveBeenCalled(); + expect(mockNavigateFn).toHaveBeenCalledWith("/"); + }); }); }); diff --git a/web/src/components/product/ProductSelectionPage.tsx b/web/src/components/product/ProductSelectionPage.tsx index c24135c712..994f4ac269 100644 --- a/web/src/components/product/ProductSelectionPage.tsx +++ b/web/src/components/product/ProductSelectionPage.tsx @@ -34,15 +34,16 @@ import { FormGroup, Button, } from "@patternfly/react-core"; +import { Navigate, useNavigate } from "react-router-dom"; import { Page } from "~/components/core"; import { Center } from "~/components/layout"; -import { useConfigMutation, useProduct } from "~/queries/software"; +import { useConfigMutation, useProduct, useRegistration } from "~/queries/software"; import pfTextStyles from "@patternfly/react-styles/css/utilities/Text/text"; import pfRadioStyles from "@patternfly/react-styles/css/components/Radio/radio"; -import { slugify } from "~/utils"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -import { useNavigate } from "react-router-dom"; +import { PATHS } from "~/router"; +import { isEmpty } from "~/utils"; const ResponsiveGridItem = ({ children }) => ( @@ -51,8 +52,7 @@ const ResponsiveGridItem = ({ children }) => ( ); const Option = ({ product, isChecked, onChange }) => { - const id = slugify(product.name); - const detailsId = `${id}-details`; + const detailsId = `${product.id}-details`; const logoSrc = `assets/logos/${product.icon}`; // TRANSLATORS: %s will be replaced by a product name. E.g., "openSUSE Tumbleweed" const logoAltText = sprintf(_("%s logo"), product.name); @@ -63,7 +63,7 @@ const Option = ({ product, isChecked, onChange }) => { { {logoAltText}
- + {selectedProduct && !isLoading && } () =>
ProgressReport Mock
); + +let isBusy = false; +const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: "no" }; + +jest.mock("~/queries/status", () => ({ + ...jest.requireActual("~/queries/status"), + useInstallerStatus: () => ({ isBusy }), +})); + +jest.mock("~/queries/software", () => ({ + ...jest.requireActual("~/queries/software"), + useProduct: () => ({ selectedProduct: tumbleweed }), +})); + +describe("ProductSelectionProgress", () => { + describe("when installer is not busy", () => { + it("redirects to the root path", async () => { + installerRender(); + await screen.findByText(`Navigating to ${ROOT.root}`); + }); + }); + + describe("when installer in busy", () => { + beforeEach(() => { + isBusy = true; + }); + + it("renders progress report", () => { + installerRender(); + screen.getByText("ProgressReport Mock"); + }); + }); +}); diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.tsx similarity index 96% rename from web/src/components/product/ProductSelectionProgress.jsx rename to web/src/components/product/ProductSelectionProgress.tsx index cfc80bed82..09df5286d2 100644 --- a/web/src/components/product/ProductSelectionProgress.jsx +++ b/web/src/components/product/ProductSelectionProgress.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -29,8 +29,6 @@ import { ROOT as PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; /** - * @component - * * Shows progress steps when a product is selected. */ function ProductSelectionProgress() { diff --git a/web/src/components/product/index.ts b/web/src/components/product/index.ts index 1340569f37..fc63c951d0 100644 --- a/web/src/components/product/index.ts +++ b/web/src/components/product/index.ts @@ -22,3 +22,5 @@ export { default as ProductSelectionPage } from "./ProductSelectionPage"; export { default as ProductSelectionProgress } from "./ProductSelectionProgress"; +export { default as ProductRegistrationPage } from "./ProductRegistrationPage"; +export { default as ProductRegistrationAlert } from "./ProductRegistrationAlert"; diff --git a/web/src/components/software/SoftwarePage.test.tsx b/web/src/components/software/SoftwarePage.test.tsx index 88a99d05e3..132d5b0e23 100644 --- a/web/src/components/software/SoftwarePage.test.tsx +++ b/web/src/components/software/SoftwarePage.test.tsx @@ -28,6 +28,10 @@ import testingPatterns from "./patterns.test.json"; import testingProposal from "./proposal.test.json"; import SoftwarePage from "./SoftwarePage"; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/issues", () => ({ useIssues: () => [], })); diff --git a/web/src/components/software/SoftwarePatternsSelection.test.tsx b/web/src/components/software/SoftwarePatternsSelection.test.tsx index 1072054b56..e80ecac6b9 100644 --- a/web/src/components/software/SoftwarePatternsSelection.test.tsx +++ b/web/src/components/software/SoftwarePatternsSelection.test.tsx @@ -28,6 +28,10 @@ import SoftwarePatternsSelection from "./SoftwarePatternsSelection"; const onConfigMutationMock = { mutate: jest.fn() }; +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + jest.mock("~/queries/software", () => ({ usePatterns: () => testingPatterns, useConfigMutation: () => onConfigMutationMock, diff --git a/web/src/components/software/index.js b/web/src/components/software/index.ts similarity index 100% rename from web/src/components/software/index.js rename to web/src/components/software/index.ts diff --git a/web/src/components/storage/BootSelection.test.tsx b/web/src/components/storage/BootSelection.test.tsx index f2d8450770..4347e0212b 100644 --- a/web/src/components/storage/BootSelection.test.tsx +++ b/web/src/components/storage/BootSelection.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -20,15 +20,19 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; import { screen, within } from "@testing-library/react"; -import { mockNavigateFn, plainRender } from "~/test-utils"; +import { installerRender, mockNavigateFn } from "~/test-utils"; import BootSelection from "./BootSelection"; import { StorageDevice } from "~/types/storage"; import { BootHook } from "~/queries/storage/config-model"; +// FIXME: drop this mock once a better solution for dealing with +// ProductRegistrationAlert, which uses a query with suspense, +jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( +
ProductRegistrationAlert Mock
+)); + const sda: StorageDevice = { sid: 59, isDrive: true, @@ -122,24 +126,24 @@ jest.mock("~/queries/storage/config-model", () => ({ })); describe("BootSelection", () => { - const automaticOption = () => screen.queryByRole("radio", { name: "Automatic" }); - const selectDiskOption = () => screen.queryByRole("radio", { name: "Select a disk" }); - const notConfigureOption = () => screen.queryByRole("radio", { name: "Do not configure" }); - const diskSelector = () => screen.queryByRole("combobox", { name: /choose a disk/i }); + const automaticOption = () => screen.getByRole("radio", { name: "Automatic" }); + const selectDiskOption = () => screen.getByRole("radio", { name: "Select a disk" }); + const notConfigureOption = () => screen.getByRole("radio", { name: "Do not configure" }); + const diskSelector = () => screen.getByRole("combobox", { name: /choose a disk/i }); it("offers an option to configure boot in the installation disk", () => { - plainRender(); + installerRender(); expect(automaticOption()).toBeInTheDocument(); }); it("offers an option to configure boot in a selected disk", () => { - plainRender(); + installerRender(); expect(selectDiskOption()).toBeInTheDocument(); expect(diskSelector()).toBeInTheDocument(); }); it("offers an option to not configure boot", () => { - plainRender(); + installerRender(); expect(notConfigureOption()).toBeInTheDocument(); }); @@ -150,7 +154,7 @@ describe("BootSelection", () => { }); it("selects 'Automatic' option by default", () => { - plainRender(); + installerRender(); expect(automaticOption()).toBeChecked(); expect(selectDiskOption()).not.toBeChecked(); expect(diskSelector()).toBeDisabled(); @@ -166,7 +170,7 @@ describe("BootSelection", () => { }); it("selects 'Select a disk' option by default", () => { - plainRender(); + installerRender(); expect(automaticOption()).not.toBeChecked(); expect(selectDiskOption()).toBeChecked(); expect(diskSelector()).toBeEnabled(); @@ -180,7 +184,7 @@ describe("BootSelection", () => { }); it("selects 'Do not configure' option by default", () => { - plainRender(); + installerRender(); expect(automaticOption()).not.toBeChecked(); expect(selectDiskOption()).not.toBeChecked(); expect(diskSelector()).toBeDisabled(); @@ -189,7 +193,7 @@ describe("BootSelection", () => { }); it("does not change the boot options on cancel", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const cancel = screen.getByRole("button", { name: "Cancel" }); await user.click(cancel); @@ -200,7 +204,7 @@ describe("BootSelection", () => { }); it("applies the expected boot options when 'Automatic' is selected", async () => { - const { user } = plainRender(); + const { user } = installerRender(); await user.click(automaticOption()); const accept = screen.getByRole("button", { name: "Accept" }); @@ -210,7 +214,7 @@ describe("BootSelection", () => { }); it("applies the expected boot options when a disk is selected", async () => { - const { user } = plainRender(); + const { user } = installerRender(); await user.click(selectDiskOption()); const selector = diskSelector(); @@ -224,7 +228,7 @@ describe("BootSelection", () => { }); it("applies the expected boot options when 'No configure' is selected", async () => { - const { user } = plainRender(); + const { user } = installerRender(); await user.click(notConfigureOption()); const accept = screen.getByRole("button", { name: "Accept" }); diff --git a/web/src/components/storage/BootSelection.tsx b/web/src/components/storage/BootSelection.tsx index 5f4b56a8d6..b94a3e1d26 100644 --- a/web/src/components/storage/BootSelection.tsx +++ b/web/src/components/storage/BootSelection.tsx @@ -88,10 +88,6 @@ export default function BootSelectionDialog() { const onSubmit = async (e) => { e.preventDefault(); - // FIXME: try to use formData here too? - // const formData = new FormData(e.target); - // const mode = formData.get("bootMode"); - // const device = formData.get("bootDevice"); switch (state.selectedOption) { case BOOT_DISABLED_ID: diff --git a/web/src/components/storage/DeviceSelection.tsx b/web/src/components/storage/DeviceSelection.tsx index 434880e177..aefffe1f81 100644 --- a/web/src/components/storage/DeviceSelection.tsx +++ b/web/src/components/storage/DeviceSelection.tsx @@ -27,11 +27,17 @@ import { Page } from "~/components/core"; import { DeviceSelectorTable } from "~/components/storage"; import DevicesTechMenu from "./DevicesTechMenu"; import { ProposalTarget, StorageDevice } from "~/types/storage"; -import { useAvailableDevices, useProposalMutation, useProposalResult } from "~/queries/storage"; +import { + useAvailableDevices, + useProposalMutation, + useProposalResult, + useRefresh, +} from "~/queries/storage"; import { deviceChildren } from "~/components/storage/utils"; import { compact } from "~/utils"; import a11y from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; import { _ } from "~/i18n"; +import { Loading } from "~/components/layout"; const SELECT_DISK_ID = "select-disk"; const CREATE_LVM_ID = "create-lvm"; @@ -49,25 +55,33 @@ type DeviceSelectionState = { * @component */ export default function DeviceSelection() { - const { settings } = useProposalResult(); + const proposal = useProposalResult(); const availableDevices = useAvailableDevices(); const updateProposal = useProposalMutation(); const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); const [state, setState] = useState({}); const isTargetDisk = state.target === ProposalTarget.DISK; const isTargetNewLvmVg = state.target === ProposalTarget.NEW_LVM_VG; + useRefresh({ + onStart: () => setIsLoading(true), + onFinish: () => setIsLoading(false), + }); + useEffect(() => { if (state.target !== undefined) return; // FIXME: move to a state/reducer setState({ - target: settings.target, - targetDevice: availableDevices.find((d) => d.name === settings.targetDevice), - targetPVDevices: availableDevices.filter((d) => settings.targetPVDevices?.includes(d.name)), + target: proposal.settings.target, + targetDevice: availableDevices.find((d) => d.name === proposal.settings.targetDevice), + targetPVDevices: availableDevices.filter((d) => + proposal.settings.targetPVDevices?.includes(d.name), + ), }); - }, [settings, availableDevices, state.target]); + }, [proposal, availableDevices, state.target]); const selectTargetDisk = () => setState({ ...state, target: ProposalTarget.DISK }); const selectTargetNewLvmVG = () => setState({ ...state, target: ProposalTarget.NEW_LVM_VG }); @@ -86,7 +100,7 @@ export default function DeviceSelection() { targetPVDevices: isTargetNewLvmVg ? state.targetPVDevices.map((d) => d.name) : [], }; - updateProposal.mutateAsync({ ...settings, ...newSettings }); + updateProposal.mutateAsync({ ...proposal.settings, ...newSettings }); navigate(".."); }; @@ -116,6 +130,8 @@ physical volumes will be created on demand as new partitions at the selected \ devices.", ).split(/[[\]]/); + if (isLoading) return ; + return ( diff --git a/web/src/components/storage/DevicesTechMenu.test.tsx b/web/src/components/storage/DevicesTechMenu.test.tsx index a527322d8a..5b8bf07358 100644 --- a/web/src/components/storage/DevicesTechMenu.test.tsx +++ b/web/src/components/storage/DevicesTechMenu.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * @@ -24,7 +24,6 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import DevicesTechMenu from "./DevicesTechMenu"; -import { _ } from "~/i18n"; import { supportedDASD } from "~/api/storage/dasd"; import { supportedZFCP } from "~/api/storage/zfcp"; @@ -37,7 +36,7 @@ beforeEach(() => { }); it("contains an entry for configuring iSCSI", async () => { - const { user } = installerRender(); + const { user } = installerRender(); const toggler = screen.getByRole("button"); await user.click(toggler); const link = screen.getByRole("option", { name: /iSCSI/ }); @@ -45,7 +44,7 @@ it("contains an entry for configuring iSCSI", async () => { }); it("does not contain an entry for configuring DASD when is NOT supported", async () => { - const { user } = installerRender(); + const { user } = installerRender(); const toggler = screen.getByRole("button"); await user.click(toggler); expect(screen.queryByRole("option", { name: /DASD/ })).toBeNull(); @@ -53,7 +52,7 @@ it("does not contain an entry for configuring DASD when is NOT supported", async it("contains an entry for configuring DASD when is supported", async () => { (supportedDASD as jest.Mock).mockResolvedValue(true); - const { user } = installerRender(); + const { user } = installerRender(); const toggler = screen.getByRole("button"); await user.click(toggler); const link = screen.getByRole("option", { name: /DASD/ }); @@ -61,7 +60,7 @@ it("contains an entry for configuring DASD when is supported", async () => { }); it("does not contain an entry for configuring zFCP when is NOT supported", async () => { - const { user } = installerRender(); + const { user } = installerRender(); const toggler = screen.getByRole("button"); await user.click(toggler); expect(screen.queryByRole("option", { name: /DASD/ })).toBeNull(); @@ -69,7 +68,7 @@ it("does not contain an entry for configuring zFCP when is NOT supported", async it("contains an entry for configuring zFCP when is supported", async () => { (supportedZFCP as jest.Mock).mockResolvedValue(true); - const { user } = installerRender(); + const { user } = installerRender(); const toggler = screen.getByRole("button"); await user.click(toggler); const link = screen.getByRole("option", { name: /zFCP/ }); diff --git a/web/src/components/storage/ISCSIPage.tsx b/web/src/components/storage/ISCSIPage.tsx index 401c03d84a..79c7852505 100644 --- a/web/src/components/storage/ISCSIPage.tsx +++ b/web/src/components/storage/ISCSIPage.tsx @@ -24,6 +24,7 @@ import { Grid, GridItem } from "@patternfly/react-core"; import React from "react"; import { Page } from "~/components/core"; import { InitiatorSection, TargetsSection } from "~/components/storage/iscsi"; +import { STORAGE as PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; export default function ISCSIPage() { @@ -42,6 +43,11 @@ export default function ISCSIPage() {
+ + + {_("Back to device selection")} + + ); } diff --git a/web/src/components/storage/ProposalPage.test.tsx b/web/src/components/storage/ProposalPage.test.tsx index 68e6d235eb..f16f32d8fc 100644 --- a/web/src/components/storage/ProposalPage.test.tsx +++ b/web/src/components/storage/ProposalPage.test.tsx @@ -27,7 +27,7 @@ */ import React from "react"; import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; import { ProposalPage } from "~/components/storage"; import { ProposalResult, @@ -140,6 +140,6 @@ jest.mock("~/queries/storage", () => ({ })); it("renders the device, settings and result sections", () => { - plainRender(); + installerRender(); screen.findByText("Device"); }); diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index 2a7b8023c3..620d14731b 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -20,21 +20,20 @@ * find current contact information at www.suse.com. */ -import React from "react"; +import React, { useState } from "react"; import { Grid, GridItem, SplitItem } from "@patternfly/react-core"; import { Page } from "~/components/core/"; +import { Loading } from "~/components/layout"; +import EncryptionField from "~/components/storage/EncryptionField"; import ProposalResultSection from "./ProposalResultSection"; import ProposalTransactionalInfo from "./ProposalTransactionalInfo"; import ConfigEditor from "./ConfigEditor"; import ConfigEditorMenu from "./ConfigEditorMenu"; -import EncryptionField from "~/components/storage/EncryptionField"; -import { _ } from "~/i18n"; import { toValidationError } from "~/utils"; import { useIssues } from "~/queries/issues"; import { IssueSeverity } from "~/types/issues"; -import { useDeprecated, useDevices, useProposalResult } from "~/queries/storage"; -import { useQueryClient } from "@tanstack/react-query"; -import { refresh } from "~/api/storage"; +import { useDevices, useProposalResult, useRefresh } from "~/queries/storage"; +import { _ } from "~/i18n"; /** * Which UI item is being changed by user @@ -60,24 +59,34 @@ export const NOT_AFFECTED = { }; export default function ProposalPage() { + const [isLoading, setIsLoading] = useState(false); const systemDevices = useDevices("system"); const stagingDevices = useDevices("result"); const { actions } = useProposalResult(); - const deprecated = useDeprecated(); - const queryClient = useQueryClient(); - React.useEffect(() => { - if (deprecated) { - refresh().then(() => { - queryClient.invalidateQueries({ queryKey: ["storage"] }); - }); - } - }, [deprecated, queryClient]); + useRefresh({ + onStart: () => setIsLoading(true), + onFinish: () => setIsLoading(false), + }); const errors = useIssues("storage") .filter((s) => s.severity === IssueSeverity.Error) .map(toValidationError); + if (isLoading) { + return ( + + +

{_("Storage")}

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