Skip to content

Commit 3f8c147

Browse files
committed
Next: Work on grid selection and refresh icon.
1 parent 001bc6b commit 3f8c147

File tree

7 files changed

+107
-55
lines changed

7 files changed

+107
-55
lines changed

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,6 @@ export function getTableContext<TSummaryModel extends SummaryModel<SummaryTempla
169169
const previousPageIndex = pagination().pageIndex;
170170
setPagination(updaterOrValue);
171171

172-
// Force a reset of the row selection state until we get smarter about it.
173-
setRowSelection({});
174-
175172
const currentPageInfo = pagination();
176173
_parameters = {
177174
..._parameters,

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/Overview.svelte

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ClickableVersionFilter from '$comp/filters/ClickableVersionFilter.svelte';
1111
import Duration from '$comp/formatters/Duration.svelte';
1212
import TimeAgo from '$comp/formatters/TimeAgo.svelte';
13+
import Live from '$comp/Live.svelte';
1314
import { A, H4 } from '$comp/typography';
1415
import { Badge } from '$comp/ui/badge';
1516
import { Button } from '$comp/ui/button';
@@ -96,11 +97,7 @@
9697
<Table.Head class="w-40 whitespace-nowrap">Duration</Table.Head>
9798
<Table.Cell class="w-4 pr-0"></Table.Cell>
9899
<Table.Cell>
99-
{#if !event.data?.sessionend}
100-
<span class="inline-flex h-2 w-2 animate-pulse items-center rounded-full bg-green-500" title="Online"></span>
101-
{:else}
102-
<span class="inline-flex h-2 w-2 items-center rounded-full bg-destructive" title="Ended"></span>
103-
{/if}
100+
<Live live={!event.data?.sessionend} liveTitle="Online" notLiveTitle="Ended" />
104101
<Duration value={getSessionStartDuration(event)}></Duration>
105102
{#if event.data?.sessionend}
106103
(ended <TimeAgo value={event.data.sessionend}></TimeAgo>)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script lang="ts">
2+
import Live from '$comp/Live.svelte';
3+
import { Button } from '$comp/ui/button';
4+
5+
interface Props {
6+
canRefresh: boolean;
7+
refresh: () => Promise<void>;
8+
}
9+
10+
let { canRefresh, refresh }: Props = $props();
11+
12+
const refreshButtonTitle = $derived(
13+
canRefresh ? 'Data will automatically-refresh' : 'Refresh data which is not automatically-refreshing'
14+
);
15+
</script>
16+
17+
{#if canRefresh}
18+
<div class="inline-flex h-6">
19+
<Live liveTitle={refreshButtonTitle} class="ml-3 size-3 motion-safe:animate-none" />
20+
</div>
21+
{:else}
22+
<Button variant="ghost" size="icon" onclick={refresh} title={refreshButtonTitle}>
23+
<Live live={false} notLiveTitle={refreshButtonTitle} class="size-3 motion-safe:animate-none" />
24+
</Button>
25+
{/if}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts">
2+
import type { HTMLAttributes } from 'svelte/elements';
3+
4+
import { cn } from '$lib/utils';
5+
6+
type Props = HTMLAttributes<Element> & {
7+
live?: boolean;
8+
liveTitle?: string;
9+
notLiveTitle?: string;
10+
};
11+
12+
let { class: className, live = true, liveTitle = 'Live', notLiveTitle = 'Not Live' }: Props = $props();
13+
</script>
14+
15+
{#if live}
16+
<span class={cn('inline-flex size-2 items-center rounded-full bg-green-500 motion-safe:animate-pulse', className)} title={liveTitle}></span>
17+
{:else}
18+
<span class={cn('inline-flex size-2 items-center rounded-full bg-destructive', className)} title={notLiveTitle}></span>
19+
{/if}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { type Table as SvelteTable } from '@tanstack/svelte-table';
2+
3+
export function isTableEmpty<TData>(table: SvelteTable<TData>): boolean {
4+
return table.options.data.length === 0;
5+
}
6+
7+
/***
8+
* Removes data from the table.
9+
* @param table The table to remove data from.
10+
* @param predicate A function that determines whether a row should be removed.
11+
* @returns True if data was removed, false otherwise.
12+
*/
13+
export function removeTableData<TData>(table: SvelteTable<TData>, predicate: (value: TData, index: number, array: TData[]) => boolean): boolean {
14+
if (table.options.data.some(predicate)) {
15+
table.options.data = table.options.data.filter((value, index, array) => !predicate(value, index, array));
16+
return true;
17+
}
18+
19+
return false;
20+
}
21+
22+
/***
23+
* Removes a selection from the table.
24+
* @param table The table to remove the selection from.
25+
* @param selectionId The id of the selection to remove.
26+
* @returns True if the selection was removed, false otherwise.
27+
*/
28+
export function removeTableSelection<TData>(table: SvelteTable<TData>, selectionId: string): boolean {
29+
if (table.getIsSomeRowsSelected()) {
30+
const { rowSelection } = table.getState();
31+
if (rowSelection[selectionId]) {
32+
table.setRowSelection((old: Record<string, boolean>) => {
33+
const filtered = Object.entries(old).filter(([id]) => id !== selectionId);
34+
return Object.fromEntries(filtered);
35+
});
36+
37+
return true;
38+
}
39+
}
40+
41+
return false;
42+
}

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

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import type { EventSummaryModel, SummaryTemplateKeys } from '$features/events/components/summary/index';
33
4-
import * as DataTable from '$comp/data-table';
4+
import AutomaticRefreshIndicatorButton from '$comp/AutomaticRefreshIndicatorButton.svelte';
55
import * as FacetedFilter from '$comp/faceted-filter';
66
import { toFacetedFilters } from '$comp/filters/facets';
77
import { DateFilter, filterChanged, filterRemoved, FilterSerializer, getDefaultFilters, type IFilter, toFilter } from '$comp/filters/filters.svelte';
@@ -12,6 +12,7 @@
1212
import { shouldRefreshPersistentEventChanged } from '$features/events/components/filters';
1313
import EventsDataTable from '$features/events/components/table/EventsDataTable.svelte';
1414
import { getTableContext } from '$features/events/components/table/options.svelte';
15+
import { isTableEmpty, removeTableData, removeTableSelection } from '$features/shared/table';
1516
import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models';
1617
import { useFetchClientStatus } from '$shared/api/api.svelte';
1718
import { persisted } from '$shared/persisted.svelte';
@@ -26,7 +27,6 @@
2627
selectedEventId = row.id;
2728
}
2829
29-
let showRefreshStaleDataRow = $state(false);
3030
const limit = persisted<number>('events.limit', 10);
3131
const defaultFilters = getDefaultFilters();
3232
const persistedFilters = persisted<IFilter[]>('events.filters', defaultFilters, new FilterSerializer());
@@ -51,52 +51,39 @@
5151
5252
const context = getTableContext<EventSummaryModel<SummaryTemplateKeys>>({ limit: limit.value, mode: 'summary' });
5353
const table = createTable(context.options);
54+
const canRefresh = $derived(!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected() && !table.getCanPreviousPage());
5455
5556
const client = useFetchClient();
5657
const clientStatus = useFetchClientStatus(client);
57-
58-
let response = $state<FetchClientResponse<EventSummaryModel<SummaryTemplateKeys>[]>>();
58+
let clientResponse = $state<FetchClientResponse<EventSummaryModel<SummaryTemplateKeys>[]>>();
5959
6060
async function loadData() {
6161
if (client.isLoading) {
6262
return;
6363
}
6464
65-
response = await client.getJSON<EventSummaryModel<SummaryTemplateKeys>[]>('events', {
65+
clientResponse = await client.getJSON<EventSummaryModel<SummaryTemplateKeys>[]>('events', {
6666
params: {
6767
...context.parameters,
6868
filter,
6969
time
7070
}
7171
});
7272
73-
if (response.ok) {
74-
context.data = response.data || [];
75-
context.meta = response.meta;
76-
table.resetRowSelection();
73+
if (clientResponse.ok) {
74+
context.data = clientResponse.data || [];
75+
context.meta = clientResponse.meta;
7776
}
7877
}
7978
const debouncedLoadData = debounce(10000, loadData);
8079
8180
async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) {
8281
if (message.id && message.change_type === ChangeType.Removed) {
83-
// Remove the event from the selection if it was selected
84-
if (table.getIsSomeRowsSelected()) {
85-
const { rowSelection } = table.getState();
86-
if (message.id && rowSelection[message.id]) {
87-
table.setRowSelection((old) => {
88-
const filtered = Object.entries(old).filter(([id]) => id !== message.id);
89-
return Object.fromEntries(filtered);
90-
});
91-
}
92-
}
93-
94-
// Remove deleted event from the grid data
95-
if (table.options.data.find((doc) => doc.id === message.id)) {
96-
table.options.data = table.options.data.filter((doc) => doc.id !== message.id);
82+
removeTableSelection(table, message.id);
9783
84+
if (removeTableData(table, (doc) => doc.id === message.id)) {
9885
// If the grid data is empty from all events being removed, we should refresh the data.
99-
if (table.options.data.length === 0) {
86+
if (isTableEmpty(table)) {
10087
await debouncedLoadData();
10188
return;
10289
}
@@ -108,27 +95,15 @@
10895
return;
10996
}
11097
111-
// Do not refresh if the grid has selections.
112-
if (table.getIsSomeRowsSelected()) {
113-
showRefreshStaleDataRow = true;
114-
return;
115-
}
116-
117-
// Do not refresh if the grid is currently paged.
118-
if (table.getPageCount() > 1) {
119-
showRefreshStaleDataRow = true;
98+
// Do not refresh if the grid has selections or grid is currently paged.
99+
if (canRefresh) {
120100
return;
121101
}
122102
123103
await debouncedLoadData();
124104
}
125105
126-
async function refresh() {
127-
showRefreshStaleDataRow = false;
128-
await loadData();
129-
}
130-
131-
useEventListener(document, 'refresh', async () => await loadData());
106+
useEventListener(document, 'refresh', () => loadData());
132107
useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail));
133108
134109
$effect(() => {
@@ -138,17 +113,15 @@
138113

139114
<div class="flex flex-col space-y-4">
140115
<Card.Root>
141-
<Card.Title class="p-6 pb-0 text-2xl" level={2}>Events</Card.Title>
142-
<Card.Content>
116+
<Card.Title class="gap-x-1 p-6 pb-0 text-2xl" level={2}
117+
>Events
118+
<AutomaticRefreshIndicatorButton {canRefresh} refresh={loadData} /></Card.Title
119+
>
120+
<Card.Content class="pt-4">
143121
<EventsDataTable bind:limit={limit.value} isLoading={clientStatus.isLoading} rowClick={rowclick} {table}>
144122
{#snippet toolbarChildren()}
145123
<FacetedFilter.Root changed={onFilterChanged} {facets} remove={onFilterRemoved}></FacetedFilter.Root>
146124
{/snippet}
147-
{#snippet bodyChildren()}
148-
{#if showRefreshStaleDataRow}
149-
<DataTable.DataTableRefresh {table} {refresh} />
150-
{/if}
151-
{/snippet}
152125
</EventsDataTable>
153126
</Card.Content>
154127
</Card.Root>

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@
9090
if (response.ok) {
9191
context.data = response.data || [];
9292
context.meta = response.meta;
93-
table.resetRowSelection();
9493
}
9594
}
9695
const debouncedLoadData = debounce(10000, loadData);

0 commit comments

Comments
 (0)