Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apps: Warn if appstream data package is missing #19281

Merged
merged 2 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 58 additions & 15 deletions pkg/apps/application-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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]);
Expand All @@ -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());
Comment on lines +129 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test. Details

}
}

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,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 (
<Page id="list-page">
<Page id="list-page" data-packages-checked={dataPackagesInstalled !== null}>
<PageSection variant={PageSectionVariants.light}>
<Flex alignItems={{ default: 'alignItemsCenter' }}>
<h2 className="pf-v5-u-font-size-3xl">{_("Applications")}</h2>
Expand All @@ -165,14 +195,27 @@ 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={ data_missing_msg && _("Install application information")} onAction={refresh} />
: <PageSection>
<Card>
<DataList aria-label={_("Applications list")}>
{ tbody }
</DataList>
</Card>
</PageSection>}
<Stack hasGutter>
{!progress && data_missing_msg &&
<StackItem key="missing-meta-alert">
<Alert variant="warning" isInline title={data_missing_msg}
actionLinks={ <AlertActionLink onClick={refresh}>{_("Install")}</AlertActionLink>} />
</StackItem>
}
<StackItem>
<Card>
<DataList aria-label={_("Applications list")}>
{ tbody }
</DataList>
</Card>
</StackItem>
</Stack>
</PageSection>
}
</Page>
);
};
29 changes: 27 additions & 2 deletions pkg/lib/packagekit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -359,6 +361,7 @@ export function watchRedHatSubscription(callback) {

export function check_missing_packages(names, progress_cb) {
const data = {
missing_ids: [],
missing_names: [],
unavailable_names: [],
};
Expand All @@ -371,8 +374,6 @@ export function check_missing_packages(names, progress_cb) {
}

function resolve() {
data.missing_ids = [];

const installed_names = { };

return cancellableTransaction("Resolve",
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 12 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 Expand Up @@ -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')
Expand Down