From 09b9bc2a5dfc91e9802c2e1b8e6839fbdf41d577 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Tue, 5 Sep 2023 21:24:30 -0300 Subject: [PATCH] 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 | 51 +++++++++++++++++++++++++++++------ pkg/lib/packagekit.js | 25 +++++++++++++++++ test/verify/check-apps | 8 ++++-- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/pkg/apps/application-list.jsx b/pkg/apps/application-list.jsx index 847249f5b170..04f2484f29a6 100644 --- a/pkg/apps/application-list.jsx +++ b/pkg/apps/application-list.jsx @@ -19,7 +19,7 @@ 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"; @@ -27,11 +27,13 @@ import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/ind import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/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 +95,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 +120,37 @@ export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, ac } } + async function check_missing_data(packages) { + if (packages.length === 0) + return; + 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,6 +173,10 @@ export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, ac action={action} />); } + const data_missing_msg = (dataPackagesInstalled == false && !refresh_progress) + ? _("Application information is missing") + : null; + return ( @@ -165,8 +195,13 @@ export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, ac {comps.length == 0 - ? + ? : + {!progress && data_missing_msg && + {_("Install")}} />} { tbody } diff --git a/pkg/lib/packagekit.js b/pkg/lib/packagekit.js index 6bc76149ee1e..5ba1cca3bc1c 100644 --- a/pkg/lib/packagekit.js +++ b/pkg/lib/packagekit.js @@ -458,6 +458,31 @@ 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_NEWEST, names], + null, // don't need progress, this is fast + { + Package: (info, package_id) => { + const parts = package_id.split(";"); + if (parts[3].includes("installed")) + 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 a847ca35e81c..51cc2e913e36 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')")