feat(api, web): enhance list entry management and UI consistency
- 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:
@@ -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'])
|
||||
})
|
||||
})
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user