- {@render children()}
+ {#key $page.url.pathname}
+
+ {@render children()}
+
+ {/key}
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte
index 6a7eb8d32b..a0c7663af5 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte
@@ -8,8 +8,15 @@
import * as Card from '$comp/ui/card';
import * as Sheet from '$comp/ui/sheet';
import EventsDrawer from '$features/events/components/EventsDrawer.svelte';
+ import { shouldRefreshPersistentEventChanged } from '$features/events/components/filters';
import EventsDataTable from '$features/events/components/table/EventsDataTable.svelte';
+ import { getTableContext } from '$features/events/components/table/options.svelte';
+ import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models';
import { persisted } from '$shared/persisted.svelte';
+ import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient';
+ import { createTable } from '@tanstack/svelte-table';
+ import { useEventListener } from 'runed';
+ import { debounce } from 'throttle-debounce';
import IconOpenInNew from '~icons/mdi/open-in-new';
let selectedEventId: null | string = $state(null);
@@ -38,13 +45,72 @@
function onFilterRemoved(filter?: IFilter): void {
persistedFilters.value = filterRemoved(persistedFilters.value, defaultFilters, filter);
}
+
+ const context = getTableContext
>({ limit: limit.value, mode: 'summary' });
+ const table = createTable(context.options);
+
+ const client = useFetchClient();
+ let response: FetchClientResponse[]>;
+
+ async function loadData() {
+ if (client.loading) {
+ return;
+ }
+
+ response = await client.getJSON[]>('events', {
+ params: {
+ ...context.parameters,
+ filter,
+ time
+ }
+ });
+
+ if (response.ok) {
+ context.data = response.data || [];
+ context.meta = response.meta;
+ table.resetRowSelection();
+ }
+ }
+ const debouncedLoadData = debounce(10000, loadData);
+
+ async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) {
+ const shouldRefresh = () =>
+ shouldRefreshPersistentEventChanged(persistedFilters.value, filter, message.organization_id, message.project_id, message.stack_id, message.id);
+
+ switch (message.change_type) {
+ case ChangeType.Added:
+ case ChangeType.Saved:
+ if (shouldRefresh()) {
+ await debouncedLoadData();
+ }
+
+ break;
+ case ChangeType.Removed:
+ if (shouldRefresh()) {
+ if (message.id) {
+ table.options.data = table.options.data.filter((doc) => doc.id !== message.id);
+ }
+
+ await debouncedLoadData();
+ }
+
+ break;
+ }
+ }
+
+ useEventListener(document, 'refresh', async () => await loadData());
+ useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail));
+
+ $effect(() => {
+ loadData();
+ });
Events
-
+
{#snippet toolbarChildren()}
{/snippet}
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte
index 2bcd1c5810..ceb08a68a9 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte
@@ -31,7 +31,7 @@
}
});
- const updateEmailAddressForm = superForm(defaults(userResponse.data ?? {}, classvalidatorClient(User)), {
+ const updateEmailAddressForm = superForm(defaults(userResponse.data ?? new User(), classvalidatorClient(User)), {
dataType: 'json',
id: 'update-email-address',
async onUpdate({ form, result }) {
@@ -58,7 +58,7 @@
validators: classvalidatorClient(User)
});
- const updateUserForm = superForm(defaults(userResponse.data ?? {}, classvalidatorClient(UpdateUser)), {
+ const updateUserForm = superForm(defaults(userResponse.data ?? new UpdateUser(), classvalidatorClient(UpdateUser)), {
dataType: 'json',
id: 'update-user',
async onUpdate({ form, result }) {
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte
index 8b5edcc222..85a1922a8a 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte
@@ -9,8 +9,15 @@
import * as Sheet from '$comp/ui/sheet';
import { getEventsByStackIdQuery } from '$features/events/api.svelte';
import EventsDrawer from '$features/events/components/EventsDrawer.svelte';
+ import { shouldRefreshPersistentEventChanged } from '$features/events/components/filters';
import EventsDataTable from '$features/events/components/table/EventsDataTable.svelte';
+ import { getTableContext } from '$features/events/components/table/options.svelte';
+ import { type WebSocketMessageValue } from '$features/websockets/models';
import { persisted } from '$shared/persisted.svelte';
+ import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient';
+ import { createTable } from '@tanstack/svelte-table';
+ import { useEventListener } from 'runed';
+ import { debounce } from 'throttle-debounce';
import IconOpenInNew from '~icons/mdi/open-in-new';
let selectedStackId = $state();
@@ -53,13 +60,52 @@
function onFilterRemoved(filter?: IFilter): void {
persistedFilters.value = filterRemoved(persistedFilters.value, defaultFilters, filter);
}
+
+ const context = getTableContext>({ limit: limit.value, mode: 'stack_frequent' });
+ const table = createTable(context.options);
+
+ const client = useFetchClient();
+ let response: FetchClientResponse[]>;
+
+ async function loadData() {
+ if (client.loading) {
+ return;
+ }
+
+ response = await client.getJSON[]>('events', {
+ params: {
+ ...context.parameters,
+ filter: ['(type:404 OR type:error)', filter].filter(Boolean).join(' '),
+ time
+ }
+ });
+
+ if (response.ok) {
+ context.data = response.data || [];
+ context.meta = response.meta;
+ table.resetRowSelection();
+ }
+ }
+ const debouncedLoadData = debounce(10000, loadData);
+ async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) {
+ if (shouldRefreshPersistentEventChanged(persistedFilters.value, filter, message.organization_id, message.project_id, message.stack_id, message.id)) {
+ await debouncedLoadData();
+ }
+ }
+
+ useEventListener(document, 'refresh', async () => await loadData());
+ useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail));
+
+ $effect(() => {
+ loadData();
+ });
Issues
-
+
{#snippet toolbarChildren()}
{/snippet}
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte
index b9b8198f04..8cbe32a663 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte
@@ -1,15 +1,24 @@
Event Stream
-
- {#snippet toolbarChildren()}
+
+
- {/snippet}
-
+
+
+
+
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/tsconfig.json b/src/Exceptionless.Web/ClientApp/tsconfig.json
index 305f1e901f..7ce50b4eaf 100644
--- a/src/Exceptionless.Web/ClientApp/tsconfig.json
+++ b/src/Exceptionless.Web/ClientApp/tsconfig.json
@@ -7,11 +7,14 @@
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"forceConsistentCasingInFileNames": true,
+ "moduleDetection": "force",
+ "moduleResolution": "bundler",
+ "noImplicitOverride": true,
+ "noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
- "strict": true,
- "moduleResolution": "bundler"
+ "strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
diff --git a/src/Exceptionless.Web/ClientApp/vite.config.ts b/src/Exceptionless.Web/ClientApp/vite.config.ts
index 34e8c47f5f..f366b5d315 100644
--- a/src/Exceptionless.Web/ClientApp/vite.config.ts
+++ b/src/Exceptionless.Web/ClientApp/vite.config.ts
@@ -87,7 +87,7 @@ function getAspNetConfig() {
if (aspnetHttpsPort) {
url = `https://localhost:${aspnetHttpsPort}`;
} else if (aspnetUrls) {
- url = aspnetUrls.split(';')[0];
+ url = aspnetUrls.split(';')[0] as string;
}
const wsUrl = url.replace('https://', 'wss://').replace('http://', 'ws://');