Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Exceptionless.Core/Models/SavedViewColumnSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

namespace Exceptionless.Core.Models;

Expand All @@ -18,6 +19,10 @@ public sealed record SavedViewColumnSettings
/// <summary>Whether the column fills the table's remaining width. Null or false means use fixed-width behavior.</summary>
public bool? AutoFill { get; set; }

/// <summary>Whether cell content wraps onto multiple lines. Null or false means keep content on one line.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Wrap { get; set; }

/// <summary>Zero-based display position. Null means use the table default order.</summary>
[Range(0, MaxPosition)]
public int? Position { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ test('event list and detail effects stay bounded through paging and background c
await expect(rowSelection).toBeChecked();

const response = page.waitForResponse((candidate) => isEventListResponse(candidate, e2eScenario.organizationId));
await page.getByTitle('Return to the first page to refresh results').click();
await page.getByTitle('Refresh results').click();
expect((await response).ok()).toBe(true);
await expect(rowSelection).not.toBeChecked();
});
Expand Down Expand Up @@ -263,7 +263,7 @@ async function clickAndWaitForPage(
await page.getByRole('button', { name: buttonName }).click();
await expect(
page
.getByText(new RegExp(`^Page ${expectedPage} of`))
.getByLabel(new RegExp(`^Page ${expectedPage} of`))
.filter({ visible: true })
.first()
).toBeVisible();
Expand Down
314 changes: 314 additions & 0 deletions src/Exceptionless.Web/ClientApp/e2e/tests/rataplan-feedback.e2e.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ test('stack effects stay bounded through background, paging, and navigation chao
await expect(rowSelection).toBeChecked();

const response = page.waitForResponse((candidate) => isStackListResponse(candidate, e2eScenario.organizationId));
await page.getByTitle('Return to the first page to refresh results').click();
await page.getByTitle('Refresh results').click();
expect((await response).ok()).toBe(true);
await expect(rowSelection).not.toBeChecked();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@
<DataTable.Empty {table} />
{/if}
</DataTable.Body>
<DataTable.Footer {table} class="space-x-6 lg:space-x-8">
<DataTable.PageSize bind:value={limit} {table} />
<div class="flex items-center space-x-6 lg:space-x-8">
<DataTable.PageCount {table} />
<DataTable.Pagination {table} />
</div>
<DataTable.Footer {table} class="w-full">
<DataTable.Pager bind:value={limit} {table} />
</DataTable.Footer>
</DataTable.Root>
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@
<DataTable.Empty {table} />
{/if}
</DataTable.Body>
<DataTable.Footer {table} class="space-x-6 lg:space-x-8">
<DataTable.PageSize bind:value={limit} {table} />
<div class="flex items-center space-x-6 lg:space-x-8">
<DataTable.PageCount {table} />
<DataTable.Pagination {table} />
</div>
<DataTable.Footer {table} class="w-full">
<DataTable.Pager bind:value={limit} {table} />
</DataTable.Footer>
</DataTable.Root>
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@
<DataTable.Empty {table} />
{/if}
</DataTable.Body>
<DataTable.Footer {table} class="space-x-6 lg:space-x-8">
<DataTable.PageSize bind:value={limit} {table} />
<div class="flex items-center space-x-6 lg:space-x-8">
<DataTable.PageCount {table} />
<DataTable.Pagination {table} />
</div>
<DataTable.Footer {table} class="w-full">
<DataTable.Pager bind:value={limit} {table} />
</DataTable.Footer>
</DataTable.Root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function shouldResetActiveEventTab(eventLoaded: boolean, projectPending: boolean, tabs: readonly string[], activeTab: string): boolean {
return eventLoaded && !projectPending && !tabs.includes(activeTab);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import type { PersistentEvent } from '../models/index';

import { getSessionId } from '../utils';
import { shouldResetActiveEventTab } from './events-overview-tab-state';
import Environment from './views/environment.svelte';
import Error from './views/error.svelte';
import ExtendedData from './views/extended-data.svelte';
Expand Down Expand Up @@ -162,6 +163,12 @@
let notifiedEventId = $state('');
let showJsonDialog = $state(false);

$effect(() => {
if (shouldResetActiveEventTab(!!event, projectQuery.isPending, tabs, activeTab)) {
activeTab = 'Overview';
Comment thread
ejsmith marked this conversation as resolved.
}
});

function isPromotedTab(tab: TabType): boolean {
return !!projectQuery.data?.promoted_tabs?.includes(tab);
}
Expand Down Expand Up @@ -390,7 +397,7 @@
</Table.Root>

{#if event}
<Tabs.Root class="mt-4 mb-4" value={activeTab}>
<Tabs.Root class="mt-4 mb-4" bind:value={activeTab}>
Comment thread
ejsmith marked this conversation as resolved.
<div class="relative">
{#if canScrollTabsLeft}
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';

import { shouldResetActiveEventTab } from './events-overview-tab-state';

describe('event overview tab state', () => {
it('waits for project metadata before deciding whether a promoted tab is unavailable', () => {
const activeTab = 'Customer Context';

expect(shouldResetActiveEventTab(true, true, ['Overview', 'Exception'], activeTab)).toBe(false);
expect(shouldResetActiveEventTab(true, false, ['Overview', 'Exception', activeTab], activeTab)).toBe(false);
expect(shouldResetActiveEventTab(true, false, ['Overview', 'Exception'], activeTab)).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@
let { onTagClick, tags }: Props = $props();
</script>

<TagList class="max-w-48 flex-nowrap" maxVisible={2} {onTagClick} {tags} />
<TagList
class="max-w-48 flex-nowrap group-data-[wrap=true]/wrapped:w-full group-data-[wrap=true]/wrapped:max-w-none group-data-[wrap=true]/wrapped:flex-wrap"
maxVisible={2}
wrappedMaxVisible={6}
{onTagClick}
{tags}
/>
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { describe, expect, it } from 'vitest';
import EventTagsSummaryCell from './event-tags-summary-cell.svelte';

describe('EventTagsSummaryCell', () => {
it('shows two tags and summarizes the remaining tags', () => {
it('keeps compact mode to two tags and reveals more when wrapping', () => {
render(EventTagsSummaryCell, { tags: ['api', 'production', 'critical', 'customer'] });

expect(screen.getByText('api')).toBeTruthy();
expect(screen.getByText('production')).toBeTruthy();
expect(screen.getByText('+2')).toBeTruthy();
expect(screen.queryByText('critical')).toBeNull();
expect(screen.getByText('+2').closest<HTMLElement>('[data-slot="tooltip-trigger"]')?.classList).toContain('group-data-[wrap=true]/wrapped:hidden');

const thirdTagTrigger = screen.getByText('critical').closest<HTMLElement>('[data-slot="tooltip-trigger"]');
expect(thirdTagTrigger?.classList).toContain('hidden');
expect(thirdTagTrigger?.classList).toContain('group-data-[wrap=true]/wrapped:inline-flex');
expect(screen.getByLabelText('Tags: api, production, critical, customer').getAttribute('title')).toBeNull();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.GroupHeading>Bulk Actions</DropdownMenu.GroupHeading>
<DropdownMenu.Item onclick={() => (openRemoveEventDialog = true)} class="text-destructive" title="Delete event">Delete</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/svelte';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mutateAsync = vi.hoisted(() => vi.fn());
const deleteEvent = vi.hoisted(() => vi.fn(() => ({ mutateAsync })));
Expand All @@ -17,6 +17,25 @@ describe('EventsBulkActionsDropdownMenu', () => {
toast.success.mockClear();
});

afterEach(async () => {
cleanup();
// Bits UI defers body-scroll restoration by 24 ms after an overlay unmounts.
await new Promise((resolve) => window.setTimeout(resolve, 30));
});

it('does not repeat the trigger label inside the menu', async () => {
const table = {
getSelectedRowModel: () => ({ flatRows: [{ id: 'event-id' }] }),
resetRowSelection: vi.fn()
} as never;
render(EventsBulkActionsDropdownMenu, { props: { table } });

await fireEvent.click(screen.getByRole('button', { name: /Bulk Actions/ }));

expect(document.querySelector('[data-slot="dropdown-menu-group-heading"]')).toBeNull();
expect(screen.getByRole('menuitem', { name: 'Delete' })).toBeTruthy();
});

it('deletes the selected events and clears the selection', async () => {
// Arrange
const resetRowSelection = vi.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
rowHref?: (row: EventSummaryModel<SummaryTemplateKeys>) => string;
table: Table<StockFeatures, EventSummaryModel<SummaryTemplateKeys>>;
toolbarChildren?: Snippet;
wrappedColumnIds?: readonly string[];
}

let {
Expand All @@ -30,7 +31,8 @@
rowClick,
rowHref,
table,
toolbarChildren
toolbarChildren,
wrappedColumnIds = []
}: Props = $props();
</script>

Expand All @@ -40,7 +42,7 @@
{@render toolbarChildren()}
</DataTable.Toolbar>
{/if}
<DataTable.Body {autoFillColumnId} {onAutoFillColumnResized} {rowClick} {rowHref} {table}>
<DataTable.Body {autoFillColumnId} {onAutoFillColumnResized} {rowClick} {rowHref} {table} {wrappedColumnIds}>
{#if isLoading}
<DelayedRender>
<DataTable.Loading {table} />
Expand All @@ -56,20 +58,8 @@
{#if footerChildren}
{@render footerChildren()}
{:else}
<div class="grid w-full grid-cols-1 items-center gap-2 sm:grid-cols-3">
<div class="flex min-w-0 items-center gap-2">
<DataTable.Selection {table} />
</div>

<div class="flex min-w-0 items-center justify-center">
<DataTable.PageCount {table} />
</div>

<div class="flex min-w-0 items-center justify-end gap-4">
<DataTable.PageSize bind:value={limit} {table} />
<DataTable.Pagination {table} />
</div>
</div>
<DataTable.Selection {table} />
<DataTable.Pager bind:value={limit} {table} />
{/if}
</DataTable.Footer>
</DataTable.Root>
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { supportsColumnWrapping } from '$features/shared/components/data-table/column-meta';
import { describe, expect, it } from 'vitest';

import type { EventSummaryModel, StackSummaryModel, SummaryTemplateKeys } from '../summary';
Expand All @@ -22,7 +23,9 @@ describe('event table columns', () => {
const summary = columns.find((column) => column.id === 'summary');

expect(summary).toMatchObject({ enableResizing: true, maxSize: 1200, minSize: 240, size: 480 });
expect(supportsColumnWrapping(summary?.meta)).toBe(true);
expect(project).toMatchObject({ maxSize: 800, minSize: 160, size: 240 });
expect(supportsColumnWrapping(project?.meta)).toBe(false);
expect(select?.enableResizing).toBe(false);
});

Expand All @@ -35,4 +38,18 @@ describe('event table columns', () => {
expect(defaultStackColumnVisibility.project).toBe(false);
expect(defaultStackColumnVisibility.tags).toBe(false);
});

it('allows wrapping only for summary, tags, and message event columns', () => {
const columns = getColumns<EventSummaryModel<SummaryTemplateKeys>>();
const wrappableColumnIds = columns.filter((column) => supportsColumnWrapping(column.meta)).map((column) => column.id);

expect(wrappableColumnIds).toEqual(['summary', 'tags', 'message']);
});

it('allows wrapping only for summary and tags stack columns', () => {
const columns = getColumns<StackSummaryModel<SummaryTemplateKeys>>('stack_frequent');
const wrappableColumnIds = columns.filter((column) => supportsColumnWrapping(column.meta)).map((column) => column.id);

expect(wrappableColumnIds).toEqual(['summary', 'tags']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKey
id: 'summary',
maxSize: 1200,
meta: {
class: 'w-full'
class: 'w-full',
enableWrapping: true
},
minSize: 240,
size: 480
Expand Down Expand Up @@ -149,7 +150,8 @@ export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKey
id: 'tags',
maxSize: 800,
meta: {
class: 'w-52 min-w-52 max-w-52'
class: 'w-52 min-w-52 max-w-52',
enableWrapping: true
},
minSize: 120,
size: 208
Expand All @@ -162,7 +164,8 @@ export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKey
id: 'message',
maxSize: 800,
meta: {
class: 'w-full'
class: 'w-full',
enableWrapping: true
},
minSize: 160,
size: 320
Expand Down Expand Up @@ -259,7 +262,8 @@ export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKey
id: 'tags',
maxSize: 800,
meta: {
class: 'w-52 min-w-52 max-w-52'
class: 'w-52 min-w-52 max-w-52',
enableWrapping: true
},
minSize: 120,
size: 208
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@
<CopyToClipboardButton size="icon-sm" title="Copy Stack Trace to Clipboard" value={stackTrace} variant="outline"></CopyToClipboardButton>
</div>
</div>
<div class="mt-2 max-h-75 grow overflow-auto text-xs">
<div class="mt-2 grow text-xs">
{#if event.data?.['@error']}
<StackTrace error={event.data['@error']} />
{:else if event.data?.['@simple_error']}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import type { StringValueFromBody, WorkInProgressResult } from '$features/shared
import type { WebSocketMessageValue } from '$features/websockets/models';

import { accessToken } from '$features/auth/index.svelte';
import { queryKeys as eventQueryKeys } from '$features/events/api.svelte';
import { fetchApiJson } from '$features/shared/api/api.svelte';
import { queryKeys as stackQueryKeys } from '$features/stacks/api.svelte';
import { type FetchClientResponse, type ProblemDetails, useFetchClient } from '@foundatiofx/fetchclient';
import { createMutation, createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query';

Expand All @@ -30,6 +32,19 @@ export async function invalidateProjectQueries(queryClient: QueryClient, message
queryKey: queryKeys.projects()
});
}

await invalidateProjectSummaryQueries(queryClient);
}

export async function invalidateProjectSummaryQueries(queryClient: QueryClient): Promise<void> {
await Promise.all([
queryClient.invalidateQueries({
queryKey: eventQueryKeys.type
}),
queryClient.invalidateQueries({
queryKey: stackQueryKeys.type
})
]);
}

// TODO: Do we need to scope these all by organization?
Expand Down Expand Up @@ -717,8 +732,17 @@ export function updateProject(request: UpdateProjectRequest) {
queryKey: queryKeys.id(request.route.id)
});
},
onSuccess: (project: ViewProject) => {
onSuccess: async (project: ViewProject) => {
queryClient.setQueryData(queryKeys.id(request.route.id), project);
await Promise.all([
queryClient.invalidateQueries({
queryKey: queryKeys.organization(project.organization_id)
}),
queryClient.invalidateQueries({
queryKey: queryKeys.projects()
}),
invalidateProjectSummaryQueries(queryClient)
]);
}
}));
}
Loading
Loading