feat: integrate sonner for toast notifications and enhance UI feedback

Added the sonner library for toast notifications across various components, improving user feedback for actions such as saving settings, syncing rules, and handling errors. Updated the layout to include a Toaster component for consistent notification display. Refactored alert messages in the backups, gre, and filters pages to utilize the new notification system, enhancing overall user experience.
This commit is contained in:
Denozordec
2026-05-07 20:49:35 +07:00
parent 84ecd4f061
commit 11ad94f67d
33 changed files with 12350 additions and 252 deletions
@@ -0,0 +1,90 @@
import { and, desc, eq, gte, lte } from "drizzle-orm"
import { db } from "../../../db/index.js"
import { events } from "../../../db/schema.js"
import type { EventItem, EventLevel, EventSourceModule } from "../../../../../packages/contracts/dist/events.js"
export type EventInsertInput = {
id: string
createdAt: string
level: EventLevel
eventType: string
sourceModule: EventSourceModule
title: string
message: string
entityType?: string
entityId?: string
payload?: Record<string, unknown>
}
export type ListEventsParams = {
limit: number
level?: EventLevel
sourceModule?: EventSourceModule
from?: string
to?: string
}
function parsePayload(payloadJson: string | null): Record<string, unknown> {
if (!payloadJson) return {}
try {
const parsed = JSON.parse(payloadJson) as unknown
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>
}
} catch {
// keep backwards compatibility with malformed legacy payloads
}
return {}
}
function mapEventRow(row: typeof events.$inferSelect): EventItem {
return {
id: row.id,
createdAt: row.createdAt,
level: row.level,
eventType: row.eventType,
sourceModule: row.sourceModule as EventSourceModule,
title: row.title,
message: row.message,
entityType: row.entityType ?? null,
entityId: row.entityId ?? null,
payload: parsePayload(row.payloadJson ?? null),
}
}
export function insertEventsBatch(items: EventInsertInput[]) {
if (items.length === 0) return
db.insert(events)
.values(
items.map((item) => ({
id: item.id,
createdAt: item.createdAt,
level: item.level,
eventType: item.eventType,
sourceModule: item.sourceModule,
title: item.title,
message: item.message,
entityType: item.entityType ?? null,
entityId: item.entityId ?? null,
payloadJson: item.payload ? JSON.stringify(item.payload) : null,
})),
)
.run()
}
export function listEvents(params: ListEventsParams): EventItem[] {
const where = and(
params.level ? eq(events.level, params.level) : undefined,
params.sourceModule ? eq(events.sourceModule, params.sourceModule) : undefined,
params.from ? gte(events.createdAt, params.from) : undefined,
params.to ? lte(events.createdAt, params.to) : undefined,
)
const rows = db
.select()
.from(events)
.where(where)
.orderBy(desc(events.createdAt))
.limit(params.limit)
.all()
return rows.map(mapEventRow)
}
@@ -0,0 +1,39 @@
import { randomUUID } from "node:crypto"
import {
appendEventSchema,
listEventsQuerySchema,
type AppendEventBody,
type EventItem,
type ListEventsQuery,
} from "../../../../../packages/contracts/dist/events.js"
import { insertEventsBatch, listEvents, type EventInsertInput } from "../repository/events-repository.js"
function normalizeEventInput(input: AppendEventBody): EventInsertInput {
return {
id: randomUUID(),
createdAt: input.createdAt ?? new Date().toISOString(),
level: input.level,
eventType: input.eventType.trim(),
sourceModule: input.sourceModule,
title: input.title.trim(),
message: input.message.trim(),
entityType: input.entityType?.trim() || undefined,
entityId: input.entityId?.trim() || undefined,
payload: input.payload ?? {},
}
}
export function appendEvent(input: AppendEventBody) {
const parsed = appendEventSchema.parse(input)
insertEventsBatch([normalizeEventInput(parsed)])
}
export function appendEvents(inputs: AppendEventBody[]) {
const rows = inputs.map((entry) => normalizeEventInput(appendEventSchema.parse(entry)))
insertEventsBatch(rows)
}
export function readEvents(query: Partial<ListEventsQuery>): EventItem[] {
const parsed = listEventsQuerySchema.parse(query)
return listEvents(parsed)
}