github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/deck/static/command-help/command-help.ts (about) 1 import "dialog-polyfill"; 2 import {Command, Help, PluginHelp} from "../api/help"; 3 4 declare const allHelp: Help; 5 6 declare const dialogPolyfill: { 7 registerDialog(element: HTMLDialogElement): void; 8 }; 9 10 function getParameterByName(name: string): string | null { // http://stackoverflow.com/a/5158301/3694 11 const match = new RegExp('[?&]' + name + '=([^&/]*)').exec(window.location.search); 12 return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); 13 } 14 15 function redrawOptions(): void { 16 const rs = allHelp.AllRepos.sort(); 17 const sel = document.getElementById("repo") as HTMLSelectElement; 18 while (sel.length > 1) { 19 sel.removeChild(sel.lastChild!); 20 } 21 const param = getParameterByName("repo"); 22 rs.forEach((opt) => { 23 const o = document.createElement("option"); 24 o.text = opt; 25 o.selected = !!(param && opt === param); 26 sel.appendChild(o); 27 }); 28 } 29 30 window.onload = function (): void { 31 // set dropdown based on options from query string 32 const hash = window.location.hash; 33 redrawOptions(); 34 redraw(); 35 36 // Register dialog 37 const dialog = document.querySelector('dialog') as HTMLDialogElement; 38 dialogPolyfill.registerDialog(dialog); 39 dialog.querySelector('.close')!.addEventListener('click', () => { 40 dialog.close(); 41 }); 42 43 if (hash !== "") { 44 const el = document.body.querySelector(hash); 45 const mainContainer = document.body.querySelector(".mdl-layout__content"); 46 if (el && mainContainer) { 47 setTimeout(() => { 48 mainContainer.scrollTop = el.getBoundingClientRect().top; 49 window.location.hash = hash; 50 }, 32); 51 (el.querySelector(".mdl-button--primary") as HTMLButtonElement).click(); 52 } 53 } 54 }; 55 56 function selectionText(sel: HTMLSelectElement): string { 57 return sel.selectedIndex === 0 ? "" : sel.options[sel.selectedIndex].text; 58 } 59 60 /** 61 * Takes an org/repo string and a repo to plugin map and returns the plugins 62 * that apply to the repo. 63 * @param repoSel repo name 64 * @param repoPlugins maps plugin name to plugin 65 */ 66 function applicablePlugins(repoSel: string, repoPlugins: {[key: string]: string[]}): string[] { 67 if (repoSel === "") { 68 const all = repoPlugins[""]; 69 if (all) { 70 return all.sort(); 71 } 72 return []; 73 } 74 const parts = repoSel.split("/"); 75 const byOrg = repoPlugins[parts[0]]; 76 let plugins: string[] = []; 77 if (byOrg && byOrg !== []) { 78 plugins = plugins.concat(byOrg); 79 } 80 const pluginNames = repoPlugins[repoSel]; 81 if (pluginNames) { 82 pluginNames.forEach((pluginName) => { 83 if (!plugins.includes(pluginName)) { 84 plugins.push(pluginName); 85 } 86 }); 87 } 88 return plugins.sort(); 89 } 90 91 /** 92 * Returns a normal cell for the command row. 93 * @param data content of the cell 94 * @param styles a list of styles applied to the cell. 95 * @param noWrap true if the content of the cell should be wrap. 96 */ 97 function createCommandCell(data: string | string[], styles: string[] = [], noWrap = false): HTMLTableDataCellElement { 98 const cell = document.createElement("td"); 99 cell.classList.add("mdl-data-table__cell--non-numeric"); 100 if (!noWrap) { 101 cell.classList.add("table-cell"); 102 } 103 let content: HTMLElement; 104 if (Array.isArray(data)) { 105 content = document.createElement("ul"); 106 content.classList.add("command-example-list"); 107 108 data.forEach((item) => { 109 const itemContainer = document.createElement("li"); 110 const span = document.createElement("span"); 111 span.innerHTML = item; 112 span.classList.add(...styles); 113 itemContainer.appendChild(span); 114 content.appendChild(itemContainer); 115 }); 116 } else { 117 content = document.createElement("div"); 118 content.classList.add(...styles); 119 content.innerHTML = data; 120 } 121 122 cell.appendChild(content); 123 124 return cell; 125 } 126 127 /** 128 * Returns an icon element. 129 * @param no no. command 130 * @param iconString icon name 131 * @param styles list of styles of the icon 132 * @param tooltip tooltip string 133 * @param isButton true if icon is a button 134 */ 135 function createIcon(no: number, iconString: string, styles: string[], tooltip: string, isButton?: false): HTMLDivElement 136 function createIcon(no: number, iconString: string, styles: string[], tooltip: string, isButton?: true): HTMLButtonElement 137 function createIcon(no: number, iconString: string, styles: string[] = [], tooltip: string = "", isButton = false) { 138 const icon = document.createElement("i"); 139 icon.id = "icon-" + iconString + "-" + no; 140 icon.classList.add("material-icons"); 141 icon.classList.add(...styles); 142 icon.innerHTML = iconString; 143 144 const container = isButton ? document.createElement("button") : document.createElement("div"); 145 container.appendChild(icon); 146 if (isButton) { 147 container.classList.add(...["mdl-button", "mdl-js-button", "mdl-button--icon"]); 148 } 149 150 if (tooltip === "") { 151 return container; 152 } 153 154 const tooltipEl = document.createElement("div"); 155 tooltipEl.setAttribute("for", icon.id); 156 tooltipEl.classList.add("mdl-tooltip"); 157 tooltipEl.innerHTML = tooltip; 158 container.appendChild(tooltipEl); 159 160 return container; 161 } 162 163 /** 164 * Returns the feature cell for the command row. 165 * @param isFeatured true if the command is featured. 166 * @param isExternal true if the command is external. 167 * @param no no. command. 168 */ 169 function commandStatus(isFeatured: boolean, isExternal: boolean, no: number): HTMLTableDataCellElement { 170 const status = document.createElement("td"); 171 status.classList.add("mdl-data-table__cell--non-numeric"); 172 if (isFeatured) { 173 status.appendChild( 174 createIcon(no, "stars", ["featured-icon"], "Featured command")); 175 } 176 if (isExternal) { 177 status.appendChild( 178 createIcon(no, "open_in_new", ["external-icon"], "External plugin")); 179 } 180 return status; 181 } 182 183 /** 184 * Returns a section to the content of the dialog 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 * @param repo repo name 209 * @param pluginName plugin name. 210 * @param plugin the plugin to which the command belong to 211 */ 212 function createPluginCell(repo: string, pluginName: string, plugin: PluginHelp): HTMLTableDataCellElement { 213 const pluginCell = document.createElement("td"); 214 const button = document.createElement("button"); 215 pluginCell.classList.add("mdl-data-table__cell--non-numeric"); 216 button.classList.add("mdl-button", "mdl-button--js", "mdl-button--primary"); 217 button.innerHTML = pluginName; 218 219 // Attach Event Handlers. 220 const dialog = document.querySelector('dialog') as HTMLDialogElement; 221 button.addEventListener('click', () => { 222 const title = dialog.querySelector(".mdl-dialog__title")!; 223 const content = dialog.querySelector(".mdl-dialog__content")!; 224 225 while (content.firstChild) { 226 content.removeChild(content.firstChild); 227 } 228 229 title.innerHTML = pluginName; 230 if (plugin.Description) { 231 content.appendChild(addDialogSection("Description", plugin.Description)); 232 } 233 if (plugin.Events) { 234 const sectionContent = "[" + plugin.Events.sort().join(", ") + "]"; 235 content.appendChild(addDialogSection("Events handled", sectionContent)); 236 } 237 if (plugin.Config) { 238 let sectionContent = plugin.Config ? plugin.Config[repo] : ""; 239 let sectionTitle = 240 repo === "" ? "Configuration(global)" : "Configuration(" + repo + ")"; 241 if (sectionContent && sectionContent !== "") { 242 content.appendChild(addDialogSection(sectionTitle, sectionContent)); 243 } 244 } 245 dialog.showModal(); 246 }); 247 248 pluginCell.appendChild(button); 249 return pluginCell; 250 } 251 252 /** 253 * Creates a link that links to the command. 254 */ 255 function createCommandLink(name: string, no: number): HTMLTableDataCellElement { 256 const link = document.createElement("td"); 257 const iconButton = createIcon(no, "link", ["link-icon"], "", true); 258 259 iconButton.addEventListener("click", () => { 260 const tempInput = document.createElement("input"); 261 let url = window.location.href; 262 const hashIndex = url.indexOf("#"); 263 if (hashIndex !== -1) { 264 url = url.slice(0, hashIndex); 265 } 266 267 url += "#" + name; 268 tempInput.style.zIndex = "-99999"; 269 tempInput.style.background = "transparent"; 270 tempInput.value = url; 271 272 document.body.appendChild(tempInput); 273 tempInput.select(); 274 document.execCommand("copy"); 275 document.body.removeChild(tempInput); 276 277 const toast = document.body.querySelector("#toast")! as SnackbarElement; 278 toast.MaterialSnackbar.showSnackbar({message: "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 /** 289 * Creates a row for the Command table. 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 * @param repo repo name. 318 * @param helpMap maps a plugin name to a plugin. 319 */ 320 function redrawHelpTable(repo: string, helpMap: Map<string, {isExternal: boolean, plugin: PluginHelp}>): void { 321 const table = document.getElementById("command-table")!; 322 const tableBody = document.querySelector("tbody")!; 323 if (helpMap.size === 0) { 324 table.style.display = "none"; 325 return; 326 } 327 table.style.display = "table"; 328 while (tableBody.childElementCount !== 0) { 329 tableBody.removeChild(tableBody.firstChild!); 330 } 331 const names = Array.from(helpMap.keys()); 332 const commandsWithPluginName: {pluginName: string, command: Command}[] = []; 333 for (let name of names) { 334 helpMap.get(name)!.plugin.Commands.forEach((command) => { 335 commandsWithPluginName.push({ 336 pluginName: name, 337 command: command 338 }); 339 }); 340 } 341 commandsWithPluginName 342 .sort((command1, command2) => { 343 return command1.command.Featured ? -1 : command2.command.Featured ? 1 : 0; 344 }) 345 .forEach((command, index) => { 346 const pluginName = command.pluginName; 347 const {isExternal, plugin} = helpMap.get(pluginName)!; 348 const commandRow = createCommandRow( 349 repo, 350 pluginName, 351 plugin, 352 command.command, 353 isExternal, 354 index); 355 tableBody.appendChild(commandRow); 356 }); 357 } 358 359 /** 360 * Redraws the content of the page. 361 */ 362 function redraw(): void { 363 const repoSel = selectionText(document.getElementById("repo") as HTMLSelectElement); 364 if (window.history && window.history.replaceState !== undefined) { 365 if (repoSel !== "") { 366 history.replaceState(null, "", "/command-help?repo=" 367 + encodeURIComponent(repoSel)); 368 } else { 369 history.replaceState(null, "", "/command-help") 370 } 371 } 372 redrawOptions(); 373 374 const pluginsWithCommands: Map<string, {isExternal: boolean, plugin: PluginHelp}> = new Map(); 375 applicablePlugins(repoSel, allHelp.RepoPlugins) 376 .forEach((name) => { 377 if (allHelp.PluginHelp[name] && allHelp.PluginHelp[name].Commands) { 378 pluginsWithCommands.set( 379 name, 380 { 381 isExternal: false, 382 plugin: allHelp.PluginHelp[name] 383 }); 384 } 385 }); 386 applicablePlugins(repoSel, allHelp.RepoExternalPlugins) 387 .forEach((name) => { 388 if (allHelp.ExternalPluginHelp[name] 389 && allHelp.ExternalPluginHelp[name].Commands) { 390 pluginsWithCommands.set( 391 name, 392 { 393 isExternal: true, 394 plugin: allHelp.ExternalPluginHelp[name] 395 }); 396 } 397 }); 398 redrawHelpTable(repoSel, pluginsWithCommands); 399 } 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;