feat(filters): fragment presets and code-block send log detail

This commit is contained in:
RhenCloud 2026-08-18 13:45:23 +08:00
parent c090281cb2
commit 2807c97c5f
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
5 changed files with 105 additions and 8 deletions

View file

@ -739,6 +739,10 @@
@apply font-mono text-[12px] text-muted;
}
.fragments-presets {
@apply mb-2 flex flex-col gap-1.5;
}
.fragments-list {
@apply mb-2 flex flex-col gap-1.5;
}
@ -759,6 +763,10 @@
@apply flex items-center gap-2;
}
.log-block {
@apply max-h-72 overflow-auto whitespace-pre-wrap break-all rounded-sm border border-border bg-surface-2 p-2.5 font-mono text-[12px] leading-relaxed text-text;
}
/* ---- Toasts ---- */
.toasts {
@apply pointer-events-none fixed bottom-7 left-1/2 z-[60] flex -translate-x-1/2 flex-col items-center gap-2;

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from "vue";
import type { Filter, FilterNode, NamedFragment, Route, RouteTarget, RouteTemplate } from "~/types";
import { ROUTE_TEMPLATES } from "~/types";
import { FRAGMENT_PRESETS, ROUTE_TEMPLATES } from "~/types";
import type { NodeForm } from "~/composables/useFilterNode";
import { blankLeafForm, blankNode, nodeToForm, nodeFormToRouteFilters, formToNode } from "~/composables/useFilterNode";
@ -197,8 +197,8 @@ async function runTest(): Promise<void> {
}
}
function insertFragment(frag: NamedFragment): void {
const child = nodeToForm(frag.node);
function insertNode(node: FilterNode): void {
const child = nodeToForm(node);
if (root.value.kind === "all" || root.value.kind === "any") {
root.value.children.push(child);
} else {
@ -211,6 +211,10 @@ function insertFragment(frag: NamedFragment): void {
}
}
function insertFragment(frag: NamedFragment): void {
insertNode(frag.node);
}
async function saveAsFragment(): Promise<void> {
fragmentError.value = "";
const name = fragmentName.value.trim();
@ -407,6 +411,19 @@ watch(
<section v-if="groupId" class="editor-section">
<h3 class="editor-section-title">{{ t("routeEditor.fragments") }}</h3>
<div class="fragments-presets">
<span class="hint">{{ t("routeEditor.fragmentsPresets") }}</span>
<div v-for="preset in FRAGMENT_PRESETS" :key="preset.id" class="fragment-row">
<span class="fragment-name">{{ t(preset.nameKey) }}</span>
<button
type="button"
class="btn btn-ghost fragment-action"
@click="insertNode(preset.node)"
>
{{ t("routeEditor.fragmentsInsert") }}
</button>
</div>
</div>
<div class="fragments-list">
<div v-if="!fragments.length" class="hint">{{ t("routeEditor.fragmentsEmpty") }}</div>
<div v-for="frag in fragments" :key="frag.id" class="fragment-row">

View file

@ -69,7 +69,8 @@
<template v-for="row in detailRows" :key="row.label">
<dt>{{ row.label }}</dt>
<dd>
<code v-if="row.code">{{ row.value }}</code>
<pre v-if="row.block" class="log-block">{{ row.value }}</pre>
<code v-else-if="row.code">{{ row.value }}</code>
<span v-else>{{ row.value }}</span>
</dd>
</template>
@ -107,8 +108,8 @@ const detailError = ref("");
const detailRows = computed(() => {
const l = detail.value;
if (!l) return [] as Array<{ label: string; value: string; code?: boolean }>;
const rows: Array<{ label: string; value: string; code?: boolean }> = [
if (!l) return [] as Array<{ label: string; value: string; code?: boolean; block?: boolean }>;
const rows: Array<{ label: string; value: string; code?: boolean; block?: boolean }> = [
{ label: t("logs.id"), value: String(l.id ?? "-"), code: true },
{ label: t("logs.time"), value: fmtTime(l.ts) },
{ label: t("logs.route"), value: l.routeId, code: true },
@ -125,13 +126,13 @@ const detailRows = computed(() => {
{ label: t("logs.duration"), value: l.durationMs != null ? `${l.durationMs} ms` : "-" },
{ label: t("logs.attempts"), value: l.attempts != null ? String(l.attempts) : "-" },
{ label: t("logs.ok"), value: l.ok ? t("logs.yes") : t("logs.no") },
{ label: t("logs.error"), value: l.error || "-" },
{ label: t("logs.error"), value: l.error || "-", block: true },
];
if (l.detail && Object.keys(l.detail).length > 0) {
rows.push({
label: t("logs.detail"),
value: JSON.stringify(l.detail, null, 2),
code: true,
block: true,
});
}
return rows;

View file

@ -256,6 +256,7 @@ const en: Dict = {
"routeEditor.testNotMatched": "Does not match",
"routeEditor.testInvalidJson": "Invalid JSON",
"routeEditor.fragments": "Fragments",
"routeEditor.fragmentsPresets": "Presets",
"routeEditor.fragmentsEmpty": "No fragments yet",
"routeEditor.fragmentsInsert": "Insert",
"routeEditor.fragmentsDelete": "Delete",
@ -265,6 +266,11 @@ const en: Dict = {
"routeEditor.errPath": "Field filters need a path",
"routeEditor.errValues": "Add at least one value",
"routeEditor.errGroupEmpty": "Group needs at least one condition",
"fragmentPreset.prBot": "PR by a bot",
"fragmentPreset.dependabot": "Dependabot PR",
"fragmentPreset.releasePublished": "Release published",
"fragmentPreset.pushMain": "Push to main",
"fragmentPreset.issueBug": "Issue labeled bug",
"groupEditor.editTitle": "Edit group",
"groupEditor.newTitle": "New group",
"groupEditor.eyebrow": "Group",
@ -627,6 +633,7 @@ const zh: Dict = {
"routeEditor.testNotMatched": "不匹配",
"routeEditor.testInvalidJson": "无效的 JSON",
"routeEditor.fragments": "过滤器片段",
"routeEditor.fragmentsPresets": "预设",
"routeEditor.fragmentsEmpty": "暂无片段",
"routeEditor.fragmentsInsert": "插入",
"routeEditor.fragmentsDelete": "删除",
@ -636,6 +643,11 @@ const zh: Dict = {
"routeEditor.errPath": "字段过滤需要路径",
"routeEditor.errValues": "至少需要一个值",
"routeEditor.errGroupEmpty": "分组至少需要一个条件",
"fragmentPreset.prBot": "机器人提交的 PR",
"fragmentPreset.dependabot": "Dependabot PR",
"fragmentPreset.releasePublished": "发布 Release",
"fragmentPreset.pushMain": "推送到 main 分支",
"fragmentPreset.issueBug": "标记 bug 的 Issue",
"groupEditor.editTitle": "编辑分组",
"groupEditor.newTitle": "新建分组",
"groupEditor.eyebrow": "分组",

View file

@ -208,6 +208,65 @@ export const ROUTE_TEMPLATES: RouteTemplate[] = [
},
];
export interface FragmentPreset {
id: string;
nameKey: string;
node: FilterNode;
}
export const FRAGMENT_PRESETS: FragmentPreset[] = [
{
id: "preset-pr-bot",
nameKey: "fragmentPreset.prBot",
node: {
all: [
{ type: "event", match: "pull_request" },
{ type: "field", path: "pull_request.user.type", match: "Bot" },
],
},
},
{
id: "preset-dependabot",
nameKey: "fragmentPreset.dependabot",
node: {
all: [
{ type: "event", match: "pull_request" },
{ type: "field", path: "pull_request.user.login", match: "dependabot[bot]" },
],
},
},
{
id: "preset-release-published",
nameKey: "fragmentPreset.releasePublished",
node: {
all: [
{ type: "event", match: "release" },
{ type: "action", match: "published" },
],
},
},
{
id: "preset-push-main",
nameKey: "fragmentPreset.pushMain",
node: {
all: [
{ type: "event", match: "push" },
{ type: "branch", match: "main" },
],
},
},
{
id: "preset-issue-bug",
nameKey: "fragmentPreset.issueBug",
node: {
all: [
{ type: "event", match: "issues" },
{ type: "field", path: "issue.labels.name", match: "bug" },
],
},
},
];
export const FILTER_TYPES = ["event", "repo", "actor", "action", "branch", "keyword", "field"] as const;
export const FILTER_OPS: FilterOp[] = [