Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "nonebot-plugin-mimo-console"
version = "0.1.10"
version = "0.1.11"
description = "A lightweight WebUI management console for NoneBot2"
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -58,7 +58,7 @@ requires = ["uv_build>=0.10.0,<0.12.0"]
build-backend = "uv_build"

[tool.bumpversion]
current_version = "0.1.10"
current_version = "0.1.11"
commit = true
message = "chore: 发布 v{new_version}"
tag = true
Expand Down
72 changes: 70 additions & 2 deletions src/nonebot_plugin_mimo_console/static/assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const state = {
logs: [],
logAfter: 0,
logFollow: true,
logLevel: "ALL",
timers: [],
detailPlugin: null,
detailSource: null,
Expand Down Expand Up @@ -177,7 +178,7 @@ function bindEvents() {
$("#dependency-install-form").addEventListener("submit", installDependency);
$("#config-search").addEventListener("input", renderConfig);
$("#log-search").addEventListener("input", renderLogs);
$("#log-level").addEventListener("change", renderLogs);
bindLogLevelSelect();
$("#log-follow").addEventListener("click", () => {
state.logFollow = !state.logFollow;
$("#log-follow").classList.toggle("active", state.logFollow);
Expand Down Expand Up @@ -1453,9 +1454,76 @@ function renderRecentLogs() {
: '<div class="empty-text">正在等待日志…</div>';
}

function setLogLevelSelectOpen(open, focusOption = false) {
const root = $("#log-level-select");
const trigger = $("#log-level-trigger");
const menu = $("#log-level-menu");
root.classList.toggle("open", open);
menu.classList.toggle("hidden", !open);
trigger.setAttribute("aria-expanded", String(open));
if (open && focusOption) {
const selected = $('.custom-select-option[aria-selected="true"]', menu);
(selected || $(".custom-select-option", menu))?.focus();
}
}

function selectLogLevel(value, { focusTrigger = true } = {}) {
const option = $(`.custom-select-option[data-value="${CSS.escape(value)}"]`, $("#log-level-menu"));
if (!option) return;
state.logLevel = value;
$("#log-level-label").textContent = option.textContent.trim();
$$(".custom-select-option", $("#log-level-menu")).forEach((item) => {
const active = item === option;
item.classList.toggle("active", active);
item.setAttribute("aria-selected", String(active));
});
setLogLevelSelectOpen(false);
if (focusTrigger) $("#log-level-trigger").focus();
renderLogs();
}

function bindLogLevelSelect() {
const root = $("#log-level-select");
const trigger = $("#log-level-trigger");
const menu = $("#log-level-menu");
const options = $$(".custom-select-option", menu);

trigger.addEventListener("click", () => {
setLogLevelSelectOpen(!root.classList.contains("open"), true);
});
trigger.addEventListener("keydown", (event) => {
if (!["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) return;
event.preventDefault();
setLogLevelSelectOpen(true, true);
});
options.forEach((option) => {
option.addEventListener("click", () => selectLogLevel(option.dataset.value));
option.addEventListener("keydown", (event) => {
const index = options.indexOf(option);
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
const offset = event.key === "ArrowDown" ? 1 : -1;
options[(index + offset + options.length) % options.length].focus();
} else if (event.key === "Home" || event.key === "End") {
event.preventDefault();
options[event.key === "Home" ? 0 : options.length - 1].focus();
} else if (event.key === "Escape" || event.key === "Tab") {
setLogLevelSelectOpen(false);
if (event.key === "Escape") {
event.preventDefault();
trigger.focus();
}
}
});
});
document.addEventListener("click", (event) => {
if (!root.contains(event.target)) setLogLevelSelectOpen(false);
});
}

function renderLogs() {
const query = $("#log-search").value.trim().toLowerCase();
const level = $("#log-level").value;
const level = state.logLevel;
const items = state.logs.filter((item) =>
(level === "ALL" || item.level === level)
&& [item.message, item.module].join(" ").toLowerCase().includes(query),
Expand Down
91 changes: 90 additions & 1 deletion src/nonebot_plugin_mimo_console/static/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1931,12 +1931,97 @@ a { color: inherit; text-decoration: none; }

.log-toolbar .search-box { max-width: none; }

.select {
.custom-select {
position: relative;
width: 140px;
flex: 0 0 140px;
}

.custom-select-trigger {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 12px;
border: 1px solid var(--line-strong);
border-radius: var(--radius-sm);
outline: 0;
background: var(--surface);
color: var(--text-soft);
font-size: 13px;
transition: border-color .18s var(--ease), box-shadow .18s var(--ease), background .18s var(--ease);
}
.custom-select-trigger:hover { background: var(--surface-hover); }
.custom-select.open .custom-select-trigger,
.custom-select-trigger:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.custom-select-trigger svg {
flex: 0 0 auto;
color: var(--muted);
transition: transform .18s var(--ease);
}
.custom-select.open .custom-select-trigger svg { transform: rotate(180deg); }

.custom-select-menu {
position: absolute;
z-index: 85;
top: calc(100% + 7px);
right: 0;
width: 172px;
display: grid;
gap: 3px;
padding: 6px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
box-shadow: var(--shadow-md);
animation: select-menu-in .15s var(--ease);
}
@keyframes select-menu-in {
from { opacity: 0; transform: translateY(-4px) scale(.98); }
}

.custom-select-option {
width: 100%;
height: 34px;
display: flex;
align-items: center;
gap: 9px;
padding: 0 9px;
border-radius: 7px;
outline: 0;
color: var(--text-soft);
font-family: var(--mono);
font-size: 12px;
text-align: left;
transition: color .12s, background .12s;
}
.custom-select-option:hover,
.custom-select-option:focus-visible { background: var(--surface-hover); color: var(--text); }
.custom-select-option.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.custom-select-option.active::after {
content: "✓";
margin-left: auto;
font-family: var(--font);
}

.level-dot {
width: 7px;
height: 7px;
flex: 0 0 7px;
border-radius: 50%;
background: var(--muted-2);
}
.level-dot.INFO { background: var(--color-blue); }
.level-dot.SUCCESS { background: var(--color-green); }
.level-dot.WARNING { background: var(--color-yellow); }
.level-dot.ERROR,
.level-dot.CRITICAL { background: var(--color-red); }
.level-dot.DEBUG { background: var(--muted); }

.terminal {
overflow: hidden;
Expand Down Expand Up @@ -2492,6 +2577,8 @@ a { color: inherit; text-decoration: none; }
.field input,
.config-input,
.select,
.custom-select-trigger,
.custom-select-menu,
.view-toggle,
.net-item,
.radio-option,
Expand All @@ -2518,6 +2605,8 @@ a { color: inherit; text-decoration: none; }
.field input,
.config-input,
.select,
.custom-select-trigger,
.custom-select-menu,
.net-item,
.radio-option,
.file-upload-btn,
Expand Down
35 changes: 24 additions & 11 deletions src/nonebot_plugin_mimo_console/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./assets/styles.css?v=1.3.9">
<link rel="stylesheet" href="./assets/styles.css?v=1.3.10">
</head>
<body>
<div id="bg-layer" class="bg-layer"></div>
Expand Down Expand Up @@ -407,15 +407,28 @@ <h1>运行日志</h1>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input id="log-search" placeholder="搜索日志内容">
</div>
<select id="log-level" class="select">
<option value="ALL">全部级别</option>
<option>DEBUG</option>
<option>INFO</option>
<option>SUCCESS</option>
<option>WARNING</option>
<option>ERROR</option>
<option>CRITICAL</option>
</select>
<div id="log-level-select" class="custom-select log-level-select">
<button
id="log-level-trigger"
class="custom-select-trigger"
type="button"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="log-level-menu"
>
<span id="log-level-label">全部级别</span>
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div id="log-level-menu" class="custom-select-menu hidden" role="listbox" aria-label="日志级别">
<button class="custom-select-option active" type="button" role="option" aria-selected="true" data-value="ALL"><span class="level-dot ALL"></span>全部级别</button>
<button class="custom-select-option" type="button" role="option" aria-selected="false" data-value="DEBUG"><span class="level-dot DEBUG"></span>DEBUG</button>
<button class="custom-select-option" type="button" role="option" aria-selected="false" data-value="INFO"><span class="level-dot INFO"></span>INFO</button>
<button class="custom-select-option" type="button" role="option" aria-selected="false" data-value="SUCCESS"><span class="level-dot SUCCESS"></span>SUCCESS</button>
<button class="custom-select-option" type="button" role="option" aria-selected="false" data-value="WARNING"><span class="level-dot WARNING"></span>WARNING</button>
<button class="custom-select-option" type="button" role="option" aria-selected="false" data-value="ERROR"><span class="level-dot ERROR"></span>ERROR</button>
<button class="custom-select-option" type="button" role="option" aria-selected="false" data-value="CRITICAL"><span class="level-dot CRITICAL"></span>CRITICAL</button>
</div>
</div>
<button id="log-follow" class="btn btn-secondary btn-sm active">自动滚动</button>
<button id="clear-logs" class="btn btn-danger btn-sm">清空</button>
</div>
Expand Down Expand Up @@ -626,6 +639,6 @@ <h2 id="detail-title">插件详情</h2>
</div>

<div id="toast-stack" class="toast-stack" aria-live="polite"></div>
<script src="./assets/app.js?v=1.3.9" defer></script>
<script src="./assets/app.js?v=1.3.10" defer></script>
</body>
</html>
13 changes: 13 additions & 0 deletions tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ def test_github_proxy_tests_render_per_source_latency(self) -> None:
self.assertIn('.proxy-latency[data-status="timeout"]', styles)
self.assertIn('"status": "timeout"', api)

def test_log_level_filter_uses_custom_select(self) -> None:
static = ROOT / "src" / "nonebot_plugin_mimo_console" / "static"
index = (static / "index.html").read_text(encoding="utf-8")
script = (static / "assets" / "app.js").read_text(encoding="utf-8")
styles = (static / "assets" / "styles.css").read_text(encoding="utf-8")
self.assertNotIn('id="log-level" class="select"', index)
self.assertIn('id="log-level-trigger"', index)
self.assertIn('role="listbox"', index)
self.assertIn("function bindLogLevelSelect()", script)
self.assertIn("const level = state.logLevel;", script)
self.assertIn(".custom-select-menu", styles)
self.assertIn(".custom-select-option.active", styles)

def test_header_version_is_populated_from_runtime_metadata(self) -> None:
static = ROOT / "src" / "nonebot_plugin_mimo_console" / "static"
index = (static / "index.html").read_text(encoding="utf-8")
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading