Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #60, Added syntactic content assistance to the filter field #110

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 68 additions & 21 deletions frontend/components/Filter.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import Button from './Button.svelte';
import QueryModal from './QueryModal.svelte';
import Suggestion from './Suggestion.svelte';
import { parseDSL } from '../dsl';
import { debounce } from '../util';
import {
Expand All @@ -14,28 +15,45 @@
let filter: string = '';
let filterValid: boolean = false;
let saveModal: QueryModal;
let hideSuggestions: boolean = true;
let filterState = {
suggestions: [],
partialToken: null,
};
let suggestionsDiv;
let inputField;

stringFilter.subscribe((value) => {
filter = value;
applyFilter();
});

function _filterChangeHandler() {
// TODO: validate queries as user types them.
if (filter == '') {
filterValid = true;
filterState = {
suggestions: [],
partialToken: null,
};
return;
}
let parseResult = parseDSL(filter, filterState);
filterState = filterState; // force reactivity to update suggestions
if (parseResult.lexErrors.length > 0 || parseResult.parseErrors.length > 0) {
console.log('Found some errors', parseResult.lexErrors, parseResult.parseErrors);
filterValid = false;
// TODO: highlight in red
} else {
filterValid = true;
}
if (hideSuggestions) hideSuggestions = false;
}

function filterChangeHandler(): () => void {
return debounce(() => {
// TODO: validate queries as user types them.
if (filter == '') {
filterValid = true;
return;
}
let parseResult = parseDSL(filter);
if (parseResult.lexErrors.length > 0 || parseResult.parseErrors.length > 0) {
console.log('Found some errors', parseResult.lexErrors, parseResult.parseErrors);
filterValid = false;
// TODO: highlight in red
} else {
filterValid = true;
}
}, 1000);
_filterChangeHandler();
}, 500);
}

function applyFilter() {
Expand All @@ -50,7 +68,7 @@
return;
}

let parseResult = parseDSL(filter);
let parseResult = parseDSL(filter, filterState);
if (parseResult.lexErrors.length > 0) {
console.error(parseResult.lexErrors);
filterActive.set(false);
Expand All @@ -71,15 +89,43 @@
content: filter,
});
}

function handleClickOutsideSuggestionBox(event) {
if (suggestionsDiv && !suggestionsDiv.contains(event.target) && inputField && !inputField.contains(event.target)) {
hideSuggestions = true;
}
}
</script>

<svelte:window on:click={handleClickOutsideSuggestionBox} />

<section class="filter">
<input
class:input-error={filter && !filterValid}
bind:value={filter}
placeholder="Filter destination port"
on:input={filterChangeHandler()}
/>
<div style="position: relative;">
<input
class:input-error={filter && !filterValid}
class="filter-input"
bind:value={filter}
bind:this={inputField}
placeholder="Filter destination port"
on:input={filterChangeHandler()}
on:focus={() => { hideSuggestions = false; }}

