Line data Source code
1 : /*
2 : * Copyright (C) 2024 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 341 : 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 359 : export function encode_location(location: Partial<Location>): string {
18 359 : const shell_embedded = window.location.pathname.indexOf(".html") !== -1;
19 359 : if (shell_embedded)
20 84 : return window.location.toString();
21 :
22 359 : const path = [];
23 359 : if (location.host && location.host !== "localhost")
24 94 : path.push("@" + location.host);
25 359 : if (location.path)
26 359 : path.push(...location.path.split("/"));
27 359 : let string = cockpit.location.encode(path, undefined, true);
28 260 : if (location.hash && location.hash !== "/")
29 235 : string += "#" + location.hash;
30 359 : return string;
31 359 : }
32 :
33 : /* Decodes navigate state from a string */
34 365 : export function decode_location(string: string): Location {
35 365 : let hash = "";
36 365 : const pos = string.indexOf("#");
37 241 : if (pos !== -1) {
38 241 : hash = string.substring(pos + 1);
39 241 : string = string.substring(0, pos);
40 241 : }
41 365 : if (string[0] != '/')
42 90 : string = "/" + string;
43 365 : const path = cockpit.location.decode(string);
44 365 : let host;
45 365 : if (path[0] && path[0][0] == "@")
46 121 : host = (path.shift() as string).substring(1);
47 : else
48 365 : host = "localhost";
49 365 : if (path.length && path[path.length - 1] == "index")
50 90 : path.pop();
51 365 : return { host, path: path.join("/"), hash };
52 365 : }
53 :
54 365 : export function decode_window_location(): Location {
55 365 : const shell_embedded = window.location.pathname.indexOf(".html") !== -1;
56 :
57 365 : if (shell_embedded)
58 90 : return decode_location("/" + window.location.hash);
59 : else
60 365 : return decode_location(window.location.pathname + window.location.hash);
61 365 : }
62 :
63 359 : export function replace_window_location(location: Partial<Location>): void {
64 359 : window.history.replaceState(null, "", encode_location(location));
65 359 : }
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 341 : export class CompiledComponents {
82 : manifests: Manifests;
83 365 : items: { [path: string] : ManifestItem; } = { };
84 :
85 365 : constructor(manifests?: Manifests) {
86 100 : this.manifests = manifests || { };
87 365 : }
88 :
89 365 : load(section: string, getter: (man: Manifest) => ManifestSection | undefined): void {
90 365 : Object.entries(this.manifests).forEach(([name, manifest]) => {
91 365 : const manifest_section = getter(manifest) || {};
92 365 : Object.entries(manifest_section).forEach(([prop, info]) => {
93 365 : const item: ManifestItem = {
94 365 : path: "", // set below
95 365 : hash: "", // set below
96 365 : section,
97 90 : label: info.label ? cockpit.gettext(info.label) : prop,
98 365 : order: info.order === undefined ? 1000 : info.order,
99 365 : docs: info.docs,
100 365 : keywords: info.keywords || [{ matches: [], weight: 3, translate: true, goto: undefined }],
101 365 : };
102 :
103 : // Always first keyword should be page name
104 365 : const page_name = item.label.toLowerCase();
105 365 : if (item.keywords[0].matches.indexOf(page_name) < 0)
106 365 : item.keywords[0].matches.unshift(page_name);
107 :
108 : // Keywords from manifest have different defaults than are usual
109 365 : item.keywords.forEach(i => {
110 365 : i.weight = i.weight || 3;
111 365 : i.translate = i.translate === undefined ? true : i.translate;
112 365 : });
113 :
114 365 : if (info.path)
115 95 : item.path = info.path.replace(/\.html$/, "");
116 : else
117 365 : item.path = name + "/" + prop;
118 :
119 : /* Split out any hash in the path */
120 365 : const pos = item.path.indexOf("#");
121 91 : if (pos !== -1) {
122 91 : item.hash = item.path.substring(pos + 1);
123 91 : item.path = item.path.substring(0, pos);
124 91 : }
125 :
126 : /* Fix component for compatibility and normalize it */
127 365 : if (item.path.indexOf("/") === -1)
128 95 : item.path = name + "/" + item.path;
129 365 : if (item.path.slice(-6) == "/index")
130 365 : item.path = item.path.slice(0, -6);
131 365 : this.items[item.path] = item;
132 365 : });
133 365 : });
134 365 : }
135 :
136 338 : ordered(section: string): ManifestItem[] {
137 338 : const list: ManifestItem[] = [];
138 338 : for (const x in this.items) {
139 338 : if (!section || this.items[x].section === section)
140 338 : list.push(this.items[x]);
141 338 : }
142 338 : list.sort(function(a, b) {
143 338 : let ret = a.order - b.order;
144 338 : if (ret === 0)
145 338 : ret = a.label.localeCompare(b.label);
146 338 : return ret;
147 338 : });
148 338 : return list;
149 338 : }
150 :
151 365 : find_path_item(path: string): ManifestItem {
152 365 : let component = path;
153 363 : if (this.items[path] === undefined) {
154 363 : let s = path;
155 359 : while (s && this.items[s] === undefined)
156 359 : s = s.substring(0, s.lastIndexOf("/"));
157 363 : component = s;
158 363 : }
159 :
160 : // Still don't know where it comes from, check for parent
161 110 : if (!component) {
162 110 : const comp = this.manifests[path];
163 100 : if (comp && comp.parent && comp.parent.component)
164 100 : component = comp.parent.component;
165 110 : }
166 :
167 365 : const item = this.items[component];
168 365 : if (item)
169 365 : 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 100 : return {
175 100 : path,
176 100 : label: path,
177 100 : section: "menu",
178 100 : hash: "",
179 100 : order: 3,
180 100 : keywords: [],
181 100 : docs: undefined
182 100 : };
183 365 : }
184 :
185 365 : find_path_manifest(path: string): Manifest {
186 365 : const parts = path.split("/");
187 365 : const pkg = parts[0];
188 :
189 100 : return (this.manifests[pkg]) || { };
190 365 : }
191 341 : }
192 :
193 365 : export function compile_manifests(manifests?: Manifests): CompiledComponents {
194 365 : const compiled = new CompiledComponents(manifests);
195 365 : compiled.load("tools", m => m.tools);
196 365 : compiled.load("dashboard", m => m.dashboard);
197 365 : compiled.load("menu", m => m.menu);
198 365 : return compiled;
199 365 : }
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 338 : export function compute_frame_url(machine: Machine, path: string): string {
209 338 : let base;
210 338 : let checksum;
211 338 : if (machine.manifests && machine.manifests[".checksum"])
212 63 : checksum = "$" + machine.manifests[".checksum"];
213 : else
214 338 : checksum = machine.checksum;
215 :
216 63 : if (checksum && checksum == component_checksum(machine, path)) {
217 63 : if (machine.connection_string === "localhost")
218 63 : base = "..";
219 : else
220 63 : base = "../../" + checksum;
221 63 : } 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 338 : base = "../../@" + machine.connection_string;
233 338 : }
234 :
235 338 : let url = base + "/" + path;
236 338 : if (path.indexOf("/") === -1)
237 282 : url += "/index";
238 338 : url += ".html";
239 :
240 338 : return url;
241 338 : }
|