LCOV - code coverage report
Current view: top level - pkg/shell - util.tsx Coverage Total Hit
Test: cockpit Lines: 96.3 % 164 158
Test Date: 2026-06-25 09:20:42

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2024 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          339 : import cockpit from "cockpit";
       7              : 
       8              : import { ManifestKeyword, ManifestDocs, ManifestSection, Manifest, Manifests } from "./manifests";
       9              : import { Machine } from "./machines/machines";
      10              : 
      11              : export interface Location {
      12              :     host: string;
      13              :     path: string;
      14              :     hash: string;
      15              : }
      16              : 
      17          357 : export function encode_location(location: Partial<Location>): string {
      18          357 :     const shell_embedded = window.location.pathname.indexOf(".html") !== -1;
      19          357 :     if (shell_embedded)
      20           83 :         return window.location.toString();
      21              : 
      22          357 :     const path = [];
      23          357 :     if (location.host && location.host !== "localhost")
      24           93 :         path.push("@" + location.host);
      25          357 :     if (location.path)
      26          357 :         path.push(...location.path.split("/"));
      27          357 :     let string = cockpit.location.encode(path, undefined, true);
      28          258 :     if (location.hash && location.hash !== "/")
      29          233 :         string += "#" + location.hash;
      30          357 :     return string;
      31          357 : }
      32              : 
      33              : /* Decodes navigate state from a string */
      34          363 : export function decode_location(string: string): Location {
      35          363 :     let hash = "";
      36          363 :     const pos = string.indexOf("#");
      37          239 :     if (pos !== -1) {
      38          239 :         hash = string.substring(pos + 1);
      39          239 :         string = string.substring(0, pos);
      40          239 :     }
      41          363 :     if (string[0] != '/')
      42           89 :         string = "/" + string;
      43          363 :     const path = cockpit.location.decode(string);
      44          363 :     let host;
      45          363 :     if (path[0] && path[0][0] == "@")
      46          120 :         host = (path.shift() as string).substring(1);
      47              :     else
      48          363 :         host = "localhost";
      49          363 :     if (path.length && path[path.length - 1] == "index")
      50           89 :         path.pop();
      51          363 :     return { host, path: path.join("/"), hash };
      52          363 : }
      53              : 
      54          363 : export function decode_window_location(): Location {
      55          363 :     const shell_embedded = window.location.pathname.indexOf(".html") !== -1;
      56              : 
      57          363 :     if (shell_embedded)
      58           89 :         return decode_location("/" + window.location.hash);
      59              :     else
      60          363 :         return decode_location(window.location.pathname + window.location.hash);
      61          363 : }
      62              : 
      63          357 : export function replace_window_location(location: Partial<Location>): void {
      64          357 :     window.history.replaceState(null, "", encode_location(location));
      65          357 : }
      66              : 
      67           57 : export function push_window_location(location: Partial<Location>): void {
      68           57 :     window.history.pushState(null, "", encode_location(location));
      69           57 : }
      70              : 
      71              : export interface ManifestItem {
      72              :     path: string;
      73              :     hash: string;
      74              :     section: string;
      75              :     label: string;
      76              :     order: number;
      77              :     docs: ManifestDocs[] | undefined;
      78              :     keywords: ManifestKeyword[];
      79              : }
      80              : 
      81          339 : export class CompiledComponents {
      82              :     manifests: Manifests;
      83          363 :     items: { [path: string] : ManifestItem; } = { };
      84              : 
      85          363 :     constructor(manifests?: Manifests) {
      86           99 :         this.manifests = manifests || { };
      87          363 :     }
      88              : 
      89          363 :     load(section: string, getter: (man: Manifest) => ManifestSection | undefined): void {
      90          363 :         Object.entries(this.manifests).forEach(([name, manifest]) => {
      91          363 :             const manifest_section = getter(manifest) || {};
      92          363 :             Object.entries(manifest_section).forEach(([prop, info]) => {
      93          363 :                 const item: ManifestItem = {
      94          363 :                     path: "", // set below
      95          363 :                     hash: "", // set below
      96          363 :                     section,
      97           89 :                     label: info.label ? cockpit.gettext(info.label) : prop,
      98          363 :                     order: info.order === undefined ? 1000 : info.order,
      99          363 :                     docs: info.docs,
     100          363 :                     keywords: info.keywords || [{ matches: [], weight: 3, translate: true, goto: undefined }],
     101          363 :                 };
     102              : 
     103              :                 // Always first keyword should be page name
     104          363 :                 const page_name = item.label.toLowerCase();
     105          363 :                 if (item.keywords[0].matches.indexOf(page_name) < 0)
     106          363 :                     item.keywords[0].matches.unshift(page_name);
     107              : 
     108              :                 // Keywords from manifest have different defaults than are usual
     109          363 :                 item.keywords.forEach(i => {
     110          363 :                     i.weight = i.weight || 3;
     111          363 :                     i.translate = i.translate === undefined ? true : i.translate;
     112          363 :                 });
     113              : 
     114          363 :                 if (info.path)
     115           94 :                     item.path = info.path.replace(/\.html$/, "");
     116              :                 else
     117          363 :                     item.path = name + "/" + prop;
     118              : 
     119              :                 /* Split out any hash in the path */
     120          363 :                 const pos = item.path.indexOf("#");
     121           90 :                 if (pos !== -1) {
     122           90 :                     item.hash = item.path.substring(pos + 1);
     123           90 :                     item.path = item.path.substring(0, pos);
     124           90 :                 }
     125              : 
     126              :                 /* Fix component for compatibility and normalize it */
     127          363 :                 if (item.path.indexOf("/") === -1)
     128           94 :                     item.path = name + "/" + item.path;
     129          363 :                 if (item.path.slice(-6) == "/index")
     130          363 :                     item.path = item.path.slice(0, -6);
     131          363 :                 this.items[item.path] = item;
     132          363 :             });
     133          363 :         });
     134          363 :     }
     135              : 
     136          336 :     ordered(section: string): ManifestItem[] {
     137          336 :         const list: ManifestItem[] = [];
     138          336 :         for (const x in this.items) {
     139          336 :             if (!section || this.items[x].section === section)
     140          336 :                 list.push(this.items[x]);
     141          336 :         }
     142          336 :         list.sort(function(a, b) {
     143          336 :             let ret = a.order - b.order;
     144          336 :             if (ret === 0)
     145          336 :                 ret = a.label.localeCompare(b.label);
     146          336 :             return ret;
     147          336 :         });
     148          336 :         return list;
     149          336 :     }
     150              : 
     151          363 :     find_path_item(path: string): ManifestItem {
     152          363 :         let component = path;
     153          361 :         if (this.items[path] === undefined) {
     154          361 :             let s = path;
     155          357 :             while (s && this.items[s] === undefined)
     156          357 :                 s = s.substring(0, s.lastIndexOf("/"));
     157          361 :             component = s;
     158          361 :         }
     159              : 
     160              :         // Still don't know where it comes from, check for parent
     161          109 :         if (!component) {
     162          109 :             const comp = this.manifests[path];
     163           99 :             if (comp && comp.parent && comp.parent.component)
     164           99 :                 component = comp.parent.component;
     165          109 :         }
     166              : 
     167          363 :         const item = this.items[component];
     168          363 :         if (item)
     169          363 :             return item;
     170              : 
     171              :         // Return something that can be used when the user navigates
     172              :         // to a URL for a non-existing component.
     173              : 
     174           99 :         return {
     175           99 :             path,
     176           99 :             label: path,
     177           99 :             section: "menu",
     178           99 :             hash: "",
     179           99 :             order: 3,
     180           99 :             keywords: [],
     181           99 :             docs: undefined
     182           99 :         };
     183          363 :     }
     184              : 
     185          363 :     find_path_manifest(path: string): Manifest {
     186          363 :         const parts = path.split("/");
     187          363 :         const pkg = parts[0];
     188              : 
     189           99 :         return (this.manifests[pkg]) || { };
     190          363 :     }
     191          339 : }
     192              : 
     193          363 : export function compile_manifests(manifests?: Manifests): CompiledComponents {
     194          363 :     const compiled = new CompiledComponents(manifests);
     195          363 :     compiled.load("tools", m => m.tools);
     196          363 :     compiled.load("dashboard", m => m.dashboard);
     197          363 :     compiled.load("menu", m => m.menu);
     198          363 :     return compiled;
     199          363 : }
     200              : 
     201            0 : function component_checksum(machine: Machine, path: string): string | undefined {
     202            0 :     const parts = path.split("/");
     203            0 :     const pkg = parts[0];
     204            0 :     if (machine.manifests && machine.manifests[pkg] && machine.manifests[pkg][".checksum"])
     205            0 :         return "$" + machine.manifests[pkg][".checksum"];
     206            0 : }
     207              : 
     208          336 : export function compute_frame_url(machine: Machine, path: string): string {
     209          336 :     let base;
     210          336 :     let checksum;
     211          336 :     if (machine.manifests && machine.manifests[".checksum"])
     212           62 :         checksum = "$" + machine.manifests[".checksum"];
     213              :     else
     214          336 :         checksum = machine.checksum;
     215              : 
     216           62 :     if (checksum && checksum == component_checksum(machine, path)) {
     217           62 :         if (machine.connection_string === "localhost")
     218           62 :             base = "..";
     219              :         else
     220           62 :             base = "../../" + checksum;
     221           62 :     } else {
     222              :         /* If we don't have any checksums, or if the component specifies a different
     223              :            checksum than the machine, load it via a non-caching @<host> path.  This
     224              :            makes sure that we get the right files, and also that we don't poisen the
     225              :            cache with wrong files.
     226              : 
     227              :            We can't use a $<component-checksum> path since cockpit-ws only knows how to
     228              :            route the machine checksum.
     229              : 
     230              :            TODO - make it possible to use $<component-checksum>.
     231              :         */
     232          336 :         base = "../../@" + machine.connection_string;
     233          336 :     }
     234              : 
     235          336 :     let url = base + "/" + path;
     236          336 :     if (path.indexOf("/") === -1)
     237          280 :         url += "/index";
     238          336 :     url += ".html";
     239              : 
     240          336 :     return url;
     241          336 : }
        

Generated by: LCOV version 2.0-1