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