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