Skip to content

Commit fbf716e

Browse files
committed
WIP - Debounce grid updates based on filtering
1 parent 5cf8ad8 commit fbf716e

File tree

7 files changed

+168
-11
lines changed

7 files changed

+168
-11
lines changed

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import type { Snippet } from 'svelte';
33
44
import * as DataTable from '$comp/data-table';
5+
import { getKeywordFilter, getOrganizationFilter, getProjectFilter, getStackFilter, type IFilter } from "$comp/filters/filters.svelte";
6+
import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models';
57
import { DEFAULT_LIMIT } from '$shared/api';
68
import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient';
79
import { createTable } from '@tanstack/svelte-table';
810
import { useEventListener } from 'runed';
11+
import { debounce } from 'throttle-debounce';
912
1013
import type { GetEventsMode } from '../../api.svelte';
1114
import type { EventSummaryModel, SummaryTemplateKeys } from '../summary/index';
@@ -14,6 +17,7 @@
1417
1518
interface Props {
1619
filter: string;
20+
filters: IFilter[];
1721
limit: number;
1822
mode?: GetEventsMode;
1923
pageFilter?: string;
@@ -22,7 +26,7 @@
2226
toolbarChildren?: Snippet;
2327
}
2428
25-
let { filter, limit = $bindable(DEFAULT_LIMIT), mode = 'summary', pageFilter = undefined, rowclick, time, toolbarChildren }: Props = $props();
29+
let { filter, filters, limit = $bindable(DEFAULT_LIMIT), mode = 'summary', pageFilter = undefined, rowclick, time, toolbarChildren }: Props = $props();
2630
const context = getTableContext<EventSummaryModel<SummaryTemplateKeys>>({ limit, mode });
2731
const table = createTable(context.options);
2832
@@ -53,8 +57,82 @@
5357
table.resetRowSelection();
5458
}
5559
}
60+
const debouncedLoadData = debounce(10000, loadData);
61+
62+
async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) {
63+
const shouldRefresh = () => {
64+
if (!filter) {
65+
return true;
66+
}
67+
68+
const { id, organization_id, project_id, stack_id } = message;
69+
if (id) {
70+
// Check to see if any records on the page match
71+
if (mode === "summary" && table.options.data.some((doc) => doc.id === id)) {
72+
return true;
73+
}
74+
75+
// This could match any kind of lucene query (even must not filtering)
76+
const keywordFilter = getKeywordFilter(filters);
77+
if (keywordFilter && !keywordFilter.isEmpty()) {
78+
if (keywordFilter.value!.includes(id)) {
79+
return true;
80+
}
81+
}
82+
}
83+
84+
if (stack_id) {
85+
// Check to see if any records on the page match
86+
if (mode !== "summary" && table.options.data.some((doc) => doc.id === stack_id)) {
87+
return true;
88+
}
89+
90+
const stackFilter = getStackFilter(filters);
91+
if (stackFilter && !stackFilter.isEmpty()) {
92+
return stackFilter.value === stack_id;
93+
}
94+
}
95+
96+
if (project_id) {
97+
const projectFilter = getProjectFilter(filters);
98+
if (projectFilter && !projectFilter.isEmpty()) {
99+
return projectFilter.value.includes(project_id);
100+
}
101+
}
102+
103+
if (organization_id) {
104+
const organizationFilter = getOrganizationFilter(filters);
105+
if (organizationFilter && !organizationFilter.isEmpty()) {
106+
return organizationFilter.value === organization_id;
107+
}
108+
}
109+
110+
return true;
111+
};
112+
113+
switch (message.change_type) {
114+
case ChangeType.Added:
115+
case ChangeType.Saved:
116+
if (shouldRefresh()) {
117+
await debouncedLoadData();
118+
}
119+
120+
break;
121+
case ChangeType.Removed:
122+
if (shouldRefresh()) {
123+
if (message.id && mode === "summary") {
124+
table.options.data = table.options.data.filter((doc) => doc.id !== message.id);
125+
}
126+
127+
await debouncedLoadData();
128+
}
129+
130+
break;
131+
}
132+
}
56133
57134
useEventListener(document, 'refresh', async () => await loadData());
135+
useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail));
58136
</script>
59137

