三叉戟-发版管理-表格增强

给发版管理的表格增加ID列、删除新建时间、新增发版日志按钮

目前為 2024-04-03 提交的版本,檢視 最新版本

// ==UserScript==
// @name         三叉戟-发版管理-表格增强
// @namespace    http://tampermonkey.net/
// @version      2024-04-03-1.0.1
// @description  给发版管理的表格增加ID列、删除新建时间、新增发版日志按钮
// @author       CloudS3n
// @match        https://poseidon.cisdigital.cn/app/devops*
// @icon         https://poseidon.cisdigital.cn/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const releaseUrlPrefix =
    "https://poseidon.cisdigital.cn/app/devops/releases?";
  let isFirstLoad = true;
  let projectName;
  let productName;

  initial();

  function initial() {
    // vue单页面框架,需要根据URL的变化执行相应的操作
    let lastUrl = location.href;
    new MutationObserver(() => {
      const url = location.href;
      if (isFirstLoad || url !== lastUrl) {
        lastUrl = url;
        onUrlChange();
        isFirstLoad = false;
      }
    }).observe(document, { subtree: true, childList: true });
  }

  function onUrlChange() {
    const currUrl = location.href;
    console.log("[三叉戟] URL changed!", currUrl);
    if (currUrl.startsWith(releaseUrlPrefix)) {
      doTableObserver();
    }
  }

  function doTableObserver() {
    const observer = new MutationObserver((mutations, me) => {
      console.log("[三叉戟] 监听版本管理页面");
      if (isPageLoaded()) {
        let projectDisplayName = document.querySelectorAll(
          "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-project > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
        )[0].textContent;
        let defaultProductDisplayName = document.querySelectorAll(
          "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-product > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
        )[0]?.textContent;
        let selectProductDisplayName = document.querySelectorAll(
          "body > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul:nth-child(2) > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
        )[0]?.textContent;
        let productDisplayName = defaultProductDisplayName ? defaultProductDisplayName : selectProductDisplayName;
        if (!productDisplayName) {
          console.error(
            `[三叉戟] 获取项目和产品信息失败,projectDisplayName=${productDisplayName}, productDisplayName=${productDisplayName}`
          );
          return;
        }
        fetchProjectList()
          .then((projectList) => {
            if (projectList && projectList.length > 0) {
              projectList.forEach((project) => {
                if (project.display_name === projectDisplayName) {
                  projectName = project.name;
                  project.products.forEach((product) => {
                    if (
                      product.display_name === productDisplayName
                    ) {
                      productName = product.name;
                    }
                  });
                }
              });
            }
            if (projectName && productName) {
              console.log(`[三叉戟] project=${projectName}, product=${productName}`);
              observer.disconnect();
              addIdColumn();
              removeCreationTimeColumn();
              removeEditButtons();
              addReleaseNoteButton();
              fetchReleaseList()
                .then((data) => {
                  updateTableWithRealIds(data);
                })
                .catch((error) => {
                  console.error(`[三叉戟] Error fetching data: ${error}`);
                });
            } else {
              console.error(
                `[三叉戟] 获取项目和产品信息失败,project=${projectName}, product=${productName}`
              );
            }
          })
          .catch((error) => {
            console.error(`[三叉戟] Error fetching data: ${error}`);
          });
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  function isPageLoaded() {
    const headerRow = document.querySelector(
      ".el-table__header-wrapper .el-table__header thead tr"
    );
    const bodyRows = document.querySelectorAll(
      ".el-table__body-wrapper .el-table__body tbody tr"
    );
    const project = document.querySelectorAll(
      "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-project > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
    )[0];
    const defaultProduct = document.querySelectorAll(
      "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-product > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
    )[0];
    const product = document.querySelectorAll(
      "body > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul:nth-child(2) > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
    )[0];
    return (
      headerRow &&
      bodyRows &&
      bodyRows.length > 0 &&
      project &&
      (defaultProduct || product)
    );
  }

  function addReleaseNoteDom(releaseId, taskList) {
    addModalStyles();
    document.getElementById("release-log-modal")?.remove();
    const modalHtml = buildModalContent(releaseId, taskList);
    document.body.insertAdjacentHTML("beforeend", modalHtml);
    setupModal();
  }

  function getCookie(cookieName) {
    var strCookie = document.cookie;
    var arrCookie = strCookie.split("; ");
    for (var i = 0; i < arrCookie.length; i++) {
      var arr = arrCookie[i].split("=");
      if (cookieName == arr[0]) {
        return arr[1];
      }
    }
    return "";
  }

  function removeCreationTimeColumn() {
    for (let i = 0; i++; i < 2) {
      const headers = document.querySelectorAll(
        `.el-table__${
          i == 0 ? "fixed-" : ""
        }header-wrapper .el-table__header thead th`
      );
      let columnIndex = -1;
      headers.forEach((th, index) => {
        if (th.textContent.trim() === "新建时间") {
          columnIndex = index;
          th.remove();
        }
      });

      if (columnIndex !== -1) {
        const cols = document.querySelector(
          `.el-table__${
            i == 0 ? "fixed-" : ""
          }header-wrapper .el-table__header colgroup col`
        );
        if (cols[columnIndex]) {
          cols[columnIndex].remove();
        }
        const bodyCols = document.querySelectorAll(
          `.el-table__${
            i == 0 ? "fixed-" : ""
          }body-wrapper .el-table__body colgroup col`
        );
        if (bodyCols[columnIndex]) {
          bodyCols[columnIndex].remove();
        }
      }

      const rows = document.querySelectorAll(
        `.el-table__${
          i == 0 ? "fixed-" : ""
        }body-wrapper .el-table__body tbody tr`
      );
      rows.forEach((row) => {
        const cells = row.querySelectorAll("td");
        if (cells[columnIndex]) {
          cells[columnIndex].remove();
        }
      });
    }
  }

  function removeEditButtons() {
    const actionGroups = document.querySelectorAll(".table-action-group");
    if (actionGroups && actionGroups.length > 0) {
      actionGroups.forEach((group) => {
        const actionItems = group.querySelectorAll(".table-action-item");
        actionItems.forEach((item) => {
          const buttonText = Array.from(
            item.querySelectorAll("button, button *")
          )
            .map((e) => e.textContent.trim())
            .join("");
          if (buttonText.includes("编辑")) {
            item.remove();
            console.log("删除编辑按钮");
          }
        });
      });
    }
  }

  function addIdColumn() {
    console.log("[三叉戟] 添加ID列");
    const headerRow = document.querySelector(
      ".el-table__header-wrapper .el-table__header thead tr"
    );
    const colgroup = document.querySelector(
      ".el-table__header-wrapper .el-table__header colgroup"
    );
    const bodyColgroup = document.querySelector(
      ".el-table__body-wrapper .el-table__body colgroup"
    );
    const colgroupFixed = document.querySelector(
      ".el-table__fixed-header-wrapper .el-table__header colgroup"
    );
    const bodyColgroupFixed = document.querySelector(
      ".el-table__fixed-body-wrapper .el-table__body colgroup"
    );
    const newCol = document.createElement("col");
    newCol.style.width = "240px";
    if (colgroup.firstChild) {
      colgroup.insertBefore(newCol, colgroup.firstChild);
    } else {
      colgroup.appendChild(newCol);
    }
    if (bodyColgroup.firstChild) {
      bodyColgroup.insertBefore(
        newCol.cloneNode(true),
        bodyColgroup.firstChild
      );
    } else {
      bodyColgroup.appendChild(newCol.cloneNode(true));
    }

    if (colgroupFixed.firstChild) {
      colgroupFixed.insertBefore(
        newCol.cloneNode(true),
        colgroupFixed.firstChild
      );
    } else {
      colgroupFixed.appendChild(newCol.cloneNode(true));
    }
    if (bodyColgroupFixed.firstChild) {
      bodyColgroupFixed.insertBefore(
        newCol.cloneNode(true),
        bodyColgroupFixed.firstChild
      );
    } else {
      bodyColgroupFixed.appendChild(newCol.cloneNode(true));
    }
    const idHeader = document.createElement("th");
    idHeader.innerHTML = '<div class="cell">ID</div>';
    headerRow.insertBefore(idHeader, headerRow.firstChild);
    document
      .querySelectorAll(
        ".el-table__fixed-body-wrapper .el-table__body tbody tr"
      )
      .forEach((row) => {
        const idCell = document.createElement("td");
        idCell.innerHTML = '<div class="cell"></div>';
        row.insertBefore(idCell, row.firstChild);
      });
    document
      .querySelectorAll(".el-table__body-wrapper .el-table__body tbody tr")
      .forEach((row) => {
        const idCell = document.createElement("td");
        idCell.innerHTML = '<div class="cell"></div>';
        row.insertBefore(idCell, row.firstChild);
      });
  }

  function addReleaseNoteButton() {
    console.log("[三叉戟] 添加发版日志按钮");

    document
      .querySelectorAll(
        ".el-table__fixed-body-wrapper .el-table__body tbody tr"
      )
      .forEach((row) => {
        const actionGroup = row.querySelector(
          "td:last-child .table-action-group"
        );
        if (actionGroup) {
          const actionItem = document.createElement("div");
          actionItem.className = "table-action-item";
          const btn = document.createElement("button");
          btn.setAttribute("type", "button");
          btn.style.cssText =
            "margin-top:-10px; vertical-align:middle; color:var(--cs-color_primary)";
          btn.className =
            "el-button table-action-button table-action-item el-button--text";
          btn.innerHTML = "<span>发版日志</span>";
          btn.addEventListener("click", function () {
            let releaseId = row.querySelector("td:first-child .cell").innerText;
            if (!releaseId) return;
            let env = "dev";
            fetchReleaseDetailData(releaseId)
              .then((releaseDetial) => {
                if (releaseDetial && releaseDetial.env) env = releaseDetial.env;
                fetchTaskList(releaseId, env)
                  .then((taskList) => {
                    addReleaseNoteDom(releaseDetial, taskList);
                    const modal = document.getElementById("release-log-modal");
                    modal.style.display = "block";
                  })
                  .catch((error) => {
                    console.error(`[三叉戟] Error fetching data: ${error}`);
                  });
              })
              .catch((error) => {
                console.error(`[三叉戟] Error fetching data: ${error}`);
              });
          });
          actionItem.appendChild(btn);
          actionGroup.appendChild(actionItem);
        }
      });
  }

  function fetchProjectList() {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: `https://poseidon.cisdigital.cn/api/projects/`,
        headers: {
          accept: "application/json, text/plain, */*",
          "accept-language": "zh-CN,zh;q=0.9",
          authorization: `Bearer ${getCookie("poseidon_user_token")}`,
          referer:
            "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
        },
        onload: function (response) {
          const result = JSON.parse(response.responseText);
          if (result.code === 0 && Array.isArray(result.data)) {
            console.log(
              `[三叉戟] 获取项目数据: ${JSON.stringify(result.data)}`
            );
            resolve(result.data);
          } else {
            reject(
              new Error(
                "[三叉戟] API call failed or returned an unexpected structure"
              )
            );
          }
        },
        onerror: function (error) {
          reject(new Error(`API call failed: ${error}`));
        },
      });
    });
  }

  function fetchReleaseList() {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: "https://poseidon.cisdigital.cn/devops/api/releases/?page=1&page_size=100&archived=0&search=",
        headers: {
          accept: "application/json, text/plain, */*",
          "accept-language": "zh-CN,zh;q=0.9",
          authorization: `Bearer ${getCookie("poseidon_user_token")}`,
          referer:
            "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
          "x-poseidon-product": `${productName}`,
          "x-poseidon-project": `${projectName}`,
        },
        onload: function (response) {
          const result = JSON.parse(response.responseText);
          if (result.code === 0 && Array.isArray(result.data)) {
            console.log(
              `[三叉戟] 获取版本管理数据: ${JSON.stringify(result.data)}`
            );
            resolve(result.data);
          } else {
            reject(
              new Error(
                "[三叉戟] API call failed or returned an unexpected structure"
              )
            );
          }
        },
        onerror: function (error) {
          reject(new Error(`API call failed: ${error}`));
        },
      });
    });
  }

  function fetchReleaseDetailData(releaseId) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: `https://poseidon.cisdigital.cn/devops/api/releases/${releaseId}/`,
        headers: {
          accept: "application/json, text/plain, */*",
          "accept-language": "zh-CN,zh;q=0.9",
          authorization: `Bearer ${getCookie("poseidon_user_token")}`,
          referer:
            "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
          "x-poseidon-product": `${productName}`,
          "x-poseidon-project": `${projectName}`,
        },
        onload: function (response) {
          const result = JSON.parse(response.responseText);
          if (result.code === 0 && result.data) {
            console.log(
              `[三叉戟] 获取版本详情数据: ${JSON.stringify(result.data)}`
            );
            resolve(result.data);
          } else {
            reject(
              new Error(
                "[三叉戟] API call failed or returned an unexpected structure"
              )
            );
          }
        },
        onerror: function (error) {
          reject(new Error(`API call failed: ${error}`));
        },
      });
    });
  }

  function fetchTaskList(releaseId, env) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: `https://poseidon.cisdigital.cn/devops/api/releases/${releaseId}/tasks/?env=${env}`,
        headers: {
          accept: "application/json, text/plain, */*",
          "accept-language": "zh-CN,zh;q=0.9",
          authorization: `Bearer ${getCookie("poseidon_user_token")}`,
          referer:
            "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
          "x-poseidon-product": `${productName}`,
          "x-poseidon-project": `${projectName}`,
        },
        onload: function (response) {
          const result = JSON.parse(response.responseText);
          if (result.code === 0 && Array.isArray(result.data)) {
            console.log(
              `[三叉戟] 获取版本任务数据: ${JSON.stringify(result.data)}`
            );
            resolve(result.data);
          } else {
            reject(
              new Error(
                "[三叉戟] API call failed or returned an unexpected structure"
              )
            );
          }
        },
        onerror: function (error) {
          reject(new Error(`API call failed: ${error}`));
        },
      });
    });
  }

  function updateTableWithRealIds(data) {
    const nameIdMap = data.reduce((acc, curr) => {
      acc[curr.name] = curr.id;
      return acc;
    }, {});

    const bodyRows = document.querySelectorAll(
      ".el-table__body-wrapper .el-table__body tbody tr"
    );
    const bodyRowsFixed = document.querySelectorAll(
      ".el-table__fixed-body-wrapper .el-table__body tbody tr"
    );
    console.log("[三叉戟] 填充ID列");
    bodyRows.forEach((row) => {
      const versionCell = row.querySelector("td:nth-child(2)");
      if (versionCell) {
        const versionText = versionCell.textContent.trim();
        const realId = nameIdMap[versionText];
        if (realId !== undefined) {
          const idCell = row.querySelector("td:first-child .cell");
          if (idCell) {
            idCell.textContent = realId;
          }
        }
      }
    });
    bodyRowsFixed.forEach((row) => {
      const versionCell = row.querySelector("td:nth-child(2)");
      if (versionCell) {
        const versionText = versionCell.textContent.trim();
        const realId = nameIdMap[versionText];
        if (realId !== undefined) {
          const idCell = row.querySelector("td:first-child .cell");
          if (idCell) {
            idCell.textContent = realId;
          }
        }
      }
    });
  }

  function addModalStyles() {
    GM_addStyle(`
        .modal {
            display: none;
            position: fixed;
            z-index: 10000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgb(0,0,0);
            background-color: rgba(0,0,0,0.4);
            padding-top: 60px;
        }

        .modal-content {
            background-color: #fefefe;
            margin: 5% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
        }

        .close {
            color: #aaaaaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
        }

        .close:hover,
        .close:focus {
            color: #000;
            text-decoration: none;
            cursor: pointer;
        }

        #release-note-table {
            width: 100%;
            border-collapse: collapse;
        }

        #release-note-table > thead > tr > th, td {
            border: 1px solid #ddd;
            text-align: left;
            padding: 8px;
        }

        #release-note-table > thead > tr > th {
            background-color: #f2f2f2;
            text-align: center;
            font-size: 20px;
            font-weight: bold;
        }

        #release-note-table > tbody > tr > td.sub-title {
          font-size: 18px;
          font-weight: bold;
          background-color: #f2f2f2;
        }

        release-note-table > tbody > tr.third-title {
          font-size: 16px;
          font-weight: bold;
        }

        release-note-table > tbody > tr.item-list:nth-child(odd) {
          background-color: #e0f2f1;
        }

        .multi-line-clamp {
          display: -webkit-box;
          -webkit-box-orient: vertical;
          -webkit-line-clamp: 500; /* 定义最大显示的行数 */
          overflow: hidden;
          text-overflow: ellipsis; /* 当内容溢出时显示... */
          word-break: break-word; /* 使得单词在必要时可以断行 */
          max-height: 20em; /* 根据行高和最大行数调整 */
          line-height: 1em; /* 设置行高 */
        }
    `);
  }

  // 弹窗逻辑
  function setupModal() {
    const modal = document.getElementById("release-log-modal");
    const closeBtn = modal.querySelector(".close");

    // 点击关闭按钮隐藏弹窗
    closeBtn.onclick = function () {
      modal.style.display = "none";
    };

    // 点击窗口外部区域隐藏弹窗
    window.onclick = function (event) {
      if (event.target === modal) {
        modal.style.display = "none";
      }
    };
  }

  function addElementToKey(myMap, key, element) {
    if (myMap.has(key)) {
      let currentList = myMap.get(key);
      currentList.push(element);
      myMap.set(key, currentList);
    } else {
      myMap.set(key, [element]);
    }
  }

  function buildModalContent(releaseDetail, taskList) {
    // 生成任务列表HTML
    const bodyMap = new Map();
    taskList
      ?.sort((a, b) => a.order - b.order)
      .forEach((task) => {
        switch (task.task_type) {
          case 1: {
            // 前端或后端服务
            const serverHtml = `
            <tr class="item-list">
            <td colspan="1">${task.task.app.name}</td>
            <td colspan="1">${task.task.tag}</td>
            <td colspan="3">${task.task.digest}</td>
            </tr>
        `;
            if (task.task.app.name.startsWith("fe-")) {
              addElementToKey(bodyMap, "frontEndBody", serverHtml);
            } else {
              addElementToKey(bodyMap, "backEndBody", serverHtml);
            }
            break;
          }
          case 2: {
            // 需要执行的sql
            addElementToKey(
              bodyMap,
              "sqlBody",
              `
              <tr class="item-list">
              <td colspan="1">${task.task.middleware}-${task.task.database}</td>
              <td colspan="1">${task.task.name}</td>
              <td colspan="3"><span>${task.task.content.replace(
                /\n/g,
                "<br>"
              )}</span></td>
              </tr>
        `
            );
            break;
          }
          case 3: {
            // yaml配置
            let yamlHtml = task.task
              .map(
                (t) => `
                <tr class="item-list">
                <td colspan="2">${t.name}</td>
                <td colspan="3">详情</td>
                </tr>
          `
              )
              .join("");
            addElementToKey(bodyMap, "yamlBody", yamlHtml);
            break;
          }
          case 4: {
            // 需要人工确认的事项
            addElementToKey(
              bodyMap,
              "manualBody",
              `<tr class="item-list"><td colspan="5">${task.task.content}</td></tr>`
            );
            break;
          }
          default:
            return "";
        }
      });

    return `
    <div id="release-log-modal" class="modal">
        <div id="modal-content" class="modal-content">
            <span class="close">&times;</span>
            <table id="release-note-table">
              <thead>
                <tr>
                    <th colspan="5">数据集成与开发平台-发版日志-<span id="product-version">${
                      releaseDetail.name
                    }</span></th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td colspan="5" class="sub-title">1.需要人工确认的事项</td>
                </tr>
                ${
                  bodyMap.get("manualBody")
                    ? bodyMap.get("manualBody").join("")
                    : ""
                }
                <tr>
                  <td colspan="5" class="sub-title">2.SQL变动</td>
                </tr>
                <tr class="third-title">
                  <td colspan="1">数据库</td>
                  <td colspan="1">描述</td>
                  <td colspan="3">执行sql</td>
                </tr>
                ${bodyMap.get("sqlBody") ? bodyMap.get("sqlBody").join("") : ""}
                <tr>
                  <td colspan="5" class="sub-title">3.YAML变动</td>
                </tr>
                <tr class="third-title">
                  <td colspan="2">YAML文件名</td>
                  <td colspan="3">修改详情</td>
                </tr>
                ${
                  bodyMap.get("yamlBody")
                    ? bodyMap.get("yamlBody").join("")
                    : ""
                }
                <tr>
                  <td colspan="5" class="sub-title">4.前端服务</td>
                </tr>
                <tr class="third-title">
                    <td colspan="1">服务名</td>
                    <td colspan="1">镜像tag</td>
                    <td colspan="3">镜像摘要</td>
                </tr>
                ${
                  bodyMap.get("frontEndBody")
                    ? bodyMap.get("frontEndBody").join("")
                    : ""
                }
                <tr>
                  <td colspan="5" class="sub-title">5.后端服务</td>
                </tr>
                <tr class="third-title">
                    <td colspan="1">服务名</td>
                    <td colspan="1">镜像tag</td>
                    <td colspan="3">镜像摘要</td>
                </tr>
                ${
                  bodyMap.get("backEndBody")
                    ? bodyMap.get("backEndBody").join("")
                    : ""
                }
              </tbody>
          </table>
        </div>
    </div>
    `;
  }
})();