diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.test.ts new file mode 100644 index 0000000000..ba27356aa4 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.test.ts @@ -0,0 +1,26 @@ +import type { EventSummaryModel, StackSummaryModel, SummaryTemplateKeys } from '$features/events/components/summary'; + +import { describe, expect, it } from 'vitest'; + +import { getColumns } from './options.svelte'; + +describe('getColumns', () => { + it('uses dedicated stack-mode controls instead of API sort parameters', () => { + const result = getColumns>('stack_frequent'); + const columnsById = Object.fromEntries(result.map((column) => [column.id, column])); + + expect(columnsById.events?.enableSorting).toBe(false); + expect(columnsById.first?.enableSorting).toBe(false); + expect(columnsById.last?.enableSorting).toBe(false); + expect(columnsById.events?.header).toBeTypeOf('function'); + expect(columnsById.first?.header).toBeTypeOf('function'); + expect(columnsById.last?.header).toBeTypeOf('function'); + }); + + it('keeps summary message column unsortable', () => { + const result = getColumns>('summary'); + const columnsById = Object.fromEntries(result.map((column) => [column.id, column])); + + expect(columnsById.message?.enableSorting).toBe(false); + }); +}); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts index a1d39b5f98..ef16cebf1b 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts @@ -12,6 +12,7 @@ import type { EventSummaryModel, StackSummaryModel, SummaryModel, SummaryTemplat import LogLevel from '../log-level.svelte'; import Summary from '../summary/summary.svelte'; import EventsUserIdentitySummaryCell from './events-user-identity-summary-cell.svelte'; +import StackSortHeader from './stack-sort-header.svelte'; import StackStatusCell from './stack-status-cell.svelte'; import StackUsersSummaryCell from './stack-users-summary-cell.svelte'; @@ -25,9 +26,11 @@ export const defaultEventColumnVisibility: ColumnVisibilityState = { version: false }; +export type StackSortMode = Extract; + export function getColumns>( mode: GetEventsMode = 'summary', - options?: { showType?: boolean } + options?: { onStackSort?: (mode: StackSortMode) => void; showType?: boolean } ): ColumnDef[] { const showType = options?.showType ?? true; const columns: ColumnDef[] = [ @@ -176,7 +179,12 @@ export function getColumns>('total'), cell: (prop) => renderComponent(NumberFormatter, { value: prop.getValue() }), enableSorting: false, - header: 'Events', + header: () => + renderComponent(StackSortHeader, { + active: mode === 'stack_frequent', + label: 'Events', + onclick: () => options?.onStackSort?.('stack_frequent') + }), id: 'events', meta: { class: 'w-24' @@ -186,7 +194,12 @@ export function getColumns>('first_occurrence'), cell: (prop) => renderComponent(TimeAgo, { value: prop.getValue() }), enableSorting: false, - header: 'First', + header: () => + renderComponent(StackSortHeader, { + active: mode === 'stack_new', + label: 'First', + onclick: () => options?.onStackSort?.('stack_new') + }), id: 'first', meta: { class: 'w-36' @@ -196,7 +209,12 @@ export function getColumns>('last_occurrence'), cell: (prop) => renderComponent(TimeAgo, { value: prop.getValue() }), enableSorting: false, - header: 'Last', + header: () => + renderComponent(StackSortHeader, { + active: mode === 'stack_recent', + label: 'Last', + onclick: () => options?.onStackSort?.('stack_recent') + }), id: 'last', meta: { class: 'w-36' diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/stack-sort-header.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/stack-sort-header.svelte new file mode 100644 index 0000000000..0c5413885b --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/stack-sort-header.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/stack-sort-header.svelte.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/stack-sort-header.svelte.test.ts new file mode 100644 index 0000000000..0e29592915 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/stack-sort-header.svelte.test.ts @@ -0,0 +1,24 @@ +import { fireEvent, render, screen } from '@testing-library/svelte'; +import { describe, expect, it, vi } from 'vitest'; + +import StackSortHeader from './stack-sort-header.svelte'; + +describe('StackSortHeader', () => { + it('exposes the active descending sort and handles selection', async () => { + const onclick = vi.fn(); + render(StackSortHeader, { active: true, label: 'Events', onclick }); + + const button = screen.getByRole('button', { name: 'Sort by Events descending' }); + expect(button.getAttribute('aria-pressed')).toBe('true'); + + await fireEvent.click(button); + + expect(onclick).toHaveBeenCalledOnce(); + }); + + it('does not mark inactive sort modes as selected', () => { + render(StackSortHeader, { active: false, label: 'First', onclick: vi.fn() }); + + expect(screen.getByRole('button', { name: 'Sort by First descending' }).getAttribute('aria-pressed')).toBe('false'); + }); +}); diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte index 4900d209a5..851a09bc46 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stack/+page.svelte @@ -619,6 +619,10 @@ columnPersistenceKey: 'stacks-column-visibility', get columns() { return getColumns>(eventsQueryParameters.mode, { + onStackSort: (mode) => { + eventsQueryParameters.mode = mode; + table.setPageIndex(0); + }, showType: !hasSingleTypeFilter(eventsQueryParameters.filter) }); },