github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/static/command-help/command-help.ts (about) 1 import dialogPolyfill from "dialog-polyfill"; 2 import {Command, Help, PluginHelp} from "../api/help"; 3 import {showToast} from "../common/common"; 4 import {getParameterByName} from '../common/urls'; 5 6 declare const allHelp: Help; 7 8 function redrawOptions(): void { 9 const rs = allHelp.AllRepos.sort(); 10 const sel = document.getElementById("repo") as HTMLSelectElement; 11 while (sel.length > 1) { 12 sel.removeChild(sel.lastChild); 13 } 14 const param = getParameterByName("repo"); 15 rs.forEach((opt) => { 16 const o = document.createElement("option"); 17 o.text = opt; 18 o.selected = !!(param && opt === param); 19 sel.appendChild(o); 20 }); 21 } 22 23 window.onload = (): void => { 24 // set dropdown based on options from query string 25 const hash = window.location.hash; 26 redrawOptions(); 27 redraw(); 28 29 // Register dialog 30 /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ 31 const dialog = document.querySelector('dialog') as HTMLDialogElement; 32 dialogPolyfill.registerDialog(dialog); 33 dialog.querySelector('.close')!.addEventListener('click', () => { 34 dialog.close(); 35 }); 36 37 if (hash !== "") { 38 const el = document.body.querySelector(hash); 39 const mainContainer = document.body.querySelector(".mdl-layout__content"); 40 if (el && mainContainer) { 41 setTimeout(() => { 42 mainContainer.scrollTop = el.getBoundingClientRect().top; 43 window.location.hash = hash; 44 }, 32); 45 /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ 46 (el.querySelector(".mdl-button--primary") as HTMLButtonElement).click(); 47 } 48 } 49 }; 50 51 function selectionText(sel: HTMLSelectElement): string { 52 return sel.selectedIndex === 0 ? "" : sel.options[sel.selectedIndex].text; 53 } 54 55 /** 56 * Takes an org/repo string and a repo to plugin map and returns the plugins 57 * that apply to the repo. 58 * 59 * @param repoSel repo name 60 * @param repoPlugins maps plugin name to plugin 61 */ 62 function applicablePlugins(repoSel: string, repoPlugins: {[key: string]: string[]}): string[] { 63 if (repoSel === "") { 64 const all = repoPlugins[""]; 65 if (all) { 66 return all.sort(); 67 } 68 return []; 69 } 70 const parts = repoSel.split("/"); 71 const byOrg = repoPlugins[parts[0]]; 72 let plugins: string[] = []; 73 if (byOrg && byOrg !== []) { 74 plugins = plugins.concat(byOrg); 75 } 76 const pluginNames = repoPlugins[repoSel]; 77 if (pluginNames) { 78 pluginNames.forEach((pluginName) => { 79 if (!plugins.includes(pluginName)) { 80 plugins.push(pluginName); 81 } 82 }); 83 } 84 return plugins.sort(); 85 } 86 87 /** 88 * Returns a normal cell for the command row. 89 * 90 * @param data content of the cell 91 * @param styles a list of styles applied to the cell. 92 * @param noWrap true if the content of the cell should be wrap. 93 */ 94 function createCommandCell(data: string | string[], styles: string[] = [], noWrap = false): HTMLTableDataCellElement { 95 const cell = document.createElement("td"); 96 cell.classList.add("mdl-data-table__cell--non-numeric"); 97 if (!noWrap) { 98 cell.classList.add("table-cell"); 99 } 100 let content: HTMLElement; 101 if (Array.isArray(data)) { 102 content = document.createElement("ul"); 103 content.classList.add("command-example-list"); 104 105 data.forEach((item) => { 106 const itemContainer = document.createElement("li"); 107 const span = document.createElement("span"); 108 span.innerHTML = item; 109 span.classList.add(...styles); 110 itemContainer.appendChild(span); 111 content.appendChild(itemContainer); 112 }); 113 } else { 114 content = document.createElement("div"); 115 content.classList.add(...styles); 116 content.innerHTML = data; 117 } 118 119 cell.appendChild(content); 120 121 return cell; 122 } 123 124 /** 125 * Returns an icon element. 126 * 127 * @param no no. command 128 * @param iconString icon name 129 * @param styles list of styles of the icon 130 * @param tooltip tooltip string 131 * @param isButton true if icon is a button 132 */ 133 function createIcon(no: number, iconString: string, styles: string[], tooltip: string, isButton?: false): HTMLDivElement; 134 function createIcon(no: number, iconString: string, styles: string[], tooltip: string, isButton?: true): HTMLButtonElement; 135 function createIcon(no: number, iconString: string, styles: string[] = [], tooltip = "", isButton = false) { 136 const icon = document.createElement("i"); 137 icon.id = `icon-${iconString}-${no}`; 138 icon.classList.add("material-icons"); 139 icon.classList.add(...styles); 140 icon.innerHTML = iconString; 141 142 const container = isButton ? document.createElement("button") : document.createElement("div"); 143 container.appendChild(icon); 144 if (isButton) { 145 container.classList.add(...["mdl-button", "mdl-js-button", "mdl-button--icon"]); 146 } 147 148 if (tooltip === "") { 149 return container; 150 } 151 152 const tooltipEl = document.createElement("div"); 153 tooltipEl.setAttribute("for", icon.id); 154 tooltipEl.classList.add("mdl-tooltip"); 155 tooltipEl.innerHTML = tooltip; 156 container.appendChild(tooltipEl); 157 158 return container; 159 } 160 161 /** 162 * Returns the feature cell for the command row. 163 * 164 * @param isFeatured true if the command is featured. 165 * @param isExternal true if the command is external. 166 * @param no no. command. 167 */ 168 function commandStatus(isFeatured: boolean, isExternal: boolean, no: number): HTMLTableDataCellElement { 169 const status = document.createElement("td"); 170 status.classList.add("mdl-data-table__cell--non-numeric"); 171 if (isFeatured) { 172 status.appendChild( 173 createIcon(no, "stars", ["featured-icon"], "Featured command")); 174 } 175 if (isExternal) { 176 status.appendChild( 177 createIcon(no, "open_in_new", ["external-icon"], "External plugin")); 178 } 179 return status; 180 } 181 182 /** 183 * Returns a section to the content of the dialog 184 * 185 * @param title title of the section 186 * @param body body of the section 187 */ 188 function addDialogSection(title: string, body: string): HTMLElement { 189 const container = document.createElement("div"); 190 const sectionTitle = document.createElement("h5"); 191 const sectionBody = document.createElement("p"); 192 193 sectionBody.classList.add("dialog-section-body"); 194 sectionBody.innerHTML = body; 195 196 sectionTitle.classList.add("dialog-section-title"); 197 sectionTitle.innerHTML = title; 198 199 container.classList.add("dialog-section"); 200 container.appendChild(sectionTitle); 201 container.appendChild(sectionBody); 202 203 return container; 204 } 205 206 /** 207 * Returns a cell for the Plugin column. 208 * 209 * @param repo repo name 210 * @param pluginName plugin name. 211 * @param plugin the plugin to which the command belong to 212 */ 213 function createPluginCell(repo: string, pluginName: string, plugin: PluginHelp): HTMLTableDataCellElement { 214 const pluginCell = document.createElement("td"); 215 const button = document.createElement("button"); 216 pluginCell.classList.add("mdl-data-table__cell--non-numeric"); 217 button.classList.add("mdl-button", "mdl-button--js", "mdl-button--primary"); 218 button.innerHTML = pluginName; 219 220 // Attach Event Handlers. 221 const dialog = document.querySelector('dialog') as HTMLDialogElement; 222 button.addEventListener('click', () => { 223 const title = dialog.querySelector(".mdl-dialog__title")!; 224 const content = dialog.querySelector(".mdl-dialog__content")!; 225 226 while (content.firstChild) { 227 content.removeChild(content.firstChild); 228 } 229 230 title.innerHTML = pluginName; 231 if (plugin.Description) { 232 content.appendChild(addDialogSection("Description", plugin.Description)); 233 } 234 if (plugin.Events) { 235 const sectionContent = `[${plugin.Events.sort().join(", ")}]`; 236 content.appendChild(addDialogSection("Events handled", sectionContent)); 237 } 238 if (plugin.Config) { 239 const sectionContent = plugin.Config ? plugin.Config[repo] : ""; 240 const sectionTitle = 241 repo === "" ? "Configuration(global)" : `Configuration(${repo})`; 242 if (sectionContent && sectionContent !== "") { 243 content.appendChild(addDialogSection(sectionTitle, sectionContent)); 244 } 245 } 246 dialog.showModal(); 247 }); 248 249 pluginCell.appendChild(button); 250 return pluginCell; 251 } 252 253 /** 254 * Creates a link that links to the command. 255 */ 256 function createCommandLink(name: string, no: number): HTMLTableDataCellElement { 257 const link = document.createElement("td"); 258 const iconButton = createIcon(no, "link", ["link-icon"], "", true); 259 260 iconButton.addEventListener("click", () => { 261 const tempInput = document.createElement("input"); 262 let url = window.location.href; 263 const hashIndex = url.indexOf("#"); 264 if (hashIndex !== -1) { 265 url = url.slice(0, hashIndex); 266 } 267 268 url += `#${ name}`; 269 tempInput.style.zIndex = "-99999"; 270 tempInput.style.background = "transparent"; 271 tempInput.value = url; 272 273 document.body.appendChild(tempInput); 274 tempInput.select(); 275 document.execCommand("copy"); 276 document.body.removeChild(tempInput); 277 278 showToast("Copied to clipboard"); 279 }); 280 281 link.appendChild(iconButton); 282 link.classList.add("mdl-data-table__cell--non-numeric"); 283 284 return link; 285 } 286 287 /** 288 * Creates a row for the Command table. 289 * 290 * @param repo repo name. 291 * @param pluginName plugin name. 292 * @param plugin the plugin to which the command belongs. 293 * @param command the command. 294 * @param isExternal true if the command belongs to an external 295 * @param no no. command 296 */ 297 function createCommandRow(repo: string, pluginName: string, plugin: PluginHelp, command: Command, isExternal: boolean, no: number): HTMLTableRowElement { 298 const row = document.createElement("tr"); 299 const name = extractCommandName(command.Examples[0]); 300 row.id = name; 301 302 row.appendChild(commandStatus(command.Featured, isExternal, no)); 303 row.appendChild(createCommandCell(command.Usage, ["command-usage"])); 304 row.appendChild( 305 createCommandCell(command.Examples, ["command-examples"], true)); 306 row.appendChild( 307 createCommandCell(command.Description, ["command-desc-text"])); 308 row.appendChild(createCommandCell(command.WhoCanUse, ["command-desc-text"])); 309 row.appendChild(createPluginCell(repo, pluginName, plugin)); 310 row.appendChild(createCommandLink(name, no)); 311 312 return row; 313 } 314 315 /** 316 * Redraw a plugin table. 317 * 318 * @param repo repo name. 319 * @param helpMap maps a plugin name to a plugin. 320 */ 321 function redrawHelpTable(repo: string, helpMap: Map<string, {isExternal: boolean, plugin: PluginHelp}>): void { 322 const table = document.getElementById("command-table")!; 323 const tableBody = document.querySelector("tbody")!; 324 if (helpMap.size === 0) { 325 table.style.display = "none"; 326 return; 327 } 328 table.style.display = "table"; 329 while (tableBody.childElementCount !== 0) { 330 tableBody.removeChild(tableBody.firstChild!); 331 } 332 const names = Array.from(helpMap.keys()); 333 const commandsWithPluginName: {pluginName: string, command: Command}[] = []; 334 for (const name of names) { 335 helpMap.get(name)!.plugin.Commands.forEach((command) => { 336 commandsWithPluginName.push({ 337 command, 338 pluginName: name, 339 }); 340 }); 341 } 342 commandsWithPluginName 343 .sort((command1, command2) => { 344 return command1.command.Featured ? -1 : command2.command.Featured ? 1 : 0; 345 }) 346 .forEach((command, index) => { 347 const pluginName = command.pluginName; 348 const {isExternal, plugin} = helpMap.get(pluginName)!; 349 const commandRow = createCommandRow( 350 repo, 351 pluginName, 352 plugin, 353 command.command, 354 isExternal, 355 index); 356 tableBody.appendChild(commandRow); 357 }); 358 } 359 360 /** 361 * Redraws the content of the page. 362 */ 363 function redraw(): void { 364 const repoSel = selectionText(document.getElementById("repo") as HTMLSelectElement); 365 if (window.history && window.history.replaceState !== undefined) { 366 if (repoSel !== "") { 367 history.replaceState(null, "", `/command-help?repo=${ 368 encodeURIComponent(repoSel)}`); 369 } else { 370 history.replaceState(null, "", "/command-help"); 371 } 372 } 373 redrawOptions(); 374 375 const pluginsWithCommands: Map<string, {isExternal: boolean, plugin: PluginHelp}> = new Map(); 376 applicablePlugins(repoSel, allHelp.RepoPlugins) 377 .forEach((name) => { 378 if (allHelp.PluginHelp[name] && allHelp.PluginHelp[name].Commands) { 379 pluginsWithCommands.set( 380 name, 381 { 382 isExternal: false, 383 plugin: allHelp.PluginHelp[name], 384 }); 385 } 386 }); 387 applicablePlugins(repoSel, allHelp.RepoExternalPlugins) 388 .forEach((name) => { 389 if (allHelp.ExternalPluginHelp[name] 390 && allHelp.ExternalPluginHelp[name].Commands) { 391 pluginsWithCommands.set( 392 name, 393 { 394 isExternal: true, 395 plugin: allHelp.ExternalPluginHelp[name], 396 }); 397 } 398 }); 399 redrawHelpTable(repoSel, pluginsWithCommands); 400 } 401 402 /** 403 * Extracts a command name from a command example. It takes the first example, 404 * with out the slash, as the name for the command. Also, any '-' character is 405 * replaced by '_' to make the name valid in the address. 406 */ 407 function extractCommandName(commandExample: string): string { 408 const command = commandExample.split(" "); 409 if (!command || command.length === 0) { 410 throw new Error("Cannot extract command name."); 411 } 412 return command[0].slice(1).split("-").join("_"); 413 } 414 415 // This is referenced by name in the HTML. 416 (window as any).redraw = redraw;