LCOV - code coverage report
Current view: top level - pkg/apps - application-list.jsx Coverage Total Hit
Test: cockpit Lines: 85.5 % 179 153
Test Date: 2026-06-16 14:09:37

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2017 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6            6 : import cockpit from "cockpit";
       7            6 : import React, { useState } from "react";
       8              : import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
       9              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      10              : import { DataList, DataListAction, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
      11              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      12              : import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
      13              : import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
      14              : 
      15              : import { RebootingIcon } from "@patternfly/react-icons";
      16              : 
      17              : import { get_manifest_config_matchlist } from "utils";
      18              : import { read_os_release } from "os-release";
      19              : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
      20              : import { useInit } from "hooks";
      21              : 
      22              : import { ProgressReporter, icon_url, show_error, launch, ProgressBar, CancelButton } from "./utils";
      23              : import { ActionButton } from "./application.jsx";
      24              : import { getPackageManager } from "packagemanager.js";
      25              : 
      26            6 : const _ = cockpit.gettext;
      27              : 
      28            3 : async function refresh_appstream_metadata(origin_files, config_packages, data_packages, progress_cb) {
      29              :     /* In addition to refreshing the repository metadata, we also
      30              :      * update all packages that contain AppStream collection metadata.
      31              :      *
      32              :      * AppStream collection metadata is arguably part of the
      33              :      * repository metadata and should be updated during a regular
      34              :      * refresh of the repositories.  On some distributions this is
      35              :      * what happens, but on others (such as Fedora), the collection
      36              :      * metadata is delivered in packages.  We find them and update
      37              :      * them explicitly.
      38              :      *
      39              :      * Also, we have two explicit lists of packages, and we make sure
      40              :      * that they are installed.  The first list contains packages that
      41              :      * configure the system to retrieve AppStream data as part of
      42              :      * repository metadata, and the second list contains packages that
      43              :      * contain AppStream data themselves.
      44              :      */
      45            3 :     const packagemanager = await getPackageManager();
      46            3 :     const progress = new ProgressReporter(0, 1, progress_cb);
      47              : 
      48            0 :     if (config_packages.length > 0) {
      49            0 :         progress.base = 0;
      50            0 :         progress.range = 5;
      51              : 
      52            0 :         await packagemanager.install_packages(config_packages, progress.progress_reporter);
      53            0 :     }
      54              : 
      55            3 :     const origin_pkgs = new Set(await packagemanager.find_file_packages(origin_files, progress.progress_reporter));
      56              : 
      57            3 :     progress.base = 6;
      58            3 :     progress.range = 69;
      59            3 :     await packagemanager.refresh(true, progress.progress_reporter);
      60              : 
      61            3 :     progress.base = 75;
      62            3 :     progress.range = 5;
      63              : 
      64            3 :     const updates = await packagemanager.get_updates(false, progress.progress_reporter);
      65            3 :     const filtered_updates = [];
      66              : 
      67            0 :     for (const update of updates) {
      68            0 :         if (origin_pkgs.has(update.name))
      69            0 :             filtered_updates.push(update);
      70            0 :     }
      71              : 
      72            3 :     progress.base = 80;
      73            3 :     progress.range = 15;
      74              : 
      75            3 :     if (filtered_updates.length > 0)
      76            0 :         return packagemanager.update_packages(filtered_updates, progress.progress_reporter, null);
      77              : 
      78            3 :     if (data_packages.length > 0) {
      79            3 :         progress.range = 95;
      80            3 :         await packagemanager.install_packages(data_packages, progress.progress_reporter);
      81            3 :     }
      82            3 : }
      83              : 
      84            5 : const ApplicationRow = ({ comp, progress, progress_title, action }) => {
      85            5 :     const [error, setError] = useState();
      86              : 
      87            5 :     const name = (
      88            5 :         <Button variant="link"
      89            5 :             isInline id={comp.name}
      90            0 :             onClick={() => comp.installed ? launch(comp) : cockpit.location.go(comp.id)}>
      91            5 :             {comp.name}
      92            5 :         </Button>);
      93              : 
      94            5 :     let summary_or_progress;
      95            2 :     if (progress) {
      96            2 :         summary_or_progress = (
      97            2 :             <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
      98            2 :                 <span id={comp.name + "-progress"} className="progress-title-span">{progress_title}</span>
      99            2 :                 <ProgressBar data={progress} ariaLabelledBy={comp.name + "-progress"} />
     100            2 :             </Flex>);
     101            2 :     } else {
     102            0 :         if (error) {
     103            0 :             summary_or_progress = (
     104            0 :                 <div>
     105            0 :                     {comp.summary}
     106            0 :                     <Alert isInline variant='danger'
     107            0 :                         actionClose={<AlertActionCloseButton onClose={() => setError(null)} />}
     108            0 :                         title={error} />
     109            0 :                 </div>
     110              :             );
     111            0 :         } else {
     112            5 :             summary_or_progress = comp.summary;
     113            5 :         }
     114            5 :     }
     115              : 
     116            5 :     return (
     117            5 :         <DataListItem className="app-list" aria-labelledby={comp.name}>
     118            5 :             <DataListItemRow>
     119            5 :                 <DataListItemCells
     120            5 :                     dataListCells={[
     121            5 :                         <DataListCell isIcon key="icon">
     122            5 :                             <img src={icon_url(comp.icon)} alt={cockpit.format(_("icon of $0"), comp.name)} />
     123            5 :                         </DataListCell>,
     124            5 :                         <DataListCell width={1} key="app name">
     125            5 :                             {name}
     126            5 :                         </DataListCell>,
     127            5 :                         <DataListCell width={4} key="secondary content">
     128            5 :                             {summary_or_progress}
     129            5 :                         </DataListCell>,
     130            5 :                     ]}
     131            5 :                 />
     132            5 :                 <DataListAction aria-labelledby={comp.name} aria-label={_("Actions")}>
     133            5 :                     <ActionButton comp={comp} progress={progress} action={action} />
     134            5 :                 </DataListAction>
     135            5 :             </DataListItemRow>
     136            5 :         </DataListItem>
     137              :     );
     138            5 : };
     139              : 
     140            5 : export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, action }) => {
     141            5 :     const [progress, setProgress] = useState(false);
     142            5 :     const [dataPackagesInstalled, setDataPackagesInstalled] = useState(null);
     143            5 :     const comps = [];
     144            5 :     for (const id in metainfo_db.components)
     145            5 :         comps.push(metainfo_db.components[id]);
     146            3 :     comps.sort((a, b) => a.name.localeCompare(b.name));
     147              : 
     148            5 :     async function check_missing_data(packages) {
     149            5 :         try {
     150            5 :             const packagemanager = await getPackageManager();
     151            3 :             setDataPackagesInstalled(await packagemanager.is_installed(packages));
     152            0 :         } catch (e) {
     153            0 :             console.warn("Failed to check missing AppStream metadata packages:", e.toString());
     154            0 :         }
     155            5 :     }
     156              : 
     157            5 :     async function get_packages() {
     158            5 :         const os_release = await read_os_release();
     159              :         // ID is a single value, ID_LIKE is a list
     160            5 :         const os_list = [os_release?.ID, ...(os_release?.ID_LIKE || "").split(/\s+/)];
     161            5 :         const configPackages = get_manifest_config_matchlist('apps', 'appstream_config_packages', [], os_list);
     162            5 :         const dataPackages = get_manifest_config_matchlist('apps', 'appstream_data_packages', [], os_list);
     163            5 :         return [configPackages, dataPackages];
     164            5 :     }
     165              : 
     166            5 :     useInit(async () => {
     167            5 :         const [config, data] = await get_packages();
     168            3 :         await check_missing_data([...config, ...data]);
     169            5 :     });
     170              : 
     171            3 :     async function refresh() {
     172            3 :         const [configPackages, dataPackages] = await get_packages();
     173            3 :         try {
     174            3 :             await refresh_appstream_metadata(metainfo_db.origin_files,
     175            3 :                                              configPackages,
     176            3 :                                              dataPackages,
     177            3 :                                              setProgress);
     178            0 :         } catch (e) {
     179            0 :             show_error(e);
     180            0 :         } finally {
     181            3 :             await check_missing_data([...dataPackages, ...configPackages]);
     182            3 :             setProgress(false);
     183            3 :         }
     184            3 :     }
     185              : 
     186            5 :     let refresh_progress;
     187            5 :     let refresh_button;
     188            5 :     let tbody;
     189            3 :     if (progress) {
     190            3 :         refresh_progress = <ProgressBar id="refresh-progress" size="sm" data={progress} />;
     191            3 :         refresh_button = <CancelButton data={progress} />;
     192            3 :     } else {
     193            5 :         refresh_progress = null;
     194            5 :         refresh_button = (
     195            5 :             <Button icon={<RebootingIcon />} variant="secondary" onClick={refresh} id="refresh" aria-label={_("Update package information")} />
     196              :         );
     197            5 :     }
     198              : 
     199            5 :     if (comps.length) {
     200            5 :         tbody = comps.map(c => <ApplicationRow comp={c} key={c.id}
     201            5 :                                                progress={appProgress[c.id]}
     202            5 :                                                progress_title={appProgressTitle[c.id]}
     203            5 :                                                action={action} />);
     204            5 :     }
     205              : 
     206            2 :     const data_missing_msg = (dataPackagesInstalled == false && !refresh_progress)
     207            2 :         ? _("Application information is missing")
     208            5 :         : null;
     209              : 
     210            5 :     return (
     211            5 :         <Page id="list-page" data-packages-checked={dataPackagesInstalled !== null} className="pf-m-no-sidebar">
     212            5 :             <PageSection hasBodyWrapper={false}>
     213            5 :                 <Flex alignItems={{ default: 'alignItemsCenter' }}>
     214            5 :                     <h2 className="pf-v6-u-font-size-3xl">{_("Applications")}</h2>
     215            5 :                     <FlexItem align={{ default: 'alignRight' }}>
     216            5 :                         <Flex alignItems={{ default: 'alignItemsCenter' }} spacer={{ default: 'spacerXs' }}>
     217            5 :                             <FlexItem>
     218            5 :                                 {refresh_progress}
     219            5 :                             </FlexItem>
     220            5 :                             <FlexItem>
     221            5 :                                 {refresh_button}
     222            5 :                             </FlexItem>
     223            5 :                         </Flex>
     224            5 :                     </FlexItem>
     225            5 :                 </Flex>
     226            5 :             </PageSection>
     227            5 :             {comps.length == 0
     228            4 :                 ? <EmptyStatePanel title={ _("No applications installed or available.") }
     229            4 :                                    paragraph={data_missing_msg}
     230            2 :                                    action={ data_missing_msg && _("Install application information")} onAction={refresh} />
     231            5 :                 : <PageSection hasBodyWrapper={false}>
     232            5 :                     <Stack hasGutter>
     233            4 :                         {!progress && data_missing_msg &&
     234            2 :                             <StackItem key="missing-meta-alert">
     235            2 :                                 <Alert variant="warning" isInline title={data_missing_msg}
     236            2 :                                     actionLinks={ <AlertActionLink onClick={refresh}>{_("Install")}</AlertActionLink>} />
     237            2 :                             </StackItem>
     238              :                         }
     239            5 :                         <StackItem>
     240            5 :                             <DataList aria-label={_("Applications list")}>
     241            5 :                                 { tbody }
     242            5 :                             </DataList>
     243            5 :                         </StackItem>
     244            5 :                     </Stack>
     245            5 :                 </PageSection>
     246              :             }
     247            5 :         </Page>
     248              :     );
     249            5 : };
        

Generated by: LCOV version 2.0-1