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;