Skip to content

Commit

Permalink
WIP - Custom filters
Browse files Browse the repository at this point in the history
  • Loading branch information
niemyjski committed Oct 19, 2023
1 parent 0727569 commit b01295b
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 26 deletions.
1 change: 1 addition & 0 deletions src/Exceptionless.Web/ClientApp/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"cSpell.words": [
"iconify",
"keyof",
"lucene",
"nameof",
"navigatetofirstpage",
"oidc",
Expand Down
142 changes: 117 additions & 25 deletions src/Exceptionless.Web/ClientApp/src/lib/components/filters/filters.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,134 @@
export interface IFilter {}
import type { Serializer } from 'svelte-local-storage-store';

export interface KeywordFilter extends IFilter {
keyword: string;
export interface IFilter {
toFilter(): string;
}

export interface TermFilter extends IFilter {
term: string;
value: string | number | boolean | Date | null | undefined;
export class BooleanFilter implements IFilter {
constructor(
public term: string,
public value?: boolean
) {}

public toFilter(): string {
if (this.value === undefined) {
return `_missing_:${this.term}`;
}

return `${this.term}:${this.value}`;
}
}

export function quote(value?: string | null): string | undefined {
return value ? `"${value}"` : undefined;
export class DateFilter implements IFilter {
constructor(
public term: string,
public value?: Date
) {}

public toFilter(): string {
if (this.value === undefined) {
return `_missing_:${this.term}`;
}

return `${this.term}:${this.value}`;
}
}

function isKeywordFilter(filter: IFilter): filter is KeywordFilter {
return 'keyword' in filter;
export class KeywordFilter implements IFilter {
constructor(public keyword: string) {}

public toFilter(): string {
return this.keyword;
}
}

function isTermFilter(filter: IFilter): filter is TermFilter {
return 'term' in filter;
export class NumberFilter implements IFilter {
constructor(
public term: string,
public value?: number
) {}

public toFilter(): string {
if (this.value === undefined) {
return `_missing_:${this.term}`;
}

return `${this.term}:${this.value}`;
}
}

export function toFilter(filters: IFilter[]): string {
return filters.map(toFilterPart).filter(Boolean).join(' ');
export class ReferenceFilter implements IFilter {
constructor(public referenceId: string) {}

public toFilter(): string {
return `reference:${this.referenceId}`;
}
}

function toFilterPart(filter: IFilter): string | undefined {
if (isKeywordFilter(filter)) {
return filter.keyword;
} else if (isTermFilter(filter)) {
return `${filter.term}:${filter.value}`;
export class SessionFilter implements IFilter {
constructor(public sessionId: string) {}

public toFilter(): string {
return `(reference:${this.sessionId} OR ref.session:${this.sessionId})`;
}
}

export class StringFilter implements IFilter {
constructor(
public term: string,
public value?: number
) {}

public toFilter(): string {
if (this.value === undefined) {
return `_missing_:${this.term}`;
}

return `${this.term}:${this.value}`;
}
}

export class VersionFilter implements IFilter {
constructor(
public term: string,
public value?: string
) {}

public toFilter(): string {
if (this.value === undefined) {
return `_missing_:${this.term}`;
}

return `${this.term}:${this.value}`;
}
}

export function quoteIfSpecialCharacters(value?: string | null): string | null | undefined {
// Check for lucene special characters or whitespace
const regex = new RegExp(
'\\+|\\-|\\&|\\||\\!|\\(|\\)|\\{|\\}|\\[|\\]|\\^|\\"|\\~|\\*|\\?|\\:|\\\\|\\/|\\s',
'g'
);

if (value && value.match(regex)) {
return quote(value);
}

return value;
}

export function quote(value?: string | null): string | undefined {
return value ? `"${value}"` : undefined;
}

/**
* Update the filters with the given filter. If the filter already exists, it will be removed.
* @param filters The filters
* @param filter The filter to add or remove
* @returns The updated filters
*/
export function updateFilters(filters: IFilter[], filter: IFilter): IFilter[] {
const index = filters.findIndex((f) => toFilterPart(f) === toFilterPart(filter));
const index = filters.findIndex((f) => f.toFilter() === filter.toFilter());
if (index !== -1) {
filters.splice(index, 1);
} else {
Expand All @@ -67,13 +155,13 @@ export function parseFilter(filters: IFilter[], input: string): IFilter[] {
}

// NOTE: This is a super naive implementation...
const part = toFilterPart(filter);
const part = filter.toFilter();
if (part) {
// Check for whole word / phrase match
const regex = new RegExp(`(^|\\s)${part}(\\s|$)`);
if (regex.test(input)) {
input = input.replace(regex, '');
if (isKeywordFilter(filter)) {
if (filter instanceof KeywordFilter) {
keywordFilterParts.push(part);
} else {
resolvedFilters.push(filter);
Expand All @@ -84,10 +172,14 @@ export function parseFilter(filters: IFilter[], input: string): IFilter[] {

input = `${keywordFilterParts.join(' ')} ${input ?? ''}`.trim();
if (input) {
resolvedFilters.push({
keyword: input
});
resolvedFilters.push(new KeywordFilter(input));
}

return resolvedFilters;
}


export class FilterSerializer implements Serializer<IFilter[]> {
parse(text: string): IFilter[];
stringify(object: IFilter[]): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { persisted } from 'svelte-local-storage-store';
import EventsDrawer from '$comp/events/EventsDrawer.svelte';
import CustomEventMessage from '$comp/messaging/CustomEventMessage.svelte';
import { toFilter, type IFilter, updateFilters, parseFilter } from '$comp/filters/filters';
import { type IFilter, updateFilters, parseFilter } from '$comp/filters/filters';
import { derived } from 'svelte/store';
let liveMode = persisted<boolean>('live', true);
Expand Down

0 comments on commit b01295b

Please sign in to comment.