// ==UserScript==
// @name 三叉戟-发版管理-表格增强
// @namespace http://tampermonkey.net/
// @version 2024-04-03-1.0.4
// @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;
let productDisplayName;
const observer = new MutationObserver((mutations, me) => {
console.log("[三叉戟] 版本管理页面dom发生变化");
if (isPageLoaded()) {
console.log("[三叉戟] 页面加载完毕");
observer.disconnect();
console.log("[三叉戟] 停止监听版本管理页面");
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;
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}`);
addIdColumn();
removeColumn('新建时间');
removeColumn('ID');
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}`);
});
}
});
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)) {
enableTableObserver();
}
}
function enableTableObserver() {
console.log("[三叉戟] 开始监听版本管理页面");
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 removeColumn(columnName) {
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() === columnName) {
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 > td {
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">×</span>
<table id="release-note-table">
<thead>
<tr>
<th colspan="5">${productDisplayName}-发版日志-<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>
`;
}
})();