feat(ui): add attribute-filter URL and facet parsing helpers
This commit is contained in:
69
ui/src/utils/attribute-filter.test.ts
Normal file
69
ui/src/utils/attribute-filter.test.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parseAttrParam, formatAttrParam, parseFacetQuery } from './attribute-filter';
|
||||||
|
|
||||||
|
describe('parseAttrParam', () => {
|
||||||
|
it('returns key-only for input without colon', () => {
|
||||||
|
expect(parseAttrParam('order')).toEqual({ key: 'order' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('splits on first colon, trims key, preserves value as-is', () => {
|
||||||
|
expect(parseAttrParam('order:47')).toEqual({ key: 'order', value: '47' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats a value containing colons as a single value', () => {
|
||||||
|
expect(parseAttrParam('trace-id:abc:123')).toEqual({ key: 'trace-id', value: 'abc:123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for blank input', () => {
|
||||||
|
expect(parseAttrParam('')).toBeNull();
|
||||||
|
expect(parseAttrParam(' ')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for missing key', () => {
|
||||||
|
expect(parseAttrParam(':x')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when the key contains invalid characters', () => {
|
||||||
|
expect(parseAttrParam('bad key:1')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAttrParam', () => {
|
||||||
|
it('returns bare key for key-only filter', () => {
|
||||||
|
expect(formatAttrParam({ key: 'order' })).toBe('order');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('joins with colon when value is present', () => {
|
||||||
|
expect(formatAttrParam({ key: 'order', value: '47' })).toBe('order:47');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('joins with colon when value is empty string', () => {
|
||||||
|
expect(formatAttrParam({ key: 'order', value: '' })).toBe('order:');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseFacetQuery', () => {
|
||||||
|
it('matches `key: value`', () => {
|
||||||
|
expect(parseFacetQuery('order: 47')).toEqual({ key: 'order', value: '47' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches `key:value` without spaces', () => {
|
||||||
|
expect(parseFacetQuery('order:47')).toEqual({ key: 'order', value: '47' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches wildcard values', () => {
|
||||||
|
expect(parseFacetQuery('order: 4*')).toEqual({ key: 'order', value: '4*' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when the key contains invalid characters', () => {
|
||||||
|
expect(parseFacetQuery('bad key: 1')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null without a colon', () => {
|
||||||
|
expect(parseFacetQuery('order')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null with an empty value side', () => {
|
||||||
|
expect(parseFacetQuery('order: ')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
37
ui/src/utils/attribute-filter.ts
Normal file
37
ui/src/utils/attribute-filter.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export interface AttributeFilter {
|
||||||
|
key: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEY_REGEX = /^[a-zA-Z0-9._-]+$/;
|
||||||
|
|
||||||
|
/** Parses a single `?attr=` URL value. Returns null for invalid / blank input. */
|
||||||
|
export function parseAttrParam(raw: string): AttributeFilter | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (trimmed.length === 0) return null;
|
||||||
|
|
||||||
|
const colon = trimmed.indexOf(':');
|
||||||
|
if (colon < 0) {
|
||||||
|
return KEY_REGEX.test(trimmed) ? { key: trimmed } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = trimmed.substring(0, colon).trim();
|
||||||
|
const value = raw.substring(raw.indexOf(':') + 1);
|
||||||
|
if (!KEY_REGEX.test(key)) return null;
|
||||||
|
return { key, value };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serialises an AttributeFilter back to a URL `?attr=` value. */
|
||||||
|
export function formatAttrParam(f: AttributeFilter): string {
|
||||||
|
return f.value === undefined ? f.key : `${f.key}:${f.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FACET_REGEX = /^\s*([a-zA-Z0-9._-]+)\s*:\s*(\S(?:.*\S)?)\s*$/;
|
||||||
|
|
||||||
|
/** Parses a cmd-k query like `order: 47` into a facet descriptor. */
|
||||||
|
export function parseFacetQuery(query: string): AttributeFilter | null {
|
||||||
|
const m = FACET_REGEX.exec(query);
|
||||||
|
if (!m) return null;
|
||||||
|
return { key: m[1], value: m[2] };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user