LCOV - code coverage report
Current view: top level - pkg/apps - utils.tsx Coverage Total Hit
Test: cockpit Lines: 60.7 % 89 54
Test Date: 2026-07-17 07:33:01

            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 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            7 : const _ = cockpit.gettext;
      15              : 
      16            7 : export function debug(...args: unknown[]) {
      17            1 :     if (window.debugging == "all" || window.debugging?.includes("apps")) {
      18            1 :         console.debug("apps:", ...args);
      19            1 :     }
      20            7 : }
      21              : 
      22            7 : export function icon_url(path_or_url: string): string {
      23            7 :     if (!path_or_url)
      24            4 :         return "default.png";
      25              : 
      26            6 :     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            6 :     const queryobj: QueryObj = {
      37            6 :         payload: "fsread1",
      38            6 :         binary: "raw",
      39            6 :         path: path_or_url,
      40            6 :     };
      41              : 
      42            1 :     if (path_or_url.endsWith(".svg")) {
      43            1 :         queryobj.external = { "content-type": "image/svg+xml" };
      44            1 :     }
      45              : 
      46            6 :     const prefix = (new URL(cockpit.transport.uri("channel/" + cockpit.transport.csrf_token))).pathname;
      47            6 :     const query = window.btoa(JSON.stringify(queryobj));
      48            6 :     return prefix + '?' + query;
      49            7 : }
      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            7 : 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            7 : }
     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 : }
        

Generated by: LCOV version 2.0-1