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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
margin-bottom: 14px;
|
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 {
|
.log-list {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="log-toolbar">
|
<div class="log-toolbar">
|
||||||
<span class="kpi-label">{{ t("logs.lastSends", { n: logs.length }) }}</span>
|
<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">
|
<button class="btn btn-ghost btn-sm" :disabled="loading" @click="refresh">
|
||||||
{{ t("logs.refresh") }}
|
{{ t("logs.refresh") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -68,13 +82,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SendRecord } from "~/types";
|
import type { Group, SendRecord } from "~/types";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { loadById } = useSendLogs();
|
const { loadById } = useSendLogs();
|
||||||
|
|
||||||
const props = defineProps<{ logs: SendRecord[]; loading: boolean }>();
|
const props = defineProps<{
|
||||||
const emit = defineEmits<{ (e: "refresh"): void }>();
|
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 detailOpen = ref(false);
|
||||||
const detail = ref<SendRecord | null>(null);
|
const detail = ref<SendRecord | null>(null);
|
||||||
|
|
@ -121,6 +144,11 @@ function refresh(): void {
|
||||||
emit("refresh");
|
emit("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onGroupFilter(e: Event): void {
|
||||||
|
emit("update:selectedGroupId", (e.target as HTMLSelectElement).value);
|
||||||
|
emit("filter");
|
||||||
|
}
|
||||||
|
|
||||||
async function openDetail(l: SendRecord): Promise<void> {
|
async function openDetail(l: SendRecord): Promise<void> {
|
||||||
if (l.id == null) return;
|
if (l.id == null) return;
|
||||||
detailOpen.value = true;
|
detailOpen.value = true;
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,8 @@ const en: Dict = {
|
||||||
"logs.no": "Failed",
|
"logs.no": "Failed",
|
||||||
"logs.error": "Error",
|
"logs.error": "Error",
|
||||||
"logs.detail": "Extra Info",
|
"logs.detail": "Extra Info",
|
||||||
|
"logs.filterGroup": "Group",
|
||||||
|
"logs.allGroups": "All groups",
|
||||||
"toast.routesSaved": "Routes saved",
|
"toast.routesSaved": "Routes saved",
|
||||||
"toast.routeDeleted": "Route deleted",
|
"toast.routeDeleted": "Route deleted",
|
||||||
"toast.groupSaved": "Groups saved",
|
"toast.groupSaved": "Groups saved",
|
||||||
|
|
@ -311,6 +313,8 @@ const zh: Dict = {
|
||||||
"logs.no": "失败",
|
"logs.no": "失败",
|
||||||
"logs.error": "错误信息",
|
"logs.error": "错误信息",
|
||||||
"logs.detail": "附加信息",
|
"logs.detail": "附加信息",
|
||||||
|
"logs.filterGroup": "分组",
|
||||||
|
"logs.allGroups": "全部分组",
|
||||||
"toast.routesSaved": "路由已保存",
|
"toast.routesSaved": "路由已保存",
|
||||||
"toast.routeDeleted": "路由已删除",
|
"toast.routeDeleted": "路由已删除",
|
||||||
"toast.groupSaved": "分组已保存",
|
"toast.groupSaved": "分组已保存",
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ export function useSendLogs() {
|
||||||
const needLogin = ref(false);
|
const needLogin = ref(false);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
|
|
||||||
async function load(limit = 50): Promise<void> {
|
async function load(limit = 50, groupId?: string): Promise<void> {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = "";
|
error.value = "";
|
||||||
needLogin.value = false;
|
needLogin.value = false;
|
||||||
try {
|
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" },
|
headers: { accept: "application/json" },
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
});
|
});
|
||||||
|
|
@ -44,4 +46,4 @@ export function useSendLogs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return { logs, loading, needLogin, error, load, loadById };
|
return { logs, loading, needLogin, error, load, loadById };
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +156,15 @@
|
||||||
<span>{{ logsLoading ? t("status.loading") : t("status.connected") }}</span>
|
<span>{{ logsLoading ? t("status.loading") : t("status.connected") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -214,19 +222,21 @@ const groupEditorOpen = ref(false);
|
||||||
const editingGroup = ref<Group | null>(null);
|
const editingGroup = ref<Group | null>(null);
|
||||||
const savingGroup = ref(false);
|
const savingGroup = ref(false);
|
||||||
|
|
||||||
|
const logFilterGroup = ref("");
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
forbidden.value = params.get("error") === "forbidden";
|
forbidden.value = params.get("error") === "forbidden";
|
||||||
}
|
}
|
||||||
loadGroups();
|
loadGroups();
|
||||||
loadLogs();
|
loadLogs(50, logFilterGroup.value || undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
function switchView(next: "groups" | "logs"): void {
|
function switchView(next: "groups" | "logs"): void {
|
||||||
view.value = next;
|
view.value = next;
|
||||||
if (next === "groups") loadGroups();
|
if (next === "groups") loadGroups();
|
||||||
if (next === "logs") loadLogs();
|
if (next === "logs") loadLogs(50, logFilterGroup.value || undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function enterGroup(group: Group): void {
|
function enterGroup(group: Group): void {
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ export interface SendRecord {
|
||||||
id?: number;
|
id?: number;
|
||||||
ts: number;
|
ts: number;
|
||||||
routeId: string;
|
routeId: string;
|
||||||
|
groupId?: string;
|
||||||
event: string;
|
event: string;
|
||||||
repo?: string;
|
repo?: string;
|
||||||
target: string;
|
target: string;
|
||||||
|
|
|
||||||
|
|
@ -287,13 +287,15 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
||||||
const s = await loadScope(c);
|
const s = await loadScope(c);
|
||||||
if (!s) return c.json({ error: "Unauthorized" }, 401);
|
if (!s) return c.json({ error: "Unauthorized" }, 401);
|
||||||
const limit = Math.min(Math.max(Number(c.req.query("limit") ?? 50), 1), 100);
|
const limit = Math.min(Math.max(Number(c.req.query("limit") ?? 50), 1), 100);
|
||||||
if (s.scope.isSuper) {
|
const filterGroupId = c.req.query("groupId") || undefined;
|
||||||
return c.json({ logs: await getSendLog(c.env.DB, limit) });
|
const allLogs = await getSendLog(c.env.DB, 200);
|
||||||
}
|
const allowed = s.scope.isSuper
|
||||||
const logs = (await getSendLog(c.env.DB, 200))
|
? allLogs
|
||||||
.filter((l) => l.groupId != null && s.scope.groupIds.has(l.groupId))
|
: allLogs.filter((l) => l.groupId != null && s.scope.groupIds.has(l.groupId));
|
||||||
.slice(0, limit);
|
const logs = filterGroupId
|
||||||
return c.json({ logs });
|
? allowed.filter((l) => l.groupId === filterGroupId)
|
||||||
|
: allowed;
|
||||||
|
return c.json({ logs: logs.slice(0, limit) });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/logs/:id", async (c) => {
|
app.get("/api/logs/:id", async (c) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue