mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat: add group filter to send logs view
- Add groupId query param to GET /api/logs endpoint - Add group filter dropdown in SendLogs component - Pass groups list and filter state from admin.vue - Update useLogs.ts to accept optional groupId parameter - Add groupId to SendRecord type - Add i18n keys and CSS for filter UI
This commit is contained in:
parent
03eef7ae89
commit
589558a20d
7 changed files with 94 additions and 16 deletions
|
|
@ -909,7 +909,38 @@ main {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.log-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.log-list {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,20 @@
|
|||
<div>
|
||||
<div class="log-toolbar">
|
||||
<span class="kpi-label">{{ t("logs.lastSends", { n: logs.length }) }}</span>
|
||||
<div class="log-filters">
|
||||
<label class="filter-label">{{ t("logs.filterGroup") }}</label>
|
||||
<select
|
||||
class="filter-select"
|
||||
:value="selectedGroupId"
|
||||
:disabled="loading"
|
||||
@change="onGroupFilter"
|
||||
>
|
||||
<option value="">{{ t("logs.allGroups") }}</option>
|
||||
<option v-for="g in groups" :key="g.id" :value="g.id">
|
||||
{{ g.name || g.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" :disabled="loading" @click="refresh">
|
||||
{{ t("logs.refresh") }}
|
||||
</button>
|
||||
|
|
@ -68,13 +82,22 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SendRecord } from "~/types";
|
||||
import type { Group, SendRecord } from "~/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { loadById } = useSendLogs();
|
||||
|
||||
const props = defineProps<{ logs: SendRecord[]; loading: boolean }>();
|
||||
const emit = defineEmits<{ (e: "refresh"): void }>();
|
||||
const props = defineProps<{
|
||||
logs: SendRecord[];
|
||||
loading: boolean;
|
||||
groups: Group[];
|
||||
selectedGroupId: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "refresh"): void;
|
||||
(e: "filter"): void;
|
||||
(e: "update:selectedGroupId", value: string): void;
|
||||
}>();
|
||||
|
||||
const detailOpen = ref(false);
|
||||
const detail = ref<SendRecord | null>(null);
|
||||
|
|
@ -121,6 +144,11 @@ function refresh(): void {
|
|||
emit("refresh");
|
||||
}
|
||||
|
||||
function onGroupFilter(e: Event): void {
|
||||
emit("update:selectedGroupId", (e.target as HTMLSelectElement).value);
|
||||
emit("filter");
|
||||
}
|
||||
|
||||
async function openDetail(l: SendRecord): Promise<void> {
|
||||
if (l.id == null) return;
|
||||
detailOpen.value = true;
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ const en: Dict = {
|
|||
"logs.no": "Failed",
|
||||
"logs.error": "Error",
|
||||
"logs.detail": "Extra Info",
|
||||
"logs.filterGroup": "Group",
|
||||
"logs.allGroups": "All groups",
|
||||
"toast.routesSaved": "Routes saved",
|
||||
"toast.routeDeleted": "Route deleted",
|
||||
"toast.groupSaved": "Groups saved",
|
||||
|
|
@ -311,6 +313,8 @@ const zh: Dict = {
|
|||
"logs.no": "失败",
|
||||
"logs.error": "错误信息",
|
||||
"logs.detail": "附加信息",
|
||||
"logs.filterGroup": "分组",
|
||||
"logs.allGroups": "全部分组",
|
||||
"toast.routesSaved": "路由已保存",
|
||||
"toast.routeDeleted": "路由已删除",
|
||||
"toast.groupSaved": "分组已保存",
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ export function useSendLogs() {
|
|||
const needLogin = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
async function load(limit = 50): Promise<void> {
|
||||
async function load(limit = 50, groupId?: string): Promise<void> {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
needLogin.value = false;
|
||||
try {
|
||||
const res = await fetch(`/admin/api/logs?limit=${limit}`, {
|
||||
let url = `/admin/api/logs?limit=${limit}`;
|
||||
if (groupId) url += `&groupId=${encodeURIComponent(groupId)}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { accept: "application/json" },
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
|
@ -44,4 +46,4 @@ export function useSendLogs() {
|
|||
}
|
||||
|
||||
return { logs, loading, needLogin, error, load, loadById };
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +156,15 @@
|
|||
<span>{{ logsLoading ? t("status.loading") : t("status.connected") }}</span>
|
||||
</div>
|
||||
</section>
|
||||
<SendLogs :logs="logs" :loading="logsLoading" @refresh="loadLogs" />
|
||||
<SendLogs
|
||||
:logs="logs"
|
||||
:loading="logsLoading"
|
||||
:groups="groups"
|
||||
:selected-group-id="logFilterGroup"
|
||||
@refresh="loadLogs"
|
||||
@filter="loadLogs"
|
||||
@update:selected-group-id="logFilterGroup = $event"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -214,19 +222,21 @@ const groupEditorOpen = ref(false);
|
|||
const editingGroup = ref<Group | null>(null);
|
||||
const savingGroup = ref(false);
|
||||
|
||||
const logFilterGroup = ref("");
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
forbidden.value = params.get("error") === "forbidden";
|
||||
}
|
||||
loadGroups();
|
||||
loadLogs();
|
||||
loadLogs(50, logFilterGroup.value || undefined);
|
||||
});
|
||||
|
||||
function switchView(next: "groups" | "logs"): void {
|
||||
view.value = next;
|
||||
if (next === "groups") loadGroups();
|
||||
if (next === "logs") loadLogs();
|
||||
if (next === "logs") loadLogs(50, logFilterGroup.value || undefined);
|
||||
}
|
||||
|
||||
function enterGroup(group: Group): void {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export interface SendRecord {
|
|||
id?: number;
|
||||
ts: number;
|
||||
routeId: string;
|
||||
groupId?: string;
|
||||
event: string;
|
||||
repo?: string;
|
||||
target: string;
|
||||
|
|
|
|||
|
|
@ -287,13 +287,15 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
|||
const s = await loadScope(c);
|
||||
if (!s) return c.json({ error: "Unauthorized" }, 401);
|
||||
const limit = Math.min(Math.max(Number(c.req.query("limit") ?? 50), 1), 100);
|
||||
if (s.scope.isSuper) {
|
||||
return c.json({ logs: await getSendLog(c.env.DB, limit) });
|
||||
}
|
||||
const logs = (await getSendLog(c.env.DB, 200))
|
||||
.filter((l) => l.groupId != null && s.scope.groupIds.has(l.groupId))
|
||||
.slice(0, limit);
|
||||
return c.json({ logs });
|
||||
const filterGroupId = c.req.query("groupId") || undefined;
|
||||
const allLogs = await getSendLog(c.env.DB, 200);
|
||||
const allowed = s.scope.isSuper
|
||||
? allLogs
|
||||
: allLogs.filter((l) => l.groupId != null && s.scope.groupIds.has(l.groupId));
|
||||
const logs = filterGroupId
|
||||
? allowed.filter((l) => l.groupId === filterGroupId)
|
||||
: allowed;
|
||||
return c.json({ logs: logs.slice(0, limit) });
|
||||
});
|
||||
|
||||
app.get("/api/logs/:id", async (c) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue