ui
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Stage管理</title>
|
||||
<link rel="stylesheet" href="/ui/assets/common.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #ff7875;
|
||||
}
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.breadcrumb a {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 4px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<!-- <h1 class="title">Stage管理</h1> -->
|
||||
<div class="breadcrumb">
|
||||
<a href="/project_list">返回 Project 管理</a> / Stage管理
|
||||
</div>
|
||||
<button class="btn" id="add-stage-btn">添加Stage</button>
|
||||
</div>
|
||||
|
||||
<div id="stage-list">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>关联树节点</th>
|
||||
<!-- <th>描述</th> -->
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="stage-table-body">
|
||||
<!-- 数据将通过JavaScript动态加载 -->
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="empty-state" class="empty-state" style="display: none">
|
||||
<p>暂无Stage数据,请添加新Stage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑Stage的模态框 -->
|
||||
<div class="modal" id="stage-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="modal-title">添加Stage</h2>
|
||||
<span class="close" id="close-modal">×</span>
|
||||
</div>
|
||||
<form id="stage-form">
|
||||
<input type="hidden" id="stage-id" value="" />
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stage-name">名称</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
id="stage-name"
|
||||
value=""
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stage-tree-id">关联树节点</label>
|
||||
<select class="form-input" id="stage-tree-id" required>
|
||||
<option value="">请选择树节点</option>
|
||||
<!-- 树节点选项将通过JavaScript动态加载 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn" id="cancel-btn">取消</button>
|
||||
<button type="submit" class="btn" id="save-btn">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认删除的模态框 -->
|
||||
<div class="modal" id="confirm-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">确认删除</h2>
|
||||
<span class="close" id="close-confirm-modal">×</span>
|
||||
</div>
|
||||
<p>
|
||||
确定要删除此Stage吗?此操作将同时删除该Stage下的所有图形节点和连接,且不可恢复。
|
||||
</p>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn" id="cancel-delete-btn">取消</button>
|
||||
<button type="button" class="btn btn-danger" id="confirm-delete-btn">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载指示器 -->
|
||||
<div class="loading" id="loading" style="display: none">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 基础URL
|
||||
const API_BASE_URL = "http://127.0.0.1:5002/api";
|
||||
|
||||
// DOM元素
|
||||
const stageTableBody = document.getElementById("stage-table-body");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
const addStageBtn = document.getElementById("add-stage-btn");
|
||||
const stageModal = document.getElementById("stage-modal");
|
||||
const modalTitle = document.getElementById("modal-title");
|
||||
const closeModalBtn = document.getElementById("close-modal");
|
||||
const stageForm = document.getElementById("stage-form");
|
||||
const stageIdInput = document.getElementById("stage-id");
|
||||
const stageNameInput = document.getElementById("stage-name");
|
||||
const stageTreeIdSelect = document.getElementById("stage-tree-id");
|
||||
const cancelBtn = document.getElementById("cancel-btn");
|
||||
const confirmModal = document.getElementById("confirm-modal");
|
||||
const closeConfirmModal = document.getElementById("close-confirm-modal");
|
||||
const cancelDeleteBtn = document.getElementById("cancel-delete-btn");
|
||||
const confirmDeleteBtn = document.getElementById("confirm-delete-btn");
|
||||
const loading = document.getElementById("loading");
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// 当前要删除的StageID
|
||||
let currentDeleteId = null;
|
||||
// 存储树节点数据
|
||||
let treeNodes = [];
|
||||
let projectId = urlParams.get("project_id");
|
||||
// 页面加载时获取Stage列表和树节点列表
|
||||
function init() {
|
||||
fetchTreeNodes();
|
||||
|
||||
fetchStages();
|
||||
}
|
||||
init();
|
||||
// 添加Stage按钮点击事件
|
||||
addStageBtn.addEventListener("click", () => {
|
||||
modalTitle.textContent = "添加Stage";
|
||||
stageForm.reset();
|
||||
stageIdInput.value = "";
|
||||
openModal(stageModal);
|
||||
});
|
||||
|
||||
// 关闭模态框
|
||||
closeModalBtn.addEventListener("click", () => closeModal(stageModal));
|
||||
cancelBtn.addEventListener("click", () => closeModal(stageModal));
|
||||
|
||||
// 关闭确认删除模态框
|
||||
closeConfirmModal.addEventListener("click", () =>
|
||||
closeModal(confirmModal)
|
||||
);
|
||||
cancelDeleteBtn.addEventListener("click", () => closeModal(confirmModal));
|
||||
|
||||
// 确认删除
|
||||
confirmDeleteBtn.addEventListener("click", deleteStage);
|
||||
|
||||
// 表单提交
|
||||
stageForm.addEventListener("submit", handleFormSubmit);
|
||||
// 获取所有Stage
|
||||
async function fetchStages() {
|
||||
showLoading();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/project/stage/${projectId}/`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && Array.isArray(result.data)) {
|
||||
renderStageList(result.data);
|
||||
} else {
|
||||
showError("获取Stage列表失败: " + (result.message || "未知错误"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取Stage列表错误:", error);
|
||||
showError("获取Stage列表错误: " + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
async function fetchTreeNodes() {
|
||||
showLoading();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/project/demand/${projectId}`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status}`);
|
||||
}
|
||||
console.log("fetch nodes", response);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && Array.isArray(result.data)) {
|
||||
treeNodes = result.data;
|
||||
populateTreeSelect();
|
||||
} else {
|
||||
showError("获取树节点列表失败: " + (result.message || "未知错误"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取树节点列表错误:", error);
|
||||
showError("获取树节点列表错误: " + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
// 加载Stage数据用于编辑
|
||||
|
||||
// 填充树节点选择框
|
||||
function populateTreeSelect() {
|
||||
stageTreeIdSelect.innerHTML = '<option value="">请选择树节点</option>';
|
||||
// 添加虚拟根节点选项
|
||||
/* const rootOption = document.createElement("option");
|
||||
rootOption.value = "root"; // 使用特殊值"root"表示虚拟根节点
|
||||
rootOption.textContent = "所有节点 (虚拟根节点)";
|
||||
stageTreeIdSelect.appendChild(rootOption);
|
||||
*/
|
||||
treeNodes.forEach((node) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = node.id;
|
||||
option.textContent = `${node.name} (Level: ${node.level})`;
|
||||
stageTreeIdSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
function renderStageList(stages) {
|
||||
stageTableBody.innerHTML = "";
|
||||
|
||||
if (stages.length === 0) {
|
||||
emptyState.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.style.display = "none";
|
||||
|
||||
stages.forEach((stage) => {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
// 截断ID以便显示
|
||||
const shortId = stage.id.substring(0, 8) + "...";
|
||||
|
||||
// 查找关联的树节点名称
|
||||
let treeNodeName = "未关联";
|
||||
if (stage.demand_id) {
|
||||
if (stage.demand_id === "root") {
|
||||
treeNodeName = "所有节点 (虚拟根节点)";
|
||||
} else {
|
||||
console.log("demand_id: ", stage.demand_id);
|
||||
console.log(treeNodes);
|
||||
const treeNode = treeNodes.find(
|
||||
(node) => node.id === stage.demand_id
|
||||
);
|
||||
if (treeNode) {
|
||||
treeNodeName = `${treeNode.name} (Level: ${treeNode.level})`;
|
||||
} else {
|
||||
treeNodeName = `ID: ${stage.demand_id.substring(0, 8)}...`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td title="${stage.id}">${shortId}</td>
|
||||
<td>${stage.name || "未命名"}</td>
|
||||
<td>${treeNodeName}</td>
|
||||
<td class="action-cell">
|
||||
<button class="btn" data-id="${
|
||||
stage.id
|
||||
}" data-action="view-graph">查看Graph</button>
|
||||
<button class="btn" data-id="${
|
||||
stage.id
|
||||
}" data-action="view-endpoint">查看Endpoint</button>
|
||||
<button class="btn" data-id="${
|
||||
stage.id
|
||||
}" data-action="edit">编辑</button>
|
||||
<button class="btn btn-danger" data-id="${
|
||||
stage.id
|
||||
}" data-action="delete">删除</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
stageTableBody.appendChild(row);
|
||||
});
|
||||
|
||||
// 添加按钮事件监听
|
||||
document.querySelectorAll("[data-action]").forEach((button) => {
|
||||
button.addEventListener("click", handleActionClick);
|
||||
});
|
||||
}
|
||||
// 处理操作按钮点击
|
||||
async function handleActionClick(event) {
|
||||
console.log("action click");
|
||||
const action = event.target.getAttribute("data-action");
|
||||
const stageId = event.target.getAttribute("data-id");
|
||||
|
||||
switch (action) {
|
||||
case "view-graph":
|
||||
// 跳转到图形页面
|
||||
window.location.href = `/project/stage/stageGraph?project_id=${projectId}&stage_id=${stageId}`;
|
||||
break;
|
||||
case "view-endpoint":
|
||||
// 跳转到Endpoint管理页面
|
||||
window.location.href = `/project/stage/endpoint_list?project_id=${projectId}&stage_id=${stageId}`;
|
||||
break;
|
||||
case "edit":
|
||||
await loadStageForEdit(stageId);
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
openDeleteConfirmation(stageId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载Stage数据用于编辑
|
||||
async function loadStageForEdit(stageId) {
|
||||
showLoading();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/project/stage/${project_id}/${stageId}/`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status}`);
|
||||
}
|
||||
console.log("get");
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
const stage = result.data.stage;
|
||||
|
||||
// 填充表单
|
||||
console.log(stageIdInput);
|
||||
stageIdInput.value = stage.id;
|
||||
stageNameInput.value = stage.name || "";
|
||||
stageTreeIdSelect.value = stage.demand_id || "";
|
||||
//stageLevelInput.value = stage.level || 0;
|
||||
//stageDescriptionInput.innerHTML = stage.description || "";
|
||||
|
||||
// 更新模态框标题
|
||||
modalTitle.textContent = "编辑Stage";
|
||||
|
||||
// 打开模态框
|
||||
openModal(stageModal);
|
||||
} else {
|
||||
showError("获取Stage详情失败: " + (result.message || "未知错误"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取Stage详情错误:", error);
|
||||
showError("获取Stage详情错误: " + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const stageId = stageIdInput.value;
|
||||
const isEdit = !!stageId;
|
||||
|
||||
const stageData = {
|
||||
name: stageNameInput.value,
|
||||
project_id: projectId,
|
||||
demand_id: stageTreeIdSelect.value,
|
||||
};
|
||||
|
||||
showLoading();
|
||||
try {
|
||||
const url = isEdit
|
||||
? `${API_BASE_URL}/project/stage/${projectId}/${stageId}/`
|
||||
: `${API_BASE_URL}/project/stage/`;
|
||||
|
||||
const method = isEdit ? "PATCH" : "POST";
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(stageData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
showMessage(isEdit ? "Stage更新成功" : "Stage创建成功");
|
||||
closeModal(stageModal);
|
||||
fetchStages(); // 刷新列表
|
||||
} else {
|
||||
showError(
|
||||
(isEdit ? "更新" : "创建") +
|
||||
"Stage失败: " +
|
||||
(result.message || "未知错误")
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(isEdit ? "更新Stage错误:" : "创建Stage错误:", error);
|
||||
showError((isEdit ? "更新" : "创建") + "Stage错误: " + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
// 打开删除确认对话框
|
||||
function openDeleteConfirmation(stageId) {
|
||||
currentDeleteId = stageId;
|
||||
openModal(confirmModal);
|
||||
}
|
||||
|
||||
// 删除Stage
|
||||
async function deleteStage() {
|
||||
if (!currentDeleteId) return;
|
||||
|
||||
showLoading();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/project/stage/${project_id}/${currentDeleteId}/`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
showMessage("Stage删除成功");
|
||||
closeModal(confirmModal);
|
||||
fetchStages(); // 刷新列表
|
||||
} else {
|
||||
showError("删除Stage失败: " + (result.message || "未知错误"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除Stage错误:", error);
|
||||
showError("删除Stage错误: " + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
currentDeleteId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:打开模态框
|
||||
function openModal(modal) {
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
// 工具函数:关闭模态框
|
||||
function closeModal(modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
// 工具函数:显示加载中
|
||||
function showLoading() {
|
||||
loading.style.display = "flex";
|
||||
}
|
||||
|
||||
// 工具函数:隐藏加载中
|
||||
function hideLoading() {
|
||||
loading.style.display = "none";
|
||||
}
|
||||
|
||||
// 工具函数:显示消息
|
||||
function showMessage(message) {
|
||||
alert(message); // 简单实现,可以替换为更友好的通知
|
||||
}
|
||||
|
||||
// 工具函数:显示错误
|
||||
function showError(message) {
|
||||
alert("错误: " + message); // 简单实现,可以替换为更友好的通知
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,726 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Endpoint管理</title>
|
||||
<link rel="stylesheet" href="/assets/common.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
.btn-danger {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background-color: #ff7875;
|
||||
}
|
||||
.btn-warning {
|
||||
background-color: #fa8c16;
|
||||
}
|
||||
.btn-warning:hover {
|
||||
background-color: #ffa940;
|
||||
}
|
||||
.btn-back {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
.table th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 500;
|
||||
}
|
||||
.table tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.action-cell {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
padding: 24px;
|
||||
border-radius: 4px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.close {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.notification {
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
.success {
|
||||
background-color: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
}
|
||||
.error {
|
||||
background-color: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.breadcrumb a {
|
||||
color: #1890ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="breadcrumb">
|
||||
<a href="javascript:void(0);" onclick="history.back()"
|
||||
>返回 Stage 管理</a
|
||||
>
|
||||
/ Endpoint管理
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">Endpoint管理</h1>
|
||||
<div>
|
||||
<button class="btn" id="createBtn">添加 Endpoint</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="notification" class="notification"></div>
|
||||
|
||||
<div id="loading" class="loading" style="display: none">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<div id="endpointsTable" class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>URL</th>
|
||||
<th>请求方法</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="endpointsList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="empty-state" style="display: none">
|
||||
<p>暂无 Endpoint 数据,请添加新 Endpoint</p>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑 Modal -->
|
||||
<div id="endpointModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="modalTitle">添加 Endpoint</h2>
|
||||
<span class="close" id="closeModal">×</span>
|
||||
</div>
|
||||
<form id="endpointForm">
|
||||
<input type="hidden" id="endpointId" />
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">名称:</label>
|
||||
<input type="text" class="form-input" id="name" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="url">URL:</label>
|
||||
<input type="text" class="form-input" id="url" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="method">请求方法:</label>
|
||||
<select class="form-select" id="method" required>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="description">描述:</label>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
id="description"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn" id="cancelBtn">取消</button>
|
||||
<button type="submit" class="btn" id="saveBtn">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看详情 Modal -->
|
||||
<div id="viewModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Endpoint详情</h2>
|
||||
<span class="close" id="closeViewModal">×</span>
|
||||
</div>
|
||||
<div id="endpointDetails"></div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn" id="closeViewBtn">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认删除 Modal -->
|
||||
<div id="confirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">确认删除</h2>
|
||||
<span class="close" id="closeConfirmModal">×</span>
|
||||
</div>
|
||||
<p>确定要删除此Endpoint吗?此操作不可恢复。</p>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn" id="cancelDeleteBtn">取消</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 基础API URL
|
||||
const API_BASE_URL =
|
||||
"http://127.0.0.1:5002/api/project/stage/tools/endpoint";
|
||||
|
||||
// 从URL获取参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const projectId = urlParams.get("project_id");
|
||||
const stageId = urlParams.get("stage_id");
|
||||
|
||||
// DOM 元素
|
||||
const endpointsList = document.getElementById("endpointsList");
|
||||
const emptyState = document.getElementById("emptyState");
|
||||
const endpointsTable = document.getElementById("endpointsTable");
|
||||
const createBtn = document.getElementById("createBtn");
|
||||
const notification = document.getElementById("notification");
|
||||
const loading = document.getElementById("loading");
|
||||
|
||||
// Modal 元素
|
||||
const endpointModal = document.getElementById("endpointModal");
|
||||
const viewModal = document.getElementById("viewModal");
|
||||
const confirmModal = document.getElementById("confirmModal");
|
||||
const modalTitle = document.getElementById("modalTitle");
|
||||
const endpointForm = document.getElementById("endpointForm");
|
||||
const endpointIdInput = document.getElementById("endpointId");
|
||||
const nameInput = document.getElementById("name");
|
||||
const urlInput = document.getElementById("url");
|
||||
const methodInput = document.getElementById("method");
|
||||
const descriptionInput = document.getElementById("description");
|
||||
const endpointDetails = document.getElementById("endpointDetails");
|
||||
|
||||
// 按钮元素
|
||||
const closeModal = document.getElementById("closeModal");
|
||||
const closeViewModal = document.getElementById("closeViewModal");
|
||||
const closeConfirmModal = document.getElementById("closeConfirmModal");
|
||||
const cancelBtn = document.getElementById("cancelBtn");
|
||||
const closeViewBtn = document.getElementById("closeViewBtn");
|
||||
const cancelDeleteBtn = document.getElementById("cancelDeleteBtn");
|
||||
const confirmDeleteBtn = document.getElementById("confirmDeleteBtn");
|
||||
|
||||
// 当前要删除的ID
|
||||
let currentDeleteId = null;
|
||||
|
||||
// 页面加载时检查参数并获取数据
|
||||
|
||||
if (!projectId || !stageId) {
|
||||
showNotification(
|
||||
"缺少必要的项目ID或Stage ID,请返回Stage管理页面重试",
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchEndpoints();
|
||||
|
||||
// 添加按钮事件监听
|
||||
createBtn.addEventListener("click", showCreateModal);
|
||||
endpointForm.addEventListener("submit", handleFormSubmit);
|
||||
|
||||
// 关闭Modal的事件
|
||||
closeModal.addEventListener("click", () => closeModalFunc(endpointModal));
|
||||
closeViewModal.addEventListener("click", () => closeModalFunc(viewModal));
|
||||
closeConfirmModal.addEventListener("click", () =>
|
||||
closeModalFunc(confirmModal)
|
||||
);
|
||||
cancelBtn.addEventListener("click", () => closeModalFunc(endpointModal));
|
||||
closeViewBtn.addEventListener("click", () => closeModalFunc(viewModal));
|
||||
cancelDeleteBtn.addEventListener("click", () =>
|
||||
closeModalFunc(confirmModal)
|
||||
);
|
||||
confirmDeleteBtn.addEventListener("click", deleteEndpoint);
|
||||
|
||||
// 获取Endpoints列表
|
||||
async function fetchEndpoints() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/list/${projectId}/${stageId}/`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.length === 0) {
|
||||
endpointsTable.style.display = "none";
|
||||
emptyState.style.display = "block";
|
||||
} else {
|
||||
renderEndpointsList(data.data);
|
||||
endpointsTable.style.display = "block";
|
||||
emptyState.style.display = "none";
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification(`获取Endpoint列表失败: ${error.message}`, true);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染Endpoints列表
|
||||
function renderEndpointsList(endpoints) {
|
||||
endpointsList.innerHTML = "";
|
||||
|
||||
endpoints.forEach((endpoint) => {
|
||||
// 截取ID以便显示
|
||||
const shortId = endpoint.id
|
||||
? endpoint.id.substring(0, 8) + "..."
|
||||
: "-";
|
||||
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td title="${endpoint.id}">${shortId}</td>
|
||||
<td>${endpoint.name || "-"}</td>
|
||||
<td>${endpoint.path || "-"}</td>
|
||||
<td>${endpoint.method || "-"}</td>
|
||||
<td class="action-cell">
|
||||
<button class="btn" data-id="${
|
||||
endpoint.id
|
||||
}" data-action="view">查看</button>
|
||||
<button class="btn" data-id="${
|
||||
endpoint.id
|
||||
}" data-action="edit">编辑</button>
|
||||
<button class="btn btn-warning" data-id="${
|
||||
endpoint.id
|
||||
}" data-action="debug"
|
||||
data-url="${endpoint.path || ""}" data-method="${
|
||||
endpoint.method || "GET"
|
||||
}">调试</button>
|
||||
<button class="btn btn-danger" data-id="${
|
||||
endpoint.id
|
||||
}" data-action="delete">删除</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
endpointsList.appendChild(row);
|
||||
});
|
||||
|
||||
// 为按钮添加事件
|
||||
document.querySelectorAll("[data-action]").forEach((button) => {
|
||||
button.addEventListener("click", handleActionClick);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理操作按钮点击
|
||||
async function handleActionClick(event) {
|
||||
const action = event.target.getAttribute("data-action");
|
||||
const id = event.target.getAttribute("data-id");
|
||||
|
||||
switch (action) {
|
||||
case "view":
|
||||
await viewEndpoint(id);
|
||||
break;
|
||||
case "edit":
|
||||
await showEditModal(id);
|
||||
break;
|
||||
case "delete":
|
||||
showDeleteConfirmation(id);
|
||||
break;
|
||||
case "debug":
|
||||
const url = event.target.getAttribute("data-url");
|
||||
const method = event.target.getAttribute("data-method");
|
||||
goToDebugger(id, url, method);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到调试页面
|
||||
function goToDebugger(id, url, method) {
|
||||
window.location.href = `/project/stage/utils/endpoint_debug?project_id=${projectId}&stage_id=${stageId}&endpoint_id=${id}`;
|
||||
}
|
||||
|
||||
// 显示创建Modal
|
||||
function showCreateModal() {
|
||||
endpointForm.reset();
|
||||
endpointIdInput.value = "";
|
||||
modalTitle.textContent = "添加 Endpoint";
|
||||
openModalFunc(endpointModal);
|
||||
}
|
||||
|
||||
// 显示编辑Modal
|
||||
async function showEditModal(id) {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/${id}/`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const endpointT = await response.json();
|
||||
const endpoint = endpointT.data;
|
||||
|
||||
endpointIdInput.value = endpoint.id;
|
||||
nameInput.value = endpoint.name || "";
|
||||
urlInput.value = endpoint.path || "";
|
||||
methodInput.value = endpoint.method || "GET";
|
||||
descriptionInput.value = endpoint.description || "";
|
||||
|
||||
modalTitle.textContent = "编辑 Endpoint";
|
||||
openModalFunc(endpointModal);
|
||||
} catch (error) {
|
||||
showNotification(`获取Endpoint详情失败: ${error.message}`, true);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 查看Endpoint详情
|
||||
async function viewEndpoint(id) {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/${id}/`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status}`);
|
||||
}
|
||||
const endpointT = await response.json();
|
||||
const endpoint = endpointT.data;
|
||||
|
||||
// 格式化日期
|
||||
const createdDate = endpoint.created_at
|
||||
? new Date(endpoint.created_at).toLocaleString()
|
||||
: "-";
|
||||
const updatedDate = endpoint.updated_at
|
||||
? new Date(endpoint.updated_at).toLocaleString()
|
||||
: "-";
|
||||
|
||||
endpointDetails.innerHTML = `
|
||||
<div class="form-group">
|
||||
<label class="form-label">ID:</label>
|
||||
<p>${endpoint.id}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">名称:</label>
|
||||
<p>${endpoint.name || "-"}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">URL:</label>
|
||||
<p>${endpoint.path || "-"}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">请求方法:</label>
|
||||
<p>${endpoint.method || "-"}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">项目ID:</label>
|
||||
<p>${endpoint.project_id}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Stage ID:</label>
|
||||
<p>${endpoint.stage_id}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">描述:</label>
|
||||
<p>${endpoint.description || "-"}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">创建时间:</label>
|
||||
<p>${createdDate}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">更新时间:</label>
|
||||
<p>${updatedDate}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModalFunc(viewModal);
|
||||
} catch (error) {
|
||||
showNotification(`获取Endpoint详情失败: ${error.message}`, true);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
async function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const id = endpointIdInput.value;
|
||||
const isUpdate = !!id;
|
||||
|
||||
const endpointData = {
|
||||
name: nameInput.value.trim(),
|
||||
url: urlInput.value.trim(),
|
||||
method: methodInput.value,
|
||||
project_id: projectId,
|
||||
stage_id: stageId,
|
||||
description: descriptionInput.value.trim(),
|
||||
};
|
||||
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const url = isUpdate ? `${API_BASE_URL}/${id}/` : API_BASE_URL;
|
||||
const method = isUpdate ? "PATCH" : "POST";
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(endpointData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status}`);
|
||||
}
|
||||
|
||||
closeModalFunc(endpointModal);
|
||||
showNotification(`Endpoint ${isUpdate ? "更新" : "创建"}成功!`);
|
||||
fetchEndpoints();
|
||||
} catch (error) {
|
||||
showNotification(
|
||||
`${isUpdate ? "更新" : "创建"} Endpoint失败: ${error.message}`,
|
||||
true
|
||||
);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示删除确认
|
||||
function showDeleteConfirmation(id) {
|
||||
currentDeleteId = id;
|
||||
openModalFunc(confirmModal);
|
||||
}
|
||||
|
||||
// 删除Endpoint
|
||||
async function deleteEndpoint() {
|
||||
if (!currentDeleteId) return;
|
||||
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/${currentDeleteId}/`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status}`);
|
||||
}
|
||||
|
||||
closeModalFunc(confirmModal);
|
||||
showNotification("Endpoint删除成功!");
|
||||
fetchEndpoints();
|
||||
} catch (error) {
|
||||
showNotification(`删除Endpoint失败: ${error.message}`, true);
|
||||
} finally {
|
||||
hideLoading();
|
||||
currentDeleteId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示Modal
|
||||
function openModalFunc(modal) {
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
// 关闭Modal
|
||||
function closeModalFunc(modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, isError = false) {
|
||||
notification.textContent = message;
|
||||
notification.className = isError
|
||||
? "notification error"
|
||||
: "notification success";
|
||||
notification.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.display = "none";
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 显示加载
|
||||
function showLoading() {
|
||||
loading.style.display = "flex";
|
||||
}
|
||||
|
||||
// 隐藏加载
|
||||
function hideLoading() {
|
||||
loading.style.display = "none";
|
||||
}
|
||||
|
||||
// 关闭Modal当点击外部
|
||||
window.addEventListener("click", (event) => {
|
||||
if (event.target === endpointModal) {
|
||||
closeModalFunc(endpointModal);
|
||||
}
|
||||
if (event.target === viewModal) {
|
||||
closeModalFunc(viewModal);
|
||||
}
|
||||
if (event.target === confirmModal) {
|
||||
closeModalFunc(confirmModal);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,664 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>API Debugger</title>
|
||||
<!-- Include Tailwind CSS via CDN -->
|
||||
<!-- <script src="https://cdn.tailwindcss.com"></script> -->
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800">
|
||||
<div id="notifications" class="fixed top-5 right-5 z-50"></div>
|
||||
|
||||
<div class="container mx-auto p-4 max-w-6xl">
|
||||
<header
|
||||
class="flex justify-between items-center py-4 mb-6 border-b border-gray-200"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="text-2xl mr-2">⚙️</span>
|
||||
<h1 class="text-2xl font-bold text-blue-600">API Debugger</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-5 mb-6">
|
||||
<div class="flex flex-col md:flex-row mb-4">
|
||||
<select
|
||||
id="method-select"
|
||||
class="md:w-32 w-full p-2.5 border border-gray-300 rounded-t md:rounded-tr-none md:rounded-l text-blue-600 font-bold mb-2 md:mb-0"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
<option value="HEAD">HEAD</option>
|
||||
<option value="OPTIONS">OPTIONS</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
id="url-input"
|
||||
placeholder="Enter request URL"
|
||||
class="flex-grow p-2.5 border border-gray-300 md:rounded-none border-y md:border-l-0 md:border-r-0 mb-2 md:mb-0 outline-none"
|
||||
/>
|
||||
<button
|
||||
id="send-btn"
|
||||
class="p-2.5 bg-blue-600 text-white font-medium rounded-b md:rounded-bl-none md:rounded-r hover:bg-blue-700 transition"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Headers Section -->
|
||||
<div class="mb-4">
|
||||
<div
|
||||
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
|
||||
id="headers-accordion-header"
|
||||
>
|
||||
<span class="font-semibold">Headers</span>
|
||||
<span>▼</span>
|
||||
</div>
|
||||
<div
|
||||
class="border border-gray-200 border-t-0 p-4 rounded-b"
|
||||
id="headers-accordion-content"
|
||||
>
|
||||
<div id="headers-container">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="font-semibold text-lg">Request Headers</h3>
|
||||
<button
|
||||
id="add-header-btn"
|
||||
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
|
||||
>
|
||||
Add Header
|
||||
</button>
|
||||
</div>
|
||||
<div class="header-row flex mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="p-2 border border-gray-300 rounded-l flex-1"
|
||||
placeholder="Header name"
|
||||
value="Content-Type"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="p-2 border border-gray-300 border-l-0 flex-1"
|
||||
placeholder="Header value"
|
||||
value="application/json"
|
||||
/>
|
||||
<button
|
||||
class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameters Section -->
|
||||
<div class="mb-4">
|
||||
<div
|
||||
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
|
||||
id="params-accordion-header"
|
||||
>
|
||||
<span class="font-semibold">Query Parameters</span>
|
||||
<span>▼</span>
|
||||
</div>
|
||||
<div
|
||||
class="border border-gray-200 border-t-0 p-4 rounded-b hidden"
|
||||
id="params-accordion-content"
|
||||
>
|
||||
<div id="params-container">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="font-semibold text-lg">Query Parameters</h3>
|
||||
<button
|
||||
id="add-param-btn"
|
||||
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
|
||||
>
|
||||
Add Parameter
|
||||
</button>
|
||||
</div>
|
||||
<!-- Parameters will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Body Section -->
|
||||
<div class="mb-4">
|
||||
<div
|
||||
class="bg-gray-100 p-3 flex justify-between items-center cursor-pointer rounded-t"
|
||||
id="body-accordion-header"
|
||||
>
|
||||
<span class="font-semibold">Request Body</span>
|
||||
<span>▼</span>
|
||||
</div>
|
||||
<div
|
||||
class="border border-gray-200 border-t-0 p-4 rounded-b hidden"
|
||||
id="body-accordion-content"
|
||||
>
|
||||
<div>
|
||||
<div class="flex mb-3" id="body-type-selector">
|
||||
<div
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-l cursor-pointer body-type-option bg-blue-600 text-white"
|
||||
data-type="json"
|
||||
>
|
||||
JSON
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1.5 border-t border-b border-r border-gray-300 cursor-pointer body-type-option"
|
||||
data-type="form"
|
||||
>
|
||||
Form Data
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1.5 border-t border-b border-r border-gray-300 cursor-pointer body-type-option"
|
||||
data-type="x-www-form-urlencoded"
|
||||
>
|
||||
x-www-form-urlencoded
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1.5 border-t border-b border-r border-gray-300 rounded-r cursor-pointer body-type-option"
|
||||
data-type="text"
|
||||
>
|
||||
Text
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
id="body-editor"
|
||||
class="w-full h-48 p-3 border border-gray-300 rounded font-mono resize-y"
|
||||
placeholder="Enter request body here"
|
||||
>
|
||||
{\n "key": "value"\n}</textarea
|
||||
>
|
||||
<div id="form-data-container" class="hidden">
|
||||
<div class="flex justify-between items-center mb-3 mt-4">
|
||||
<h3 class="font-semibold text-lg">Form Data</h3>
|
||||
<button
|
||||
id="add-form-field-btn"
|
||||
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition"
|
||||
>
|
||||
Add Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response Section -->
|
||||
<div class="bg-white rounded-lg shadow-md p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Response</h3>
|
||||
<div id="response-time" class="text-gray-600"></div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between items-center mb-4 pb-3 border-b border-gray-200"
|
||||
>
|
||||
<div id="response-status" class="font-bold">No request sent yet</div>
|
||||
<div id="response-size" class="text-gray-600"></div>
|
||||
</div>
|
||||
<pre
|
||||
id="response-body"
|
||||
class="bg-gray-800 text-white p-4 rounded overflow-auto max-h-96 font-mono"
|
||||
>
|
||||
Send a request to see the response</pre
|
||||
>
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button
|
||||
id="copy-response-btn"
|
||||
class="px-3 py-1.5 bg-gray-700 text-white rounded hover:bg-gray-600 transition flex items-center"
|
||||
>
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// DOM Elements
|
||||
const methodSelect = document.getElementById("method-select");
|
||||
const urlInput = document.getElementById("url-input");
|
||||
const sendBtn = document.getElementById("send-btn");
|
||||
const addHeaderBtn = document.getElementById("add-header-btn");
|
||||
const headersContainer = document.getElementById("headers-container");
|
||||
const addParamBtn = document.getElementById("add-param-btn");
|
||||
const paramsContainer = document.getElementById("params-container");
|
||||
const bodyTypeOptions = document.querySelectorAll(".body-type-option");
|
||||
const bodyEditor = document.getElementById("body-editor");
|
||||
const formDataContainer = document.getElementById("form-data-container");
|
||||
const addFormFieldBtn = document.getElementById("add-form-field-btn");
|
||||
const responseStatus = document.getElementById("response-status");
|
||||
const responseTime = document.getElementById("response-time");
|
||||
const responseSize = document.getElementById("response-size");
|
||||
const responseBody = document.getElementById("response-body");
|
||||
const copyResponseBtn = document.getElementById("copy-response-btn");
|
||||
const notificationsContainer = document.getElementById("notifications");
|
||||
|
||||
// Accordion elements
|
||||
const accordionHeaders = document.querySelectorAll(
|
||||
'[id$="accordion-header"]'
|
||||
);
|
||||
const accordionContents = document.querySelectorAll(
|
||||
'[id$="accordion-content"]'
|
||||
);
|
||||
|
||||
// State management
|
||||
let currentBodyType = "json";
|
||||
let requestStartTime = 0;
|
||||
let latestResponse = null;
|
||||
|
||||
// Accordion functionality
|
||||
accordionHeaders.forEach((header) => {
|
||||
header.addEventListener("click", () => {
|
||||
const contentId = header.id.replace("-header", "-content");
|
||||
const content = document.getElementById(contentId);
|
||||
content.classList.toggle("hidden");
|
||||
|
||||
// Update the arrow icon
|
||||
const arrow = header.querySelector("span:last-child");
|
||||
if (!content.classList.contains("hidden")) {
|
||||
arrow.textContent = "▼";
|
||||
} else {
|
||||
arrow.textContent = "▶";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Request Headers
|
||||
addHeaderBtn.addEventListener("click", addNewHeader);
|
||||
|
||||
// Add initial header row functionality
|
||||
document
|
||||
.querySelector(".header-row button")
|
||||
.addEventListener("click", function () {
|
||||
this.closest(".header-row").remove();
|
||||
});
|
||||
|
||||
// Query Parameters
|
||||
addParamBtn.addEventListener("click", addNewParam);
|
||||
|
||||
// Request Body Type Selection
|
||||
bodyTypeOptions.forEach((option) => {
|
||||
option.addEventListener("click", () => {
|
||||
bodyTypeOptions.forEach((opt) => {
|
||||
opt.classList.remove("bg-blue-600", "text-white");
|
||||
});
|
||||
option.classList.add("bg-blue-600", "text-white");
|
||||
currentBodyType = option.getAttribute("data-type");
|
||||
|
||||
if (currentBodyType === "form") {
|
||||
bodyEditor.classList.add("hidden");
|
||||
formDataContainer.classList.remove("hidden");
|
||||
} else {
|
||||
bodyEditor.classList.remove("hidden");
|
||||
formDataContainer.classList.add("hidden");
|
||||
|
||||
// Set appropriate placeholder based on body type
|
||||
switch (currentBodyType) {
|
||||
case "json":
|
||||
bodyEditor.placeholder = "Enter JSON body here";
|
||||
bodyEditor.value =
|
||||
bodyEditor.value || '{\n "key": "value"\n}';
|
||||
break;
|
||||
case "x-www-form-urlencoded":
|
||||
bodyEditor.placeholder = "key1=value1&key2=value2";
|
||||
bodyEditor.value = bodyEditor.value || "key=value";
|
||||
break;
|
||||
case "text":
|
||||
bodyEditor.placeholder = "Enter plain text body here";
|
||||
bodyEditor.value = bodyEditor.value || "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Form Data Fields
|
||||
addFormFieldBtn.addEventListener("click", addNewFormField);
|
||||
|
||||
// Send Request
|
||||
sendBtn.addEventListener("click", sendRequest);
|
||||
|
||||
// Response Actions
|
||||
copyResponseBtn.addEventListener("click", copyResponseToClipboard);
|
||||
|
||||
/**
|
||||
* Add a new header row to the headers container
|
||||
*/
|
||||
function addNewHeader() {
|
||||
const headerRow = document.createElement("div");
|
||||
headerRow.className = "header-row flex mb-2";
|
||||
headerRow.innerHTML = `
|
||||
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Header name">
|
||||
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Header value">
|
||||
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
|
||||
`;
|
||||
headersContainer.appendChild(headerRow);
|
||||
|
||||
// Add event listener to the remove button
|
||||
headerRow
|
||||
.querySelector("button")
|
||||
.addEventListener("click", function () {
|
||||
this.closest(".header-row").remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new query parameter row
|
||||
*/
|
||||
function addNewParam() {
|
||||
const paramRow = document.createElement("div");
|
||||
paramRow.className = "param-row flex mb-2";
|
||||
paramRow.innerHTML = `
|
||||
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Parameter name">
|
||||
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Parameter value">
|
||||
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
|
||||
`;
|
||||
paramsContainer.appendChild(paramRow);
|
||||
|
||||
// Add event listener to the remove button
|
||||
paramRow.querySelector("button").addEventListener("click", function () {
|
||||
this.closest(".param-row").remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new form field row for form data body type
|
||||
*/
|
||||
function addNewFormField() {
|
||||
const formFieldRow = document.createElement("div");
|
||||
formFieldRow.className = "param-row flex mb-2";
|
||||
formFieldRow.innerHTML = `
|
||||
<input type="text" class="p-2 border border-gray-300 rounded-l flex-1" placeholder="Field name">
|
||||
<input type="text" class="p-2 border border-gray-300 border-l-0 flex-1" placeholder="Field value">
|
||||
<button class="p-2 bg-red-500 text-white rounded-r hover:bg-red-600 transition">×</button>
|
||||
`;
|
||||
formDataContainer.appendChild(formFieldRow);
|
||||
|
||||
// Add event listener to the remove button
|
||||
formFieldRow
|
||||
.querySelector("button")
|
||||
.addEventListener("click", function () {
|
||||
this.closest(".param-row").remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect headers from the UI
|
||||
* @returns {Object} - Headers object
|
||||
*/
|
||||
function getHeaders() {
|
||||
const headers = {};
|
||||
const headerRows = headersContainer.querySelectorAll(".header-row");
|
||||
|
||||
headerRows.forEach((row) => {
|
||||
const key = row.querySelector("input:nth-child(1)").value.trim();
|
||||
const value = row.querySelector("input:nth-child(2)").value.trim();
|
||||
|
||||
if (key && value) {
|
||||
headers[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query string from parameters
|
||||
* @returns {string} - Query string
|
||||
*/
|
||||
function buildQueryString() {
|
||||
const params = [];
|
||||
const paramRows = paramsContainer.querySelectorAll(".param-row");
|
||||
|
||||
paramRows.forEach((row) => {
|
||||
const key = row.querySelector("input:nth-child(1)").value.trim();
|
||||
const value = row.querySelector("input:nth-child(2)").value.trim();
|
||||
|
||||
if (key && value) {
|
||||
params.push(
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return params.length > 0 ? `?${params.join("&")}` : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request body based on the selected body type
|
||||
* @returns {string|FormData|null} - Request body
|
||||
*/
|
||||
function getRequestBody() {
|
||||
if (methodSelect.value === "GET" || methodSelect.value === "HEAD") {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (currentBodyType) {
|
||||
case "json":
|
||||
try {
|
||||
// Try to parse JSON to validate it
|
||||
JSON.parse(bodyEditor.value);
|
||||
return bodyEditor.value;
|
||||
} catch (error) {
|
||||
showNotification(`Invalid JSON: ${error.message}`, "error");
|
||||
return null;
|
||||
}
|
||||
case "form":
|
||||
const formData = new FormData();
|
||||
const formFields = formDataContainer.querySelectorAll(".param-row");
|
||||
|
||||
formFields.forEach((field) => {
|
||||
const key = field
|
||||
.querySelector("input:nth-child(1)")
|
||||
.value.trim();
|
||||
const value = field
|
||||
.querySelector("input:nth-child(2)")
|
||||
.value.trim();
|
||||
|
||||
if (key) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
case "x-www-form-urlencoded":
|
||||
return bodyEditor.value;
|
||||
case "text":
|
||||
return bodyEditor.value;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the API request
|
||||
*/
|
||||
function sendRequest() {
|
||||
// Get the URL
|
||||
|
||||
const url = urlInput.value;
|
||||
console.log(url);
|
||||
if (!url) {
|
||||
showNotification("Please enter a URL", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add query string if there are query parameters
|
||||
const queryString = buildQueryString();
|
||||
const fullUrl = `${url}${queryString}`;
|
||||
|
||||
// Get request headers
|
||||
const headers = getHeaders();
|
||||
|
||||
// Get request body based on body type
|
||||
const body = getRequestBody();
|
||||
if (body === null && currentBodyType === "json") {
|
||||
return; // Stop if JSON is invalid
|
||||
}
|
||||
console.log(fullUrl);
|
||||
// Set loading state
|
||||
responseBody.textContent = "Loading...";
|
||||
responseStatus.textContent = "Sending request...";
|
||||
responseStatus.className = "font-bold";
|
||||
responseSize.textContent = "";
|
||||
requestStartTime = Date.now();
|
||||
|
||||
// Create fetch options
|
||||
const fetchOptions = {
|
||||
method: methodSelect.value,
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
// Add body to request if it's not GET or HEAD
|
||||
if (
|
||||
methodSelect.value !== "GET" &&
|
||||
methodSelect.value !== "HEAD" &&
|
||||
body
|
||||
) {
|
||||
fetchOptions.body = body;
|
||||
}
|
||||
|
||||
// Make the fetch request
|
||||
fetch(fullUrl, fetchOptions)
|
||||
.then((response) => {
|
||||
const endTime = Date.now();
|
||||
const responseTimeMs = endTime - requestStartTime;
|
||||
responseTime.textContent = `Time: ${responseTimeMs}ms`;
|
||||
|
||||
// Set status
|
||||
responseStatus.textContent = `Status: ${response.status} ${response.statusText}`;
|
||||
if (response.ok) {
|
||||
responseStatus.className = "font-bold text-green-600";
|
||||
} else {
|
||||
responseStatus.className = "font-bold text-red-600";
|
||||
}
|
||||
|
||||
// Get response size
|
||||
const contentLength = response.headers.get("content-length");
|
||||
if (contentLength) {
|
||||
const size = parseInt(contentLength, 10);
|
||||
responseSize.textContent = `Size: ${formatBytes(size)}`;
|
||||
}
|
||||
|
||||
// Store the response object for later use
|
||||
latestResponse = response;
|
||||
|
||||
// Clone the response so we can use it multiple times
|
||||
return response.text().then((text) => {
|
||||
try {
|
||||
// Try to parse as JSON
|
||||
const json = JSON.parse(text);
|
||||
return JSON.stringify(json, null, 2);
|
||||
} catch (e) {
|
||||
// If not JSON, return as text
|
||||
return text;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then((responseText) => {
|
||||
// Display the response
|
||||
responseBody.textContent = responseText;
|
||||
})
|
||||
.catch((error) => {
|
||||
responseStatus.textContent = `Error: ${error.message}`;
|
||||
responseStatus.className = "font-bold text-red-600";
|
||||
responseBody.textContent = `Failed to fetch: ${error.message}`;
|
||||
responseSize.textContent = "";
|
||||
|
||||
showNotification(`Request failed: ${error.message}`, "error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to a human-readable format
|
||||
* @param {number} bytes - The size in bytes
|
||||
* @returns {string} - Formatted size
|
||||
*/
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy response to clipboard
|
||||
*/
|
||||
function copyResponseToClipboard() {
|
||||
if (
|
||||
responseBody.textContent.trim() ===
|
||||
"Send a request to see the response" ||
|
||||
responseBody.textContent.trim() === "Loading..."
|
||||
) {
|
||||
showNotification("No response to copy", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(responseBody.textContent)
|
||||
.then(() => {
|
||||
showNotification("Response copied to clipboard", "success");
|
||||
})
|
||||
.catch((err) => {
|
||||
showNotification("Failed to copy response", "error");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification
|
||||
*/
|
||||
function showNotification(message, type = "success") {
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `mb-3 p-3 rounded-lg shadow-md border-l-4 ${
|
||||
type === "success"
|
||||
? "bg-green-100 border-green-500 text-green-800"
|
||||
: "bg-red-100 border-red-500 text-red-800"
|
||||
}`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex justify-between items-center">
|
||||
<div>${message}</div>
|
||||
<button class="ml-4 text-gray-600 hover:text-gray-800">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to notifications container
|
||||
notificationsContainer.appendChild(notification);
|
||||
|
||||
// Add event listener to close button
|
||||
notification.querySelector("button").addEventListener("click", () => {
|
||||
notification.remove();
|
||||
});
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Set up keyboard shortcuts
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Ctrl+Enter to send request
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||
sendRequest();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Open the headers section by default
|
||||
const headersContent = document.getElementById(
|
||||
"headers-accordion-content"
|
||||
);
|
||||
headersContent.classList.remove("hidden");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user