60138
<DataTable.Root>

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,28 @@
33
44
import * as DataTable from '$comp/data-table';
55
import ErrorMessage from '$comp/ErrorMessage.svelte';
6+
import { getKeywordFilter, getOrganizationFilter, getProjectFilter, getStackFilter, type IFilter } from "$comp/filters/filters.svelte";
67
import { Muted } from '$comp/typography';
78
import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models';
89
import { DEFAULT_LIMIT } from '$shared/api';
910
import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient';
1011
import { createTable } from '@tanstack/svelte-table';
1112
import { useEventListener } from 'runed';
13+
import { debounce } from "throttle-debounce";
1214
1315
import type { EventSummaryModel, SummaryTemplateKeys } from '../summary/index';
1416
1517
import { getTableContext } from './options.svelte';
1618
1719
interface Props {
1820
filter: string;
21+
filters: IFilter[];
1922
limit: number;
2023
rowclick?: (row: EventSummaryModel<SummaryTemplateKeys>) => void;
2124
toolbarChildren?: Snippet;
2225
}
2326
24-
let { filter, limit = $bindable(DEFAULT_LIMIT), rowclick, toolbarChildren }: Props = $props();
27+
let { filter, filters, limit = $bindable(DEFAULT_LIMIT), rowclick, toolbarChildren }: Props = $props();
2528
const context = getTableContext<EventSummaryModel<SummaryTemplateKeys>>({ limit, mode: 'summary' }, (options) => ({
2629
...options,
2730
columns: options.columns.filter((c) => c.id !== 'select').map((c) => ({ ...c, enableSorting: false })),
@@ -58,7 +61,9 @@
5861
});
5962
6063
if (response.ok) {
61-
before = response.meta.links.previous?.before;
64+
if (response.meta.links.previous?.before) {
65+
before = response.meta.links.previous?.before;
66+
}
6267
6368
const data = filterChanged ? [] : [...context.data];
6469
for (const summary of response.data?.reverse() || []) {
@@ -70,13 +75,71 @@
7075
}
7176
}
7277
78+
const debouncedLoadData = debounce(5000, loadData);
79+
7380
async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) {
81+
const shouldRefresh = () => {
82+
if (!filter) {
83+
return true;
84+
}
85+
86+
const { id, organization_id, project_id, stack_id } = message;
87+
if (id) {
88+
// Check to see if any records on the page match
89+
if (table.options.data.some((doc) => doc.id === id)) {
90+
return true;
91+
}
92+
93+
// This could match any kind of lucene query (even must not filtering)
94+
const keywordFilter = getKeywordFilter(filters);
95+
if (keywordFilter && !keywordFilter.isEmpty()) {
96+
if (keywordFilter.value!.includes(id)) {
97+
return true;
98+
}
99+
}
100+
}
101+
102+
if (stack_id) {
103+
const stackFilter = getStackFilter(filters);
104+
if (stackFilter && !stackFilter.isEmpty()) {
105+
return stackFilter.value === stack_id;
106+
}
107+
}
108+
109+
if (project_id) {
110+
const projectFilter = getProjectFilter(filters);
111+
if (projectFilter && !projectFilter.isEmpty()) {
112+
return projectFilter.value.includes(project_id);
113+
}
114+
}
115+
116+
if (organization_id) {
117+
const organizationFilter = getOrganizationFilter(filters);
118+
if (organizationFilter && !organizationFilter.isEmpty()) {
119+
return organizationFilter.value === organization_id;
120+
}
121+
}
122+
123+
return true;
124+
};
125+
74126
switch (message.change_type) {
75127
case ChangeType.Added:
76128
case ChangeType.Saved:
77-
return await loadData();
129+
if (shouldRefresh()) {
130+
await debouncedLoadData();
131+
}
132+
133+
break;
78134
case ChangeType.Removed:
79-
table.options.data = table.options.data.filter((doc) => doc.id !== message.id);
135+
if (shouldRefresh()) {
136+
if (message.id) {
137+
table.options.data = table.options.data.filter((doc) => doc.id !== message.id);
138+
}
139+
140+
await debouncedLoadData();
141+
}
142+
80143
break;
81144
}
82145
}

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,22 @@ export function getFilter(filter: Omit<IFilter, 'isEmpty' | 'reset' | 'toFilter'
590590
}
591591
}
592592

