Skip to content

Commit 9d9adb1

Browse files
authored
Merge branch 'main' into issue/saved-view-column-settings
2 parents 02b5011 + b74329d commit 9d9adb1

11 files changed

Lines changed: 1557 additions & 1384 deletions

File tree

src/Exceptionless.Web/ClientApp/e2e/tests/event-effects-chaos.e2e.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ test('event list and detail effects stay bounded through paging and background c
6868

6969
await waitForEventListQuiescence(page, diagnostics);
7070

71+
await measureAction(diagnostics, 'event list selected refresh', async () => {
72+
const rowSelection = page.getByRole('checkbox', { name: 'Select row' }).first();
73+
await rowSelection.click();
74+
await expect(rowSelection).toBeChecked();
75+
76+
const response = page.waitForResponse((candidate) => isEventListResponse(candidate, e2eScenario.organizationId));
77+
await page.getByTitle('Return to the first page to refresh results').click();
78+
expect((await response).ok()).toBe(true);
79+
await expect(rowSelection).not.toBeChecked();
80+
});
81+
expect(actionSample(diagnostics, 'event list selected refresh').eventList).toBe(1);
82+
7183
await measureAction(diagnostics, 'event list paging', async () => {
7284
for (let index = 0; index < 4; index++) {
7385
await clickAndWaitForList(page, e2eScenario.organizationId, 'Go to next page');

src/Exceptionless.Web/ClientApp/e2e/tests/stack-effects-chaos.e2e.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ test('stack effects stay bounded through background, paging, and navigation chao
5151
await expect(page.getByRole('button', { name: 'Go to next page' })).toBeEnabled();
5252
});
5353

54+
await measureAction(diagnostics, 'selected stack refresh', async () => {
55+
const rowSelection = page.getByRole('checkbox', { name: 'Select row' }).first();
56+
await rowSelection.click();
57+
await expect(rowSelection).toBeChecked();
58+
59+
const response = page.waitForResponse((candidate) => isStackListResponse(candidate, e2eScenario.organizationId));
60+
await page.getByTitle('Return to the first page to refresh results').click();
61+
expect((await response).ok()).toBe(true);
62+
await expect(rowSelection).not.toBeChecked();
63+
});
64+
expect(actionSample(diagnostics, 'selected stack refresh').listRequests).toBe(1);
65+
5466
await measureAction(diagnostics, 'paging', async () => {
5567
for (let index = 0; index < 4; index++) {
5668
await page.getByRole('button', { name: 'Go to next page' }).click();

src/Exceptionless.Web/ClientApp/package-lock.json

Lines changed: 1263 additions & 1132 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Exceptionless.Web/ClientApp/package.json

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,73 +36,73 @@
3636
"@chromatic-com/storybook": "^5.2.1",
3737
"@eslint/compat": "^2.1.0",
3838
"@eslint/js": "^10.0.1",
39-
"@iconify-json/lucide": "^1.2.117",
40-
"@playwright/test": "^1.61.1",
41-
"@storybook/addon-a11y": "^10.5.2",
42-
"@storybook/addon-docs": "^10.5.2",
39+
"@iconify-json/lucide": "^1.2.120",
40+
"@playwright/test": "^1.62.0",
41+
"@storybook/addon-a11y": "^10.5.5",
42+
"@storybook/addon-docs": "^10.5.5",
4343
"@storybook/addon-svelte-csf": "^5.1.2",
44-
"@storybook/sveltekit": "^10.5.2",
44+
"@storybook/sveltekit": "^10.5.5",
4545
"@sveltejs/adapter-static": "^3.0.10",
46-
"@sveltejs/kit": "^2.69.3",
47-
"@sveltejs/vite-plugin-svelte": "^6.2.4",
46+
"@sveltejs/kit": "^2.70.1",
47+
"@sveltejs/vite-plugin-svelte": "^7.2.0",
4848
"@tailwindcss/vite": "^4.3.3",
49-
"@tanstack/eslint-plugin-query": "^5.101.2",
50-
"@testing-library/jest-dom": "^6.9.1",
49+
"@tanstack/eslint-plugin-query": "^5.101.4",
50+
"@testing-library/jest-dom": "^7.0.0",
5151
"@testing-library/svelte": "^5.4.2",
5252
"@types/eslint": "^9.6.1",
53-
"@types/node": "^26.1.1",
53+
"@types/node": "^26.1.2",
5454
"@types/throttle-debounce": "^5.0.2",
5555
"cross-env": "^10.1.0",
56-
"eslint": "^10.7.0",
56+
"eslint": "^10.8.0",
5757
"eslint-config-prettier": "^10.1.8",
5858
"eslint-plugin-perfectionist": "^5.10.0",
59-
"eslint-plugin-storybook": "^10.5.2",
60-
"eslint-plugin-svelte": "^3.20.0",
61-
"globals": "^17.7.0",
62-
"jsdom": "^29.1.1",
63-
"prettier": "^3.9.5",
59+
"eslint-plugin-storybook": "^10.5.5",
60+
"eslint-plugin-svelte": "^3.22.0",
61+
"globals": "^17.8.0",
62+
"jsdom": "^30.0.0",
63+
"prettier": "^3.9.6",
6464
"prettier-plugin-svelte": "^4.1.1",
6565
"prettier-plugin-tailwindcss": "^0.8.1",
66-
"storybook": "^10.5.2",
67-
"svelte": "^5.56.6",
68-
"svelte-check": "^4.7.3",
69-
"swagger-typescript-api": "^13.12.5",
66+
"storybook": "^10.5.5",
67+
"svelte": "^5.56.8",
68+
"svelte-check": "^4.7.4",
69+
"swagger-typescript-api": "^13.12.6",
7070
"tslib": "^2.8.1",
7171
"typescript": "^6.0.3",
72-
"typescript-eslint": "^8.64.0",
73-
"vite": "^7.3.5",
72+
"typescript-eslint": "^8.65.0",
73+
"vite": "^8.1.5",
7474
"vitest": "4.1.10",
7575
"vitest-websocket-mock": "^0.7.0",
7676
"zod": "^4.4.3"
7777
},
7878
"dependencies": {
7979
"@exceptionless/browser": "^3.2.1",
80-
"@foundatiofx/fetchclient": "^1.3.3",
80+
"@foundatiofx/fetchclient": "^1.3.4",
8181
"@internationalized/date": "^3.12.2",
82-
"@lucide/svelte": "^1.24.0",
83-
"@stripe/stripe-js": "^9.10.0",
82+
"@lucide/svelte": "^1.27.0",
83+
"@stripe/stripe-js": "^9.12.1",
8484
"@tanstack/svelte-form": "^1.33.2",
85-
"@tanstack/svelte-query": "^6.1.36",
86-
"@tanstack/svelte-query-devtools": "^6.1.36",
87-
"@tanstack/svelte-table": "^9.0.0-beta.52",
85+
"@tanstack/svelte-query": "^6.1.38",
86+
"@tanstack/svelte-query-devtools": "^6.1.38",
87+
"@tanstack/svelte-table": "^9.0.0-beta.58",
8888
"@types/d3-scale": "^4.0.9",
8989
"@types/d3-shape": "^3.1.8",
9090
"bits-ui": "^2.18.1",
9191
"clsx": "^2.1.1",
9292
"d3-scale": "^4.0.2",
9393
"dompurify": "^3.4.12",
9494
"kit-query-params": "^0.0.26",
95-
"layerchart": "^2.0.1",
95+
"layerchart": "^2.0.2",
9696
"mode-watcher": "^1.1.0",
9797
"oidc-client-ts": "^3.5.0",
9898
"pretty-ms": "^9.3.0",
9999
"runed": "^0.37.1",
100100
"shiki": "^4.3.1",
101101
"svelte-intercom": "^0.0.35",
102102
"svelte-sonner": "^1.1.1",
103-
"svelte-time": "^2.2.0",
103+
"svelte-time": "^2.3.0",
104104
"tailwind-merge": "^3.6.0",
105-
"tailwind-variants": "^3.2.2",
105+
"tailwind-variants": "^3.3.0",
106106
"tailwindcss": "^4.3.3",
107107
"throttle-debounce": "^5.0.2",
108108
"tw-animate-css": "^1.4.0"

src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { CountResult, WorkInProgressResult } from '$shared/models';
44
import { accessToken } from '$features/auth/index.svelte';
55
import { queryKeys as stackQueryKeys } from '$features/stacks/api.svelte';
66
import { DEFAULT_OFFSET } from '$shared/api/api.svelte';
7-
import { type ProblemDetails, useFetchClient } from '@foundatiofx/fetchclient';
7+
import { type FetchClientResponse, type ProblemDetails, useFetchClient } from '@foundatiofx/fetchclient';
88
import { createMutation, createQuery, keepPreviousData, QueryClient, useQueryClient } from '@tanstack/svelte-query';
99

1010
import type { EventSummaryModel, SummaryTemplateKeys } from './components/summary/index';
@@ -29,7 +29,10 @@ export async function invalidatePersistentEventQueries(queryClient: QueryClient,
2929
}
3030

3131
if (!id && !stack_id) {
32-
await queryClient.invalidateQueries({ queryKey: queryKeys.type });
32+
await queryClient.invalidateQueries({
33+
predicate: (query) => !isOrganizationEventsQueryKey(query.queryKey),
34+
queryKey: queryKeys.type
35+
});
3336
}
3437
}
3538

@@ -40,6 +43,7 @@ export const queryKeys = {
4043
id: (id: string | undefined) => [...queryKeys.type, id] as const,
4144
organizations: (id: string | undefined) => [...queryKeys.type, 'organizations', id] as const,
4245
organizationsCount: (id: string | undefined, params?: GetOrganizationCountRequest['params']) => [...queryKeys.organizations(id), 'count', params] as const,
46+
organizationsEvents: (id: string | undefined, params?: GetEventsParams) => [...queryKeys.organizations(id), 'events', params] as const,
4347
projects: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const,
4448
projectsCount: (id: string | undefined, params?: GetProjectCountRequest['params']) => [...queryKeys.projects(id), 'count', params] as const,
4549
sessionEvents: (id: string | undefined, projectId?: string | undefined, params?: GetSessionEventsRequest['params']) =>
@@ -127,6 +131,14 @@ export interface GetOrganizationCountRequest {
127131
};
128132
}
129133

134+
export interface GetOrganizationEventsRequest {
135+
enabled?: () => boolean;
136+
params?: GetEventsParams;
137+
route: {
138+
organizationId: string | undefined;
139+
};
140+
}
141+
130142
export interface GetOrganizationSessionsCountRequest {
131143
params?: {
132144
aggregations?: string;
@@ -319,6 +331,28 @@ export function getOrganizationCountQuery(request: GetOrganizationCountRequest)
319331
}));
320332
}
321333

334+
export function getOrganizationEventsQuery(request: GetOrganizationEventsRequest) {
335+
return createQuery<FetchClientResponse<EventSummaryModel<SummaryTemplateKeys>[]>, ProblemDetails>(() => {
336+
const organizationId = request.route.organizationId;
337+
const params = request.params ? { ...request.params } : undefined;
338+
339+
return {
340+
enabled: () => !!accessToken.current && !!organizationId && (request.enabled?.() ?? true),
341+
placeholderData: keepPreviousData,
342+
queryFn: async ({ signal }: { signal: AbortSignal }) => {
343+
const client = useFetchClient();
344+
return await client.getJSON<EventSummaryModel<SummaryTemplateKeys>[]>(`organizations/${organizationId}/events`, {
345+
params: params as Record<string, unknown>,
346+
signal
347+
});
348+
},
349+
queryKey: queryKeys.organizationsEvents(organizationId, params),
350+
refetchOnWindowFocus: false,
351+
staleTime: 0
352+
};
353+
});
354+
}
355+
322356
/**
323357
* Get session count with aggregations for stats and chart data.
324358
* Uses aggregation: avg:value cardinality:user date:(date^offset cardinality:user)
@@ -447,15 +481,25 @@ export function getStackEventsQuery(request: GetStackEventsRequest) {
447481
}
448482

449483
export function schedulePersistentEventDeleteReconciliation(queryClient: QueryClient, eventTarget: EventTarget = document) {
484+
const invalidateQueryBackedDetails = () =>
485+
queryClient.invalidateQueries({
486+
predicate: (query) => !isOrganizationEventsQueryKey(query.queryKey),
487+
queryKey: queryKeys.type
488+
});
489+
450490
eventTarget.dispatchEvent(new Event(PERSISTENT_EVENT_DELETE_RECONCILE_EVENT));
451491
void queryClient.invalidateQueries({ queryKey: stackQueryKeys.type });
452492
setTimeout(() => {
453-
void queryClient.invalidateQueries({ queryKey: queryKeys.type });
493+
void invalidateQueryBackedDetails();
454494
void queryClient.invalidateQueries({ queryKey: stackQueryKeys.type });
455495
}, PERSISTENT_EVENT_DELETE_RECONCILE_DELAY);
456496
setTimeout(() => {
457497
eventTarget.dispatchEvent(new Event(PERSISTENT_EVENT_DELETE_RECONCILE_EVENT));
458-
void queryClient.invalidateQueries({ queryKey: queryKeys.type });
498+
void invalidateQueryBackedDetails();
459499
void queryClient.invalidateQueries({ queryKey: stackQueryKeys.type });
460500
}, PERSISTENT_EVENT_DELETE_RECONCILE_RETRY_DELAY);
461501
}
502+
503+
function isOrganizationEventsQueryKey(queryKey: readonly unknown[]): boolean {
504+
return queryKey[0] === queryKeys.type[0] && queryKey[1] === 'organizations' && queryKey[3] === 'events';
505+
}

src/Exceptionless.Web/ClientApp/src/lib/features/events/api.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,29 @@ describe('invalidatePersistentEventQueries', () => {
4444
expect(invalidateSpy).toHaveBeenCalledWith({ exact: true, queryKey: queryKeys.organizations('organization-id') });
4545
expect(invalidateSpy).not.toHaveBeenCalledWith({ queryKey: queryKeys.stacks('stack-id') });
4646
});
47+
48+
it('leaves organization event list refreshes to the document event handler for bulk updates', async () => {
49+
// Arrange
50+
const queryClient = new QueryClient();
51+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries').mockImplementation(async () => {});
52+
53+
// Act
54+
await invalidatePersistentEventQueries(queryClient, {
55+
change_type: ChangeType.Saved,
56+
data: {},
57+
organization_id: 'organization-id',
58+
project_id: 'project-id',
59+
type: 'PersistentEvent'
60+
});
61+
62+
// Assert
63+
expect(invalidateSpy).toHaveBeenCalledWith({ exact: true, queryKey: queryKeys.projects('project-id') });
64+
expect(invalidateSpy).toHaveBeenCalledWith({ exact: true, queryKey: queryKeys.organizations('organization-id') });
65+
66+
const broadInvalidation = invalidateSpy.mock.calls.find(([filters]) => filters?.queryKey === queryKeys.type)?.[0];
67+
expect(broadInvalidation?.predicate?.({ queryKey: queryKeys.organizationsEvents('organization-id') } as never)).toBe(false);
68+
expect(broadInvalidation?.predicate?.({ queryKey: queryKeys.organizationsCount('organization-id') } as never)).toBe(true);
69+
});
4770
});
4871

4972
describe('schedulePersistentEventDeleteReconciliation', () => {
@@ -62,13 +85,23 @@ describe('schedulePersistentEventDeleteReconciliation', () => {
6285

6386
await vi.advanceTimersByTimeAsync(PERSISTENT_EVENT_DELETE_RECONCILE_DELAY);
6487

65-
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: queryKeys.type });
88+
expect(invalidateSpy).toHaveBeenCalledWith({
89+
predicate: expect.any(Function),
90+
queryKey: queryKeys.type
91+
});
6692
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: stackQueryKeys.type });
6793
expect(reconcileListener).toHaveBeenCalledOnce();
6894

6995
await vi.advanceTimersByTimeAsync(PERSISTENT_EVENT_DELETE_RECONCILE_RETRY_DELAY - PERSISTENT_EVENT_DELETE_RECONCILE_DELAY);
7096

7197
expect(reconcileListener).toHaveBeenCalledTimes(2);
7298
expect(invalidateSpy).toHaveBeenCalledTimes(5);
99+
100+
const persistentEventInvalidations = invalidateSpy.mock.calls.flatMap(([filters]) => (filters?.queryKey === queryKeys.type ? [filters] : []));
101+
expect(persistentEventInvalidations).toHaveLength(2);
102+
persistentEventInvalidations.forEach((filters) => {
103+
expect(filters.predicate?.({ queryKey: queryKeys.organizationsEvents('organization-id') } as never)).toBe(false);
104+
expect(filters.predicate?.({ queryKey: queryKeys.organizationsCount('organization-id') } as never)).toBe(true);
105+
});
73106
});
74107
});

src/Exceptionless.Web/ClientApp/src/lib/features/shared/api/fetch-client-response.test.ts

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

src/Exceptionless.Web/ClientApp/src/lib/features/shared/api/fetch-client-response.ts

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

0 commit comments

Comments
 (0)