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 from "react";
8 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
9 : import { Progress, ProgressMeasureLocation } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
10 : import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
11 : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
12 : import { show_modal_dialog } from "cockpit-components-dialog.jsx";
13 :
14 6 : const _ = cockpit.gettext;
15 :
16 6 : export function debug(...args: unknown[]) {
17 1 : if (window.debugging == "all" || window.debugging?.includes("apps")) {
18 1 : console.debug("apps:", ...args);
19 1 : }
20 6 : }
21 :
22 6 : export function icon_url(path_or_url: string): string {
23 6 : if (!path_or_url)
24 3 : return "default.png";
25 :
26 5 : if (path_or_url[0] != '/')
27 1 : return path_or_url;
28 :
29 : interface QueryObj {
30 : payload: string,
31 : binary: "raw",
32 : path: string,
33 : external?: { [key: string]: string }
34 : }
35 :
36 5 : const queryobj: QueryObj = {
37 5 : payload: "fsread1",
38 5 : binary: "raw",
39 5 : path: path_or_url,
40 5 : };
41 :
42 1 : if (path_or_url.endsWith(".svg")) {
43 1 : queryobj.external = { "content-type": "image/svg+xml" };
44 1 : }
45 :
46 5 : const prefix = (new URL(cockpit.transport.uri("channel/" + cockpit.transport.csrf_token))).pathname;
47 5 : const query = window.btoa(JSON.stringify(queryobj));
48 5 : return prefix + '?' + query;
49 6 : }
50 :
51 : export interface JobProgress {
52 : percentage: number,
53 : waiting: boolean,
54 : cancel: React.MouseEventHandler,
55 : }
56 :
57 3 : export const ProgressBar = ({ size, data, ariaLabelledBy }: {
58 : size?: "lg" | "md" | "sm",
59 : data: JobProgress,
60 : ariaLabelledBy?: string,
61 3 : }) => {
62 0 : if (data.waiting) {
63 0 : return (<Split>
64 0 : <SplitItem className="progress-title" isFilled>
65 0 : {_("Waiting for other programs to finish using the package manager...")}
66 0 : </SplitItem>
67 0 : <SplitItem>
68 0 : <Spinner size="md" />
69 0 : </SplitItem>
70 0 : </Split>);
71 0 : } else {
72 3 : return <Progress className="progress-bar" value={data.percentage} size={size} measureLocation={ProgressMeasureLocation.inside} aria-labelledby={ariaLabelledBy} />;
73 3 : }
74 3 : };
75 :
76 3 : export const CancelButton = ({ data }: { data: JobProgress }) => (
77 3 : <Button variant="secondary" isDisabled={!data.cancel} onClick={data.cancel}>
78 3 : {_("Cancel")}
79 3 : </Button>);
80 :
81 : type ProgressCallback = (data: JobProgress) => void
82 :
83 6 : export class ProgressReporter {
84 : base: number;
85 : range: number;
86 : percentage: number;
87 : callback: ProgressCallback;
88 :
89 3 : constructor(base: number, range: number, callback: ProgressCallback) {
90 3 : this.base = base;
91 3 : this.range = range;
92 3 : this.callback = callback;
93 3 : this.percentage = 0;
94 3 : this.progress_reporter = this.progress_reporter.bind(this);
95 3 : }
96 :
97 3 : progress_reporter(data: JobProgress) {
98 3 : if (data.percentage >= 0) {
99 3 : const newPercentage = this.base + data.percentage / 100 * this.range;
100 : // PackageKit with Apt backend reports wrong percentages https://github.com/PackageKit/PackageKit/issues/516
101 : // Double check here that we have an increasing only progress value
102 3 : if (this.percentage == undefined || newPercentage >= this.percentage)
103 3 : this.percentage = newPercentage;
104 3 : }
105 3 : this.callback({ ...data, percentage: this.percentage });
106 3 : }
107 6 : }
108 :
109 : // ex is a PackageKit error; requires typing pkg/lib/packagekit.js first
110 : // eslint-disable-next-line @typescript-eslint/no-explicit-any
111 0 : export const show_error = (ex: any) => {
112 0 : if (ex.code == "cancelled")
113 0 : return;
114 :
115 0 : if (ex.code == "not-found")
116 0 : ex.detail = _("No installation package found for this application.");
117 :
118 0 : show_modal_dialog(
119 0 : {
120 0 : title: _("Error"),
121 0 : body: (
122 0 : <p>{typeof ex == 'string' ? ex : (ex.detail || ex.message)}</p>
123 : )
124 0 : },
125 0 : {
126 0 : cancel_button: { text: _("Close"), variant: "secondary" },
127 0 : actions: []
128 0 : });
129 0 : };
130 :
131 : export interface Launchable {
132 : type: string,
133 : name: string,
134 : }
135 :
136 : // see pkg/apps/watch-appstream.py
137 : export interface Component {
138 : id: string,
139 : pkgname: string,
140 : name: string,
141 : summary: string,
142 : description: string,
143 : icon: string, // this is a path
144 : screenshots: { full: string }[],
145 : launchables: Launchable[],
146 : urls: { type: string, link: string }[],
147 : installed?: boolean,
148 : }
149 :
150 0 : export const launch = (comp: Component) => {
151 0 : for (let i = 0; i < comp.launchables.length; i++) {
152 0 : if (comp.launchables[i].type == "cockpit-manifest") {
153 0 : debug("launching", comp.launchables[i].name, "in component", JSON.stringify(comp));
154 0 : cockpit.jump([comp.launchables[i].name]);
155 0 : return;
156 0 : }
157 0 : }
158 0 : };
159 :
160 2 : export function reload_bridge_packages() {
161 2 : return cockpit.dbus(null, { bus: "internal" }).call("/packages", "cockpit.Packages", "Reload", []);
162 2 : }
|