593+
export function getKeywordFilter(filters: IFilter[]): KeywordFilter | undefined {
594+
return filters.find((f) => f.type === 'keyword') as KeywordFilter;
595+
}
596+
597+
export function getOrganizationFilter(filters: IFilter[]): OrganizationFilter | undefined {
598+
return filters.find((f) => f.type === 'organization') as OrganizationFilter;
599+
}
600+
601+
export function getProjectFilter(filters: IFilter[]): ProjectFilter {
602+
return filters.find((f) => f.type === 'project') as ProjectFilter;
603+
}
604+
605+
export function getStackFilter(filters: IFilter[]): StringFilter | undefined {
606+
return filters.find((f) => f.type === 'string') as StringFilter;
607+
}
608+
593609
export function processFilterRules(filters: IFilter[], changed?: IFilter): IFilter[] {
594610
// Allow only one filter per type and term.
595611
const groupedFilters: Partial<Record<string, IFilter[]>> = Object.groupBy(filters, (f: IFilter) => f.key);

src/Exceptionless.Web/ClientApp/src/lib/features/websockets/models.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ export interface EntityChanged {
88
change_type: ChangeType;
99
data: Record<string, unknown>;
1010

11-
id: string;
11+
id?: string;
1212
organization_id?: string;
13-
projectId?: string;
14-
stackId?: string;
13+
project_id?: string;
14+
stack_id?: string;
1515

1616
type: string;
1717
}

src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<Card.Root>
4545
<Card.Title class="p-6 pb-0 text-2xl" level={2}>Events</Card.Title>
4646
<Card.Content>
47-
<EventsDataTable bind:limit={limit.value} {filter} {rowclick} {time}>
47+
<EventsDataTable bind:limit={limit.value} filters={persistedFilters.value} {filter} {rowclick} {time}>
4848
{#snippet toolbarChildren()}
4949
<FacetedFilter.Root changed={onFilterChanged} {facets} remove={onFilterRemoved}></FacetedFilter.Root>
5050
{/snippet}

src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
<Card.Root>
6060
<Card.Title class="p-6 pb-0 text-2xl" level={2}>Issues</Card.Title>
6161
<Card.Content>
62-
<EventsDataTable bind:limit={limit.value} {filter} mode="stack_frequent" pageFilter="(type:404 OR type:error)" {rowclick} {time}>
62+
<EventsDataTable bind:limit={limit.value} {filter} filters={persistedFilters.value} mode="stack_frequent" pageFilter="(type:404 OR type:error)" {rowclick} {time}>
6363
{#snippet toolbarChildren()}
6464
<FacetedFilter.Root changed={onFilterChanged} {facets} remove={onFilterRemoved}></FacetedFilter.Root>
6565
{/snippet}

src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<Card.Root>
4545
<Card.Title class="p-6 pb-0 text-2xl" level={2}>Event Stream</Card.Title>
4646
<Card.Content>
47-
<EventsTailLogDataTable bind:limit={limit.value} {filter} {rowclick}>
47+
<EventsTailLogDataTable bind:limit={limit.value} {filter} filters={persistedFilters.value} {rowclick}>
4848
{#snippet toolbarChildren()}
4949
<FacetedFilter.Root changed={onFilterChanged} {facets} remove={onFilterRemoved}></FacetedFilter.Root>
5050
{/snippet}

0 commit comments

Comments
 (0)