/>
<Suggestion
suggestions={filterState.suggestions}
bind:suggestionsDiv
hide={hideSuggestions}
onSelect={(value) => {
console.log('Selected', value);
if (filterState.partialToken) {
filter = filter.slice(0, filterState.partialToken.startOffset-1) + value;
} else {
filter += ' ' + value;
}
inputField.focus();
_filterChangeHandler();
}} />
</div>
<Button disabled={!filterValid} onClick={applyFilter} text="Apply" />
{#if $isAuthenticated}<QueryModal bind:this={saveModal} />
<Button disabled={!filterValid} onClick={openSaveQuery} text="Save" />
Expand All @@ -101,5 +147,6 @@

input.input-error:focus {
outline: 1px solid #ff0000;
color: #ff0000;
}
</style>
39 changes: 39 additions & 0 deletions frontend/components/Suggestion.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
export let suggestionId: string = '';
export let hide: boolean = true;
export let onSelect: (value) => void;
export let suggestions: string[] = [];
export let suggestionsDiv;
</script>

<div id={suggestionId} class="suggestions"
style="display: {(suggestions.length == 0 || hide) ? " none" : "block"};"
bind:this={suggestionsDiv}>
<ul>
{#each suggestions as suggestion}
<p on:click={() => onSelect(suggestion)}>{suggestion}</p>
{/each}
</ul>
</div>
<style>
.suggestions {
position: absolute;
z-index: 1;
background-color: #f1f1f1;
width: 100%;
border: 1px solid #d3d3d3;
}

.suggestions ul {
list-style-type: none;
padding: 0;
margin: 0;
}

.suggestions p {
padding-left: 10px;
text-decoration: none;
display: block;
cursor: pointer;
}
</style>
51 changes: 40 additions & 11 deletions frontend/dsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import type { QueryCstNode } from './generated/chevrotain_dts';
// Comparison
const eq = createToken({ name: 'EQUAL', pattern: /eq/, label: 'eq' });
const ne = createToken({ name: 'NOT_EQUAL', pattern: /ne/, label: 'ne' });
const eqSmb = createToken({ name: 'EQUAL_SMB', pattern: /==/, label: 'eq_smb' });
const neSmb = createToken({ name: 'NOT_EQUAL_SMB', pattern: /!=/, label: 'ne_smb' });
const eqSmb = createToken({ name: 'EQUAL_SMB', pattern: /==/, label: '==' });
const neSmb = createToken({ name: 'NOT_EQUAL_SMB', pattern: /!=/, label: '!=' });

// Boolean logic
const and = createToken({ name: 'AND', pattern: /and/, label: 'and' });
Expand All @@ -24,24 +24,27 @@ const not = createToken({ name: 'NOT', pattern: /not/, label: 'not' });
// Search and match operators
const contains = createToken({ name: 'CONTAINS', pattern: /contains/, label: 'contains' });
const matches = createToken({ name: 'MATCHES', pattern: /matches/, label: 'matches' });
const matchesSmb = createToken({ name: 'MATCHES_SMB', pattern: /~/, label: 'matches_smb' });
const matchesSmb = createToken({ name: 'MATCHES_SMB', pattern: /~/, label: '~' });

// Literals
const port = createToken({ name: 'PORT', pattern: /(?:0|[1-9]\d*)/ });
const port = createToken({ name: 'PORT', pattern: /(?:0|[1-9]\d*)/, label: '<PORT>' });
const ipv4 = createToken({
name: 'IPV4',
pattern:
/([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])/,
label: '<IP>',
});

const ipSrc = createToken({ name: 'IP_SRC', pattern: /ip\.src/ });
const ipDst = createToken({ name: 'IP_DST', pattern: /ip\.dst/ });
const ipSrc = createToken({ name: 'IP_SRC', pattern: /ip\.src/, label: 'ip.src' });
const ipDst = createToken({ name: 'IP_DST', pattern: /ip\.dst/, label: 'ip.dst' });

const tcpPort = createToken({ name: 'TCP_PORT', pattern: /tcp\.port/ });
const udpPort = createToken({ name: 'UDP_PORT', pattern: /udp\.port/ });
const tcpPort = createToken({ name: 'TCP_PORT', pattern: /tcp\.port/, label: 'tcp.port' });
const udpPort = createToken({ name: 'UDP_PORT', pattern: /udp\.port/, label: 'udp.port' });

const payload = createToken({ name: 'PAYLOAD', pattern: /payload/ });
const string = createToken({ name: 'STRING', pattern: /\"[a-zA-Z0-9]+\"/ });
const payload = createToken({ name: 'PAYLOAD', pattern: /payload/, label: 'payload' });
const string = createToken({ name: 'STRING', pattern: /\"[a-zA-Z0-9]+\"/, label: '"msg"' });

const partial = createToken({ name: 'PARTIAL', pattern: /[a-zA-Z0-9]+|\?/, label: 'partial' });

const whiteSpace = createToken({
name: 'WhiteSpace',
Expand Down Expand Up @@ -74,6 +77,8 @@ let allTokens = [

payload,
string,

partial,
];

let queryLexer = new Lexer(allTokens);
Expand Down Expand Up @@ -226,7 +231,7 @@ export const productions: Record<string, Rule> = parser.getGAstProductions();
// create the HTML Text
export const serializedGrammar = parser.getSerializedGastProductions();

export function parseDSL(text: string): ParseResult {
export function parseDSL(text: string, filterState = { suggestions: [], partialToken: null }): ParseResult {
const lexResult = queryLexer.tokenize(text);

if (lexResult.errors.length > 0) {
Expand All @@ -236,6 +241,30 @@ export function parseDSL(text: string): ParseResult {
};
}
// setting a new input will RESET the parser instance's state.

const tokens = lexResult.tokens;
let lastToken;
let partialSuggestion = false;

if (tokens[tokens.length - 1].tokenType === partial) {
lastToken = tokens.pop();
partialSuggestion = true;
}

let suggestions = parser.computeContentAssist("query", tokens).map(nextToken => {
return nextToken.nextTokenType.LABEL;
});
console.log(suggestions);
console.log(lastToken);
if (partialSuggestion && lastToken.image !== '?') {
suggestions = suggestions.filter(suggestion => {
return suggestion.startsWith(lastToken.image);
});
}

filterState.suggestions = suggestions;
filterState.partialToken = partialSuggestion ? lastToken.image : null;

parser.input = lexResult.tokens;
// any top level rule may be used as an entry point
const cst = parser.query();
Expand Down