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 eb2688f31cc..c11cd322076 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", @@ -457,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')