Skip to content

Commit

Permalink
chore(app): allowing optional regex searching in a dropdown (#3255)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtulk authored Dec 16, 2024
1 parent 34b2145 commit bbe99df
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 15 deletions.
151 changes: 151 additions & 0 deletions weave-js/src/common/components/elements/ModifiedDropdown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {getAsValidRegex, simpleSearch} from './ModifiedDropdown';

describe('testing regex validity', () => {
it('should handle basic character classes', () => {
expect(getAsValidRegex('[a-z]')).toBeInstanceOf(RegExp);
});

it('should handle alternation', () => {
expect(getAsValidRegex('cat|dog')).toBeInstanceOf(RegExp);
});

it('should handle quantifiers', () => {
expect(getAsValidRegex('a{1,3}')).toBeInstanceOf(RegExp);
});

it('should handle escaped special characters', () => {
expect(getAsValidRegex('\\[test\\]')).toBeInstanceOf(RegExp);
});

it('should handle complex patterns', () => {
expect(
getAsValidRegex('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$')
).toBeInstanceOf(RegExp);
});

it('should reject unmatched parentheses', () => {
expect(getAsValidRegex('(abc')).toBeNull();
});

it('should reject unmatched brackets', () => {
expect(getAsValidRegex('[abc')).toBeNull();
});

it('should reject invalid quantifiers', () => {
expect(getAsValidRegex('a{2,1}')).toBeNull();
});

it('should reject unescaped special characters', () => {
expect(getAsValidRegex('[')).toBeNull();
});

it('should reject invalid character ranges', () => {
expect(getAsValidRegex('[z-a]')).toBeNull();
});
});

describe('testing simple search', () => {
const options = [
{
icon: 'wbic-ic-up-arrow',
text: 'Step',
value: '_step',
key: '_step',
},
{
icon: 'calendar',
text: 'Relative Time (Wall)',
value: '_absolute_runtime',
key: '_absolute_runtime',
},
{
icon: 'calendar',
text: 'Relative Time (Process)',
value: '_runtime',
key: '_runtime',
},
{
icon: 'calendar',
text: 'Wall Time',
value: '_timestamp',
key: '_timestamp',
},
{
icon: 'wbic-ic-up-arrow',
text: 'custom_x',
value: 'custom_x',
key: 'custom_x',
},
{
icon: 'wbic-ic-up-arrow',
text: 'loss',
value: 'loss',
key: 'loss',
},
{
icon: 'chart bar',
text: 'eval/run/1/loss_1024',
value: 'eval/run/1/loss_1024',
key: 'eval/run/1/loss_1024',
},
{
icon: 'chart bar',
text: 'eval/run/1/loss_256',
value: 'eval/run/1/loss_256',
key: 'eval/run/1/loss_256',
},
{
icon: 'chart bar',
text: 'eval/run/1/loss_512',
value: 'eval/run/1/loss_512',
key: 'eval/run/1/loss_512',
},
{
icon: 'chart bar',
text: 'eval/run/2/loss_2048',
value: 'eval/run/2/loss_2048',
key: 'eval/run/2/loss_2048',
},
{
icon: 'chart bar',
text: 'eval/run/2/loss_4096',
value: 'eval/run/2/loss_4096',
key: 'eval/run/2/loss_4096',
},
{
icon: 'chart bar',
text: 'eval/run/2/loss_768',
value: 'eval/run/2/loss_768',
key: 'eval/run/2/loss_768',
},
];

it('simpleSearch matches exact non-regex strings', () => {
const results = simpleSearch(options, 'loss');
expect(results.every(r => (r.value as string).includes('loss'))).toBe(true);
});

it('simpleSearch matches partial non-regex strings', () => {
const results = simpleSearch(options, 'loss_');
expect(results.every(r => (r.value as string).includes('loss_'))).toBe(
true
);
});

it('simpleSearch matches case-insensitive non-regex strings', () => {
const results = simpleSearch(options, 'LOSS');
expect(results.every(r => (r.value as string).includes('loss'))).toBe(true);
});

it('simpleSearch can disallow matching regex patterns', () => {
const results = simpleSearch(options, '.*s.*s.*');
expect(results.length).toBe(0);
});

it('simpleSearch can support matching regex patterns', () => {
const results = simpleSearch(options, '.*s.*s.*', {
allowRegexSearch: true,
});
expect(results.length).toBeGreaterThan(0);
});
});
46 changes: 31 additions & 15 deletions weave-js/src/common/components/elements/ModifiedDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,28 @@ type LabelCoord = {

const ITEM_LIMIT_VALUE = '__item_limit';

const simpleSearch = (options: DropdownItemProps[], query: string) => {
export function getAsValidRegex(s: string): RegExp | null {
try {
return new RegExp(s);
} catch (e) {
return null;
}
}

export const simpleSearch = (
options: DropdownItemProps[],
query: string,
config: {
allowRegexSearch?: boolean;
} = {}
) => {
const regex = config.allowRegexSearch ? getAsValidRegex(query) : null;

return _.chain(options)
.filter(o =>
_.includes(JSON.stringify(o.text).toLowerCase(), query.toLowerCase())
)
.filter(o => {
const text = JSON.stringify(o.text).toLowerCase();
return regex ? regex.test(text) : _.includes(text, query.toLowerCase());
})
.sortBy(o => {
const valJSON = typeof o.text === 'string' ? `"${query}"` : query;
return JSON.stringify(o.text).toLowerCase() === valJSON.toLowerCase()
Expand All @@ -69,17 +86,17 @@ const getOptionProps = (opt: Option, hideText: boolean) => {
};

export interface ModifiedDropdownExtraProps {
allowRegexSearch?: boolean;
debounceTime?: number;
enableReordering?: boolean;
hideText?: boolean;
itemLimit?: number;
options: Option[];
optionTransform?(option: Option): Option;
resultLimit?: number;
resultLimitMessage?: string;
style?: CSSProperties;
hideText?: boolean;
useIcon?: boolean;

optionTransform?(option: Option): Option;
}

type ModifiedDropdownProps = Omit<StrictDropdownProps, 'options'> &
Expand All @@ -98,10 +115,11 @@ const ModifiedDropdown: FC<ModifiedDropdownProps> = React.memo(
} = props;

const {
allowAdditions,
allowRegexSearch,
enableReordering,
itemLimit,
optionTransform,
enableReordering,
allowAdditions,
resultLimit = 100,
resultLimitMessage = `Limited to ${resultLimit} items. Refine search to see other options.`,
...passProps
Expand Down Expand Up @@ -130,15 +148,13 @@ const ModifiedDropdown: FC<ModifiedDropdownProps> = React.memo(
_.concat(currentOptions, search(propsOptions, query) as Option[])
);
} else {
setOptions(
_.concat(
currentOptions,
simpleSearch(propsOptions, query) as Option[]
)
const updatedOptions = currentOptions.concat(
simpleSearch(propsOptions, query, {allowRegexSearch}) as Option[]
);
setOptions(updatedOptions);
}
}, debounceTime || 400),
[multiple, propsOptions, search, value, debounceTime]
[allowRegexSearch, debounceTime, multiple, propsOptions, search, value]
);

const firstRenderRef = useRef(true);
Expand Down

0 comments on commit bbe99df

Please sign in to comment.