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;