From 077b43eaedf3a44037f8dba11a5da3cf7ab24ae1 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Wed, 11 Oct 2023 12:49:15 +0200 Subject: [PATCH 1/2] lib: Document and properly initialize packagekit missing_ids This gets added to `data` in the middle of `check_missing_packages()` anyway, so declare it right away. Also document it, as it's part of the result and e.g. `install_missing_packages()` assumes that this field exists. --- pkg/lib/packagekit.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/lib/packagekit.js b/pkg/lib/packagekit.js index eb2688f31cc..6bc76149ee1 100644 --- a/pkg/lib/packagekit.js +++ b/pkg/lib/packagekit.js @@ -343,6 +343,8 @@ export function watchRedHatSubscription(callback) { * - missing_names: Packages that were requested, are currently not installed, * and can be installed. * + * - missing_ids: The full PackageKit IDs corresponding to missing_names + * * - unavailable_names: Packages that were requested, are currently not installed, * but can't be found in any repository. * @@ -359,6 +361,7 @@ export function watchRedHatSubscription(callback) { export function check_missing_packages(names, progress_cb) { const data = { + missing_ids: [], missing_names: [], unavailable_names: [], }; @@ -371,8 +374,6 @@ export function check_missing_packages(names, progress_cb) { } function resolve() { - data.missing_ids = []; - const installed_names = { }; return cancellableTransaction("Resolve", From 1a910defa44bb092cf04c9273365ea6589621c0f Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Tue, 5 Sep 2023 21:24:30 -0300 Subject: [PATCH 2/2] apps: Warn if appstream data package is missing Introduce a new packagekit.js helper check_uninstalled_packages() which is a lightweight version of check_missing_packages() that avoids the expensive Refresh call. If there are no installed nor available apps, show an explanation and an action button in the empty state, otherwise show an alert with an "Install" button on top of the installed apps. Use the latter in TestApps.testBasic, as the top right "Refresh" button is already covered by other test cases. Fixes #18454 Thanks to leomoty for the idea and initial implementation! --- pkg/apps/application-list.jsx | 73 ++++++++++++++++++++++++++++------- pkg/lib/packagekit.js | 24 ++++++++++++ test/verify/check-apps | 14 ++++++- 3 files changed, 94 insertions(+), 17 deletions(-) diff --git a/pkg/apps/application-list.jsx b/pkg/apps/application-list.jsx index 847249f5b17..35ee25353ce 100644 --- a/pkg/apps/application-list.jsx +++ b/pkg/apps/application-list.jsx @@ -19,19 +19,23 @@ import cockpit from "cockpit"; import React, { useState } from "react"; -import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; +import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Card } from "@patternfly/react-core/dist/esm/components/Card/index.js"; import { DataList, DataListAction, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js"; +import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js"; + import { RebootingIcon } from "@patternfly/react-icons"; +import { check_uninstalled_packages } from "packagekit.js"; import * as PackageKit from "./packagekit.js"; import { read_os_release } from "os-release.js"; import { icon_url, show_error, launch, ProgressBar, CancelButton } from "./utils.jsx"; import { ActionButton } from "./application.jsx"; import { EmptyStatePanel } from "cockpit-components-empty-state.jsx"; +import { useInit } from "../lib/hooks.js"; const _ = cockpit.gettext; @@ -93,6 +97,7 @@ const ApplicationRow = ({ comp, progress, progress_title, action }) => { export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, action }) => { const [progress, setProgress] = useState(false); + const [dataPackagesInstalled, setDataPackagesInstalled] = useState(null); const comps = []; for (const id in metainfo_db.components) comps.push(metainfo_db.components[id]); @@ -117,14 +122,35 @@ export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, ac } } + async function check_missing_data(packages) { + try { + const missing = await check_uninstalled_packages(packages); + setDataPackagesInstalled(missing.size === 0); + } catch (e) { + console.warn("Failed to check missing AppStream metadata packages:", e.toString()); + } + } + + useInit(async () => { + const os_release = await read_os_release(); + const configPackages = get_config('appstream_config_packages', os_release, []); + const dataPackages = get_config('appstream_data_packages', os_release, []); + await check_missing_data([...dataPackages, ...configPackages]); + }); + function refresh() { - read_os_release().then(os_release => + read_os_release().then(os_release => { + const configPackages = get_config('appstream_config_packages', os_release, []); + const dataPackages = get_config('appstream_data_packages', os_release, []); PackageKit.refresh(metainfo_db.origin_files, - get_config('appstream_config_packages', os_release, []), - get_config('appstream_data_packages', os_release, []), - setProgress)) - .finally(() => setProgress(false)) - .catch(show_error); + configPackages, + dataPackages, + setProgress) + .finally(async () => { + await check_missing_data([...dataPackages, ...configPackages]); + setProgress(false); + }).catch(show_error); + }); } let refresh_progress, refresh_button, tbody; @@ -147,8 +173,12 @@ export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, ac action={action} />); } + const data_missing_msg = (dataPackagesInstalled == false && !refresh_progress) + ? _("Application information is missing") + : null; + return ( - +

{_("Applications")}

@@ -165,14 +195,27 @@ export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, ac
{comps.length == 0 - ? + ? : - - - { tbody } - - - } + + {!progress && data_missing_msg && + + {_("Install")}} /> + + } + + + + { tbody } + + + + + + }
); }; diff --git a/pkg/lib/packagekit.js b/pkg/lib/packagekit.js index 6bc76149ee1..c11cd322076 100644 --- a/pkg/lib/packagekit.js +++ b/pkg/lib/packagekit.js @@ -458,6 +458,30 @@ export function check_missing_packages(names, progress_cb) { .then(get_details); } +/* Check a list of packages whether they are installed. + * + * This is a lightweight version of check_missing_packages() which does not + * refresh, simulates, or retrieves details. It just checks which of the given package + * names are already installed, and returns a Set of the missing ones. + */ +export function check_uninstalled_packages(names) { + const uninstalled = new Set(names); + + if (names.length === 0) + return Promise.resolve(uninstalled); + + return cancellableTransaction("Resolve", + [Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_INSTALLED, names], + null, // don't need progress, this is fast + { + Package: (info, package_id) => { + const parts = package_id.split(";"); + uninstalled.delete(parts[0]); + }, + }) + .then(() => uninstalled); +} + /* Carry out what check_missing_packages has planned. * * In addition to the usual "waiting", "percentage", and "cancel" diff --git a/test/verify/check-apps b/test/verify/check-apps index a847ca35e81..547e5c798fa 100755 --- a/test/verify/check-apps +++ b/test/verify/check-apps @@ -101,20 +101,24 @@ class TestApps(packagelib.PackageCase): self.login_and_go("/apps", urlroot=urlroot) b.wait_in_text(".pf-v5-c-empty-state", "No applications installed or available") + b.wait_in_text(".pf-v5-c-empty-state", "Application information is missing") # still no metadata, but already installed application self.createAppStreamPackage("already", "1.0", "1", install=True) b.wait_not_present(".pf-v5-c-empty-state") b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('already') button:contains('Remove')") + b.wait_in_text(".pf-v5-c-alert", "Application information is missing") self.createAppStreamPackage("app-1", "1.0", "1") self.createAppStreamRepoPackage() - # Refresh package info to install metadata - b.click("#refresh") + # Install package metadata + b.click(".pf-v5-c-alert button") with b.wait_timeout(30): + b.wait_not_present(".pf-v5-c-alert") b.click(".app-list #app-1") + b.wait_visible('a[href="https://app-1.com"]') b.wait_visible(f'#app-page img[src^="{urlroot}/cockpit/channel/"]') b.click(".pf-v5-c-breadcrumb a:contains('Applications')") @@ -172,6 +176,12 @@ class TestApps(packagelib.PackageCase): time.sleep(1) b.wait_not_present("#refresh-progress") b.wait_visible(".pf-v5-c-empty-state") + # wait until check for installed metadata package finished + b.wait_attr("#list-page", "data-packages-checked", "true") + # no appstream metadata available, don't advertise it + b.wait_in_text(".pf-v5-c-empty-state", "No applications installed or available") + self.assertNotIn("Install application information", b.text(".pf-v5-c-empty-state")) + b.wait_not_present(".pf-v5-c-empty-state button") # unknown OS: nothing gets installed m.write("/etc/os-release", 'ID="unmapped"\nID_LIKE="mysterious"\nVERSION_ID="1"\n')