Skip to content

Commit 7875bb0

Browse files
committed
Adds streaming updates indicator button
Replaces the automatic refresh button with a manual toggle for streaming updates. This change allows users to pause and resume the stream of updates, providing more control over data refreshing. The button is only enabled when the table is in a state where it can be automatically updated (i.e., no rows are selected and the first page is displayed).
1 parent 1cd624c commit 7875bb0

File tree

7 files changed

+120
-50
lines changed

7 files changed

+120
-50
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,23 @@
1414
limit: number;
1515
rowClick?: (row: EventSummaryModel<SummaryTemplateKeys>) => void;
1616
table: Table<EventSummaryModel<SummaryTemplateKeys>>;
17+
toolbarActions?: Snippet;
1718
toolbarChildren?: Snippet;
1819
}
1920
20-
let { bodyChildren, footerChildren, isLoading, limit = $bindable(), rowClick, table, toolbarChildren }: Props = $props();
21+
let { bodyChildren, footerChildren, isLoading, limit = $bindable(), rowClick, table, toolbarActions, toolbarChildren }: Props = $props();
2122
</script>
2223

2324
<DataTable.Root>
2425
<DataTable.Toolbar {table}>
2526
{#if toolbarChildren}
2627
{@render toolbarChildren()}
2728
{/if}
29+
{#snippet actions()}
30+
{#if toolbarActions}
31+
{@render toolbarActions()}
32+
{/if}
33+
{/snippet}
2834
</DataTable.Toolbar>
2935
<DataTable.Body {rowClick} {table}>
3036
{#if isLoading}

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/automatic-refresh-indicator-button.svelte

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-toolbar.svelte

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@
77
import type { Snippet } from 'svelte';
88
99
import DataTableViewOptions from './data-table-view-options.svelte';
10-
import AutomaticRefreshIndicatorButton from '$comp/automatic-refresh-indicator-button.svelte';
1110
1211
interface Props {
12+
actions?: Snippet;
1313
children: Snippet;
1414
table: Table<TData>;
1515
}
1616
17-
let { children, table }: Props = $props();
17+
let { actions, children, table }: Props = $props();
1818
</script>
1919

2020
<div class="flex items-center justify-between gap-x-2">
2121
<div class="flex flex-1 flex-wrap items-center gap-x-2 gap-y-2">
2222
{@render children()}
23-
<div class="ml-auto flex">
24-
<AutomaticRefreshIndicatorButton canRefresh={true} refresh={() => Promise.resolve()} />
23+
<div class="ml-auto flex gap-x-2">
24+
{#if actions}
25+
{@render actions()}
26+
{/if}
2527
<DataTableViewOptions {table} />
2628
</div>
2729
</div>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script lang="ts">
2+
import { Button } from '$comp/ui/button';
3+
import Play from '@lucide/svelte/icons/play';
4+
5+
interface Props {
6+
onToggle: () => void;
7+
paused: boolean;
8+
}
9+
10+
let { onToggle, paused }: Props = $props();
11+
12+
const title = $derived(paused ? 'Resume streaming updates' : 'Pause streaming updates');
13+
</script>
14+
15+
<Button variant="outline" size="icon" onclick={onToggle} {title}>
16+
{#if paused}
17+
<Play class="text-primary fill-primary size-4" />
18+
{:else}
19+
<span class="inline-flex size-2 animate-pulse items-center rounded-full bg-red-500"></span>
20+
{/if}
21+
</Button>

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

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
import type { EventSummaryModel, SummaryTemplateKeys } from '$features/events/components/summary/index';
44
55
import { page } from '$app/state';
6-
import AutomaticRefreshIndicatorButton from '$comp/automatic-refresh-indicator-button.svelte';
76
import * as DataTable from '$comp/data-table';
87
import * as FacetedFilter from '$comp/faceted-filter';
8+
import StreamingIndicatorButton from '$comp/streaming-indicator-button.svelte';
99
import { Button } from '$comp/ui/button';
10-
import * as Card from '$comp/ui/card';
1110
import * as Sheet from '$comp/ui/sheet';
1211
import EventsOverview from '$features/events/components/events-overview.svelte';
1312
import { type DateFilter, StatusFilter } from '$features/events/components/filters';
@@ -72,6 +71,7 @@
7271
updateFilterCache(filterCacheKey(DEFAULT_PARAMS.filter), DEFAULT_FILTERS);
7372
//params.$reset(); // Work around for https://github.com/beynar/kit-query-params/issues/7
7473
Object.assign(queryParams, DEFAULT_PARAMS);
74+
reset();
7575
},
7676
{ lazy: true }
7777
);
@@ -147,9 +147,30 @@
147147
})
148148
);
149149
150-
const canRefresh = $derived(!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && !table.getCanPreviousPage());
150+
const canRefresh = $derived(!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && table.getState().pagination.pageIndex === 0);
151+
152+
let manualPause = $state(false);
153+
let paused = $derived(manualPause || !canRefresh);
154+
155+
function reset() {
156+
manualPause = false;
157+
table.resetRowSelection();
158+
table.setPageIndex(0);
159+
}
160+
161+
function handleToggle() {
162+
if (!canRefresh) {
163+
reset();
164+
} else {
165+
manualPause = !manualPause;
166+
}
167+
}
151168
152169
async function loadData() {
170+
if (paused) {
171+
return;
172+
}
173+
153174
if (client.isLoading || !organization.current) {
154175
return;
155176
}
@@ -166,25 +187,24 @@
166187
167188
if (removeTableData(table, (doc) => doc.id === message.id)) {
168189
// If the grid data is empty from all events being removed, we should refresh the data.
169-
if (isTableEmpty(table)) {
190+
if (isTableEmpty(table) && !paused) {
170191
await throttledLoadData();
171192
return;
172193
}
173194
}
174195
}
175196
197+
if (paused) {
198+
return;
199+
}
200+
176201
// Do not refresh if the filter criteria doesn't match the web socket message.
177202
if (
178203
!shouldRefreshPersistentEventChanged(filters ?? [], queryParams.filter, message.organization_id, message.project_id, message.stack_id, message.id)
179204
) {
180205
return;
181206
}
182207
183-
// Do not refresh if the grid has selections or grid is currently paged.
184-
if (!canRefresh) {
185-
return;
186-
}
187-
188208
await throttledLoadData();
189209
}
190210
@@ -199,11 +219,14 @@
199219
<div class="flex flex-col space-y-4">
200220
<EventsDataTable bind:limit={queryParams.limit!} isLoading={clientStatus.isLoading} rowClick={rowclick} {table}>
201221
{#snippet toolbarChildren()}
202-
<div class="text-lg font-medium pr-2">Events</div>
222+
<div class="pr-2 text-lg font-medium">Events</div>
203223
<FacetedFilter.Root changed={onFilterChanged} {filters} remove={onFilterRemoved}>
204224
<OrganizationDefaultsFacetedFilterBuilder />
205225
</FacetedFilter.Root>
206226
{/snippet}
227+
{#snippet toolbarActions()}
228+
<StreamingIndicatorButton {paused} onToggle={handleToggle} />
229+
{/snippet}
207230
{#snippet footerChildren()}
208231
<div class="h-9 min-w-[140px]">
209232
{#if table.getSelectedRowModel().flatRows.length}

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import type { EventSummaryModel, SummaryTemplateKeys } from '$features/events/components/summary/index';
33
44
import { page } from '$app/state';
5-
import AutomaticRefreshIndicatorButton from '$comp/automatic-refresh-indicator-button.svelte';
65
import * as DataTable from '$comp/data-table';
76
import * as FacetedFilter from '$comp/faceted-filter';
7+
import StreamingIndicatorButton from '$comp/streaming-indicator-button.svelte';
88
import { Button } from '$comp/ui/button';
9-
import * as Card from '$comp/ui/card';
109
import * as Sheet from '$comp/ui/sheet';
1110
import { type GetEventsParams, getStackEventsQuery } from '$features/events/api.svelte';
1211
import EventsOverview from '$features/events/components/events-overview.svelte';
@@ -85,6 +84,7 @@
8584
updateFilterCache(filterCacheKey(DEFAULT_PARAMS.filter), DEFAULT_FILTERS);
8685
//params.$reset(); // Work around for https://github.com/beynar/kit-query-params/issues/7
8786
Object.assign(queryParams, DEFAULT_PARAMS);
87+
reset();
8888
},
8989
{ lazy: true }
9090
);
@@ -159,9 +159,30 @@
159159
})
160160
);
161161
162-
const canRefresh = $derived(!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && !table.getCanPreviousPage());
162+
const canRefresh = $derived(!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && table.getState().pagination.pageIndex === 0);
163+
164+
let manualPause = $state(false);
165+
let paused = $derived(manualPause || !canRefresh);
166+
167+
function reset() {
168+
manualPause = false;
169+
table.resetRowSelection();
170+
table.setPageIndex(0);
171+
}
172+
173+
function handleToggle() {
174+
if (!canRefresh) {
175+
reset();
176+
} else {
177+
manualPause = !manualPause;
178+
}
179+
}
163180
164181
async function loadData() {
182+
if (paused) {
183+
return;
184+
}
185+
165186
if (client.isLoading || !organization.current) {
166187
return;
167188
}
@@ -179,20 +200,19 @@
179200
180201
if (removeTableData(table, (doc) => doc.id === message.id)) {
181202
// If the grid data is empty from all events being removed, we should refresh the data.
182-
if (isTableEmpty(table)) {
203+
if (isTableEmpty(table) && !paused) {
183204
await throttledLoadData();
184205
return;
185206
}
186207
}
187208
}
188209
189-
// Do not refresh if the filter criteria doesn't match the web socket message.
190-
if (!shouldRefreshPersistentEventChanged(filters, queryParams.filter, message.organization_id, message.project_id, message.id)) {
210+
if (paused) {
191211
return;
192212
}
193213
194-
// Do not refresh if the grid has selections or grid is currently paged.
195-
if (!canRefresh) {
214+
// Do not refresh if the filter criteria doesn't match the web socket message.
215+
if (!shouldRefreshPersistentEventChanged(filters, queryParams.filter, message.organization_id, message.project_id, message.id)) {
196216
return;
197217
}
198218
@@ -210,11 +230,14 @@
210230
<div class="flex flex-col space-y-4">
211231
<EventsDataTable bind:limit={queryParams.limit!} isLoading={clientStatus.isLoading} rowClick={rowclick} {table}>
212232
{#snippet toolbarChildren()}
213-
<div class="text-lg font-medium pr-2">Issues</div>
233+
<div class="pr-2 text-lg font-medium">Issues</div>
214234
<FacetedFilter.Root changed={onFilterChanged} {filters} remove={onFilterRemoved}>
215235
<OrganizationDefaultsFacetedFilterBuilder />
216236
</FacetedFilter.Root>
217237
{/snippet}
238+
{#snippet toolbarActions()}
239+
<StreamingIndicatorButton {paused} onToggle={handleToggle} />
240+
{/snippet}
218241
{#snippet footerChildren()}
219242
<div class="h-9 min-w-[140px]">
220243
<TableStacksBulkActionsDropdownMenu {table} />

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import DelayedRender from '$comp/delayed-render.svelte';
88
import ErrorMessage from '$comp/error-message.svelte';
99
import * as FacetedFilter from '$comp/faceted-filter';
10+
import StreamingIndicatorButton from '$comp/streaming-indicator-button.svelte';
1011
import { Button } from '$comp/ui/button';
11-
import * as Card from '$comp/ui/card';
1212
import * as Sheet from '$comp/ui/sheet';
1313
import EventsOverview from '$features/events/components/events-overview.svelte';
1414
import { StatusFilter } from '$features/events/components/filters';
@@ -67,6 +67,7 @@
6767
updateFilterCache(filterCacheKey(DEFAULT_PARAMS.filter), DEFAULT_FILTERS);
6868
//params.$reset(); // Work around for https://github.com/beynar/kit-query-params/issues/7
6969
Object.assign(queryParams, DEFAULT_PARAMS);
70+
paused = false;
7071
},
7172
{ lazy: true }
7273
);
@@ -146,7 +147,16 @@
146147
})
147148
);
148149
150+
let paused = $state(false);
151+
function handleToggle() {
152+
paused = !paused;
153+
}
154+
149155
async function loadData(filterChanged: boolean = false) {
156+
if (paused) {
157+
return;
158+
}
159+
150160
if (!organization.current) {
151161
return;
152162
}
@@ -185,13 +195,17 @@
185195
if (message.id && message.change_type === ChangeType.Removed) {
186196
if (removeTableData(table, (doc) => doc.id === message.id)) {
187197
// If the grid data is empty from all events being removed, we should refresh the data.
188-
if (isTableEmpty(table)) {
198+
if (isTableEmpty(table) && !paused) {
189199
await debouncedLoadData();
190200
return;
191201
}
192202
}
193203
}
194204
205+
if (paused) {
206+
return;
207+
}
208+
195209
// Do not refresh if the filter criteria doesn't match the web socket message.
196210
if (!shouldRefreshPersistentEventChanged(filters, queryParams.filter, message.organization_id, message.project_id, message.stack_id, message.id)) {
197211
return;
@@ -215,10 +229,14 @@
215229

216230
<DataTable.Root>
217231
<DataTable.Toolbar {table}>
218-
<div class="text-lg font-medium pr-2">Event Stream</div>
232+
<div class="pr-2 text-lg font-medium">Event Stream</div>
219233
<FacetedFilter.Root changed={onFilterChanged} {filters} remove={onFilterRemoved}>
220234
<OrganizationDefaultsFacetedFilterBuilder includeDateFacets={false} />
221235
</FacetedFilter.Root>
236+
237+
{#snippet actions()}
238+
<StreamingIndicatorButton {paused} onToggle={handleToggle} />
239+
{/snippet}
222240
</DataTable.Toolbar>
223241
<DataTable.Body rowClick={rowclick} {table}>
224242
{#if clientStatus.isLoading}

0 commit comments

Comments
 (0)