Skip to content

Commit

Permalink
apps: Warn if appstream data package is missing
Browse files Browse the repository at this point in the history
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 <[email protected]> for the idea and initial
implementation!
  • Loading branch information
martinpitt committed Oct 11, 2023
1 parent 077b43e commit 09b9bc2
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 10 deletions.
51 changes: 43 additions & 8 deletions pkg/apps/application-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@

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 { 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;

Expand Down Expand Up @@ -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]);
Expand All @@ -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;
Expand All @@ -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 (
<Page id="list-page">
<PageSection variant={PageSectionVariants.light}>
Expand All @@ -165,8 +195,13 @@ export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, ac
</Flex>
</PageSection>
{comps.length == 0
? <EmptyStatePanel title={ _("No applications installed or available.") } />
? <EmptyStatePanel title={ _("No applications installed or available.") }
paragraph={data_missing_msg}
action={_("Install application information")} onAction={refresh} />
: <PageSection>
{!progress && data_missing_msg &&
<Alert variant="warning" isInline title={data_missing_msg}
actionLinks={ <AlertActionLink onClick={refresh}>{_("Install")}</AlertActionLink>} />}
<Card>
<DataList aria-label={_("Applications list")}>
{ tbody }
Expand Down
25 changes: 25 additions & 0 deletions pkg/lib/packagekit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 6 additions & 2 deletions test/verify/check-apps
Original file line number Diff line number Diff line change
Expand Up @@ -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')")
Expand Down

0 comments on commit 09b9bc2

Please sign in to comment.