Skip to content

Commit

Permalink
WIP: Clickable filters with ability to enter custom keyword filters.
Browse files Browse the repository at this point in the history
  • Loading branch information
niemyjski committed Oct 14, 2023
1 parent 60d6b73 commit 6822209
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
globalFetchClient as api,
globalLoading as loading
} from '$api/FetchClient';
import WebSocketMessage from '$comp/WebSocketMessage.svelte';
import WebSocketMessage from '$comp/messaging/WebSocketMessage.svelte';
import EventsUserIdentitySummaryColumn from './EventsUserIdentitySummaryColumn.svelte';
import ErrorMessage from '$comp/ErrorMessage.svelte';
import Loading from '$comp/Loading.svelte';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
} from '$lib/helpers/persistent-event';
import SimpleStackTrace from '../SimpleStackTrace.svelte';
import StackTrace from '../StackTrace.svelte';
import SearchItem from '$comp/filters/FilterableItem.svelte';
//import { buildUrl } from '$lib/helpers/url';
export let event: PersistentEvent;
Expand Down Expand Up @@ -102,7 +103,7 @@
{#if event.type !== 'error'}
<tr>
<th class="whitespace-nowrap">Event Type</th>
<td>{event.type}</td>
<td><SearchItem term="type" value={event.type}>{event.type}</SearchItem></td>
</tr>
{/if}
{#if hasError}
Expand Down Expand Up @@ -146,7 +147,9 @@
<th class="whitespace-nowrap">Tags</th>
<td class="flex flex-wrap justify-start gap-2 overflow-auto">
{#each event.tags as tag}
<div class="badge badge-neutral">{tag}</div>
<SearchItem term="tag" value={tag}
><div class="badge badge-neutral">{tag}</div></SearchItem
>
{/each}
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import type { TermFilter } from './filters';
export let term: string;
export let value: string | number | boolean | null | undefined;
const title = `Search ${term}:${value}`;
function onSearchClick() {
document.dispatchEvent(
new CustomEvent('filter', {
detail: <TermFilter>{
term,
value
},
bubbles: true
})
);
}
</script>

<button on:click|preventDefault={onSearchClick} {title}>
<slot />
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
export interface IFilter {}

export interface KeywordFilter extends IFilter {
keyword: string;
}

export interface TermFilter extends IFilter {
term: string;
value: string | number | boolean | Date | null | undefined;
}

function isKeywordFilter(filter: IFilter): filter is KeywordFilter {
return 'keyword' in filter;
}

function isTermFilter(filter: IFilter): filter is TermFilter {
return 'term' in filter;
}

export function toFilter(filters: IFilter[]): string {
return filters.map(toFilterPart).filter(Boolean).join(' ');
}

function toFilterPart(filter: IFilter): string | undefined {
if (isKeywordFilter(filter)) {
return filter.keyword;
} else if (isTermFilter(filter)) {
return `${filter.term}:${filter.value}`;
}
}

/**
* 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));
if (index !== -1) {
filters.splice(index, 1);
} else {
filters.push(filter);
}

return filters;
}

/**
* Given the existing filters try and parse out any existing filters while adding new user filters as a keyword filter.
* @param filters The current filters
* @param filter The current filter string that was modified by the user
* @returns The updated filter
*/
export function updateFiltersWithUserModifications(filters: IFilter[], input: string): IFilter[] {
const resolvedFilters: IFilter[] = [];

const keywordFilterParts = [];
for (const filter of filters) {
input = input?.trim();
if (!input) {
break;
}

// NOTE: This is a super naive implementation...
const part = toFilterPart(filter);
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)) {
keywordFilterParts.push(part);
} else {
resolvedFilters.push(filter);
}
}
}
}

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

return resolvedFilters;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script lang="ts">
import type { WebSocketMessageType, WebSocketMessageValue } from '$lib/models/websocket';
import { createEventDispatcher, onMount } from 'svelte';
export let type: WebSocketMessageType;
export let type: string;
function onMessage({ detail }: CustomEvent<WebSocketMessageValue<WebSocketMessageType>>) {
function onMessage({ detail }: CustomEvent<unknown>) {
dispatch('message', detail);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import type { WebSocketMessageType } from '$lib/models/websocket';
import CustomEventMessage from './CustomEventMessage.svelte';
export let type: WebSocketMessageType;
</script>

<CustomEventMessage {type} on:message}></CustomEventMessage>
32 changes: 29 additions & 3 deletions src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
import { drawerComponent, drawerComponentProps, showDrawer } from '$lib/stores/drawer';
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,
updateFiltersWithUserModifications
} from '$comp/filters/filters';
import { derived } from 'svelte/store';
let liveMode = persisted<boolean>('live', true);
Expand All @@ -15,14 +23,30 @@
drawerComponentProps.set({ id: detail.id });
}
let filter = persisted<string>('filter', '');
let time = persisted<string>('time', '');
const filters = persisted<IFilter[]>('filters', []);
let filter = derived(filters, ($filters) => toFilter($filters));
function onFilterChanged({ detail }: CustomEvent<IFilter>): void {
filters.set(updateFilters($filters, detail));
}
let updateFiltersDebounceTimer: ReturnType<typeof setTimeout>;
function updateFiltersWithCustomInput(event: Event) {
clearTimeout(updateFiltersDebounceTimer);
updateFiltersDebounceTimer = setTimeout(() => {
const { value } = event.target as HTMLInputElement;
filters.set(updateFiltersWithUserModifications($filters, value));
}, 500);
}
</script>

<svelte:head>
<title>Exceptionless</title>
</svelte:head>

<CustomEventMessage type="filter" on:message={onFilterChanged}></CustomEventMessage>

<h1 class="text-xl">Events</h1>

{#if $liveMode}
Expand All @@ -33,7 +57,8 @@
type="search"
placeholder="Search..."
class="input input-sm w-full max-w-xs"
bind:value={$filter}
value={$filter}
on:input={updateFiltersWithCustomInput}
/>
<div class="flex items-center space-x-2">
<div class="flex items-center">
Expand All @@ -59,7 +84,8 @@
type="search"
placeholder="Search..."
class="input input-sm w-full max-w-xs"
bind:value={$filter}
value={$filter}
on:input={updateFiltersWithCustomInput}
/>
<select class="select select-sm" bind:value={$time}>
<option value="last hour">Last Hour</option>
Expand Down

0 comments on commit 6822209

Please sign in to comment.