feat(api, web): enhance list entry management and UI consistency
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m57s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Updated API to support structured list entry inputs, allowing for nested list references.
- Improved error handling for list operations to prevent cyclic references.
- Refactored UI components to ensure consistent labeling and navigation for IP lists.
- Enhanced list detail and catalog pages with better filtering and entry management features.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-20 23:25:05 +07:00
co-authored by Cursor
parent 3f7672ab7c
commit c505ab82e8
13 changed files with 952 additions and 518 deletions
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest'
import {
listEntriesBodySchema,
listEntryKindSchema,
listNestedChildIds,
parseListEntry,
} from '../src/list-entries.js'
describe('list entry kinds', () => {
it('includes list kind', () => {
expect(listEntryKindSchema.parse('list')).toBe('list')
})
it('parses ip/cidr/hostname but not list ids from plaintext', () => {
expect(parseListEntry('8.8.8.8').kind).toBe('ip')
expect(parseListEntry('10.0.0.0/8').kind).toBe('cidr')
expect(parseListEntry('bad.example.com').kind).toBe('hostname')
expect(() => parseListEntry('not-a-valid-token')).toThrow()
})
it('accepts values or items in body', () => {
expect(
listEntriesBodySchema.parse({ values: ['8.8.8.8'] }).values,
).toEqual(['8.8.8.8'])
expect(
listEntriesBodySchema.parse({
items: [{ kind: 'list', value: 'abc' }],
}).items,
).toEqual([{ kind: 'list', value: 'abc' }])
expect(() => listEntriesBodySchema.parse({})).toThrow()
})
it('reads nested child ids from config', () => {
const ids = listNestedChildIds(
JSON.stringify({
items: [
{ kind: 'ip', value: '1.1.1.1' },
{ kind: 'list', value: 'child-1' },
{ kind: 'list', value: 'child-2' },
],
}),
)
expect(ids).toEqual(['child-1', 'child-2'])
})
})
+33 -4
View File
@@ -21,7 +21,7 @@ export function isManualListType(type: string): boolean {
return type === 'static' || type === 'domains'
}
export const listEntryKindSchema = z.enum(['ip', 'cidr', 'hostname'])
export const listEntryKindSchema = z.enum(['ip', 'cidr', 'hostname', 'list'])
export type ListEntryKind = z.infer<typeof listEntryKindSchema>
export const listConfigItemSchema = z.object({
@@ -35,6 +35,7 @@ export const listEntryKindLabels: Record<ListEntryKind, string> = {
ip: 'IP',
cidr: 'CIDR',
hostname: 'Домен',
list: 'Список',
}
const IPV4 =
@@ -96,7 +97,8 @@ export function normalizeItemToCidrs(
if (item.kind === 'cidr') {
return [item.value]
}
return resolvedHostCidrs ?? []
// hostname | list — CIDRs come from resolution / nested list materialization
return resolvedHostCidrs ?? item.resolved_cidrs ?? []
}
export function readManualItems(
@@ -135,15 +137,42 @@ export function readManualItems(
return []
}
/** Nested list refs in config.items (kind=list, value=child list id). */
export function listNestedChildIds(configJson: string): string[] {
return readManualItems(configJson)
.filter((i) => i.kind === 'list')
.map((i) => i.value)
}
export function guessListSourceFromInput(extra: string): 'static' | 'json_url' {
const t = extra.trim()
if (/^https?:\/\//i.test(t)) return 'json_url'
return 'static'
}
export const listEntriesBodySchema = z.object({
values: z.array(z.string().min(1)).min(1),
export const listEntryInputSchema = z.object({
kind: listEntryKindSchema,
value: z.string().min(1),
})
export type ListEntryInput = z.infer<typeof listEntryInputSchema>
export const listEntriesBodySchema = z
.object({
/** Legacy plaintext tokens (auto-classified; not for kind=list). */
values: z.array(z.string().min(1)).optional(),
/** Structured entries including nested list refs. */
items: z.array(listEntryInputSchema).optional(),
})
.superRefine((body, ctx) => {
const hasValues = Boolean(body.values?.length)
const hasItems = Boolean(body.items?.length)
if (!hasValues && !hasItems) {
ctx.addIssue({
code: 'custom',
message: 'Нужны values или items',
})
}
})
export const deleteListEntryBodySchema = z.object({
value: z.string().min(1),