diff --git a/src/Exceptionless.Web/ClientApp/e2e/tests/saved-views.e2e.ts b/src/Exceptionless.Web/ClientApp/e2e/tests/saved-views.e2e.ts index 761e501b6d..302447fec4 100644 --- a/src/Exceptionless.Web/ClientApp/e2e/tests/saved-views.e2e.ts +++ b/src/Exceptionless.Web/ClientApp/e2e/tests/saved-views.e2e.ts @@ -50,6 +50,16 @@ test('events saved view can be saved, renamed, loaded, and deleted', async ({ e2 await expect(getVisibleText(page, journey.message)).toBeVisible(); }); + await test.step('reset route-specific filter overrides to the saved view', async () => { + await page.goto(`/next/event/${viewSlug}?project=${e2eScenario.projectId}`); + await openViewMenu(page); + await page.getByRole('menuitem', { name: 'Reset to Saved' }).click(); + + await expect(page.getByRole('menu')).toBeHidden(); + await expect(page).not.toHaveURL(/[?&]project=/); + await expect(getVisibleText(page, journey.message)).toBeVisible(); + }); + await test.step('delete the saved view and return to the default Events view', async () => { await openViewMenu(page); await page.getByRole('menuitem', { name: `Delete "${renamedViewName}"` }).click(); @@ -65,6 +75,35 @@ test('events saved view can be saved, renamed, loaded, and deleted', async ({ e2 }); }); +test('switching saved views discards temporary filter overrides', async ({ e2eApi, e2eScenario, page }) => { + const journey = ExceptionlessE2EJourney.fromScenario(page, e2eApi, e2eScenario); + const suffix = journey.run.slice(-28); + const firstViewName = `E2E First View ${suffix}`; + const secondViewName = `E2E Second View ${suffix}`; + const firstViewSlug = savedViewSlug(firstViewName); + const secondViewSlug = savedViewSlug(secondViewName); + + await journey.submitRepresentativeEvent(); + await saveView(page, firstViewName, journey.referenceId, '15m'); + await saveView(page, secondViewName, journey.referenceId, '1d'); + + await page.goto(`/next/event/${firstViewSlug}`); + const dateFilter = page.getByRole('button', { name: /^Date/ }).filter({ visible: true }).first(); + await dateFilter.click(); + await page.getByRole('button', { name: 'Last 90 days' }).click(); + await expect(page).toHaveURL(/[?&]time=90d(?:&|$)/); + + await page.getByRole('link', { exact: true, name: secondViewName }).first().click(); + await expect(page).toHaveURL(new RegExp(`/next/event/${escapeRegExp(secondViewSlug)}(?:[?#]|$)`)); + await expect(page.getByRole('heading', { name: secondViewName })).toBeVisible(); + await expect( + page + .getByRole('button', { name: /Date\s+Last 24 hours/ }) + .filter({ visible: true }) + .first() + ).toBeVisible(); +}); + function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -81,3 +120,14 @@ function savedViewSlug(value: string): string { .replace(/^-+|-+$/g, '') .replace(/-+/g, '-'); } + +async function saveView(page: Page, viewName: string, referenceId: string, time: string): Promise { + await page.goto(`/next/event?reference=${encodeURIComponent(referenceId)}&time=${time}`); + await openViewMenu(page); + await page.getByRole('menuitem', { name: 'Save As...' }).click(); + const dialog = page.getByRole('dialog', { name: 'Save View' }); + await dialog.getByLabel('Name', { exact: true }).fill(viewName); + await dialog.getByRole('button', { name: 'Save' }).click(); + await expect(dialog).toBeHidden({ timeout: 30_000 }); + await expect(page.getByRole('heading', { name: viewName })).toBeVisible({ timeout: 30_000 }); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/components/saved-view-picker.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/components/saved-view-picker.svelte index 4dd1fc9e0b..1fee7ccc46 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/components/saved-view-picker.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/saved-views/components/saved-view-picker.svelte @@ -86,6 +86,7 @@ let isRenameDialogOpen = $state(false); let isDeleteDialogOpen = $state(false); let isColumnDialogOpen = $state(false); + let isMenuOpen = $state(false); let viewToDelete = $state(null); const organizationId = $derived(organization.current); @@ -166,6 +167,11 @@ isDeleteDialogOpen = true; } + function handleResetToSaved(): void { + isMenuOpen = false; + onResetToSaved(); + } + async function handleSave(name: string, slug: string, isPrivate: boolean) { if (!organizationId) { return; @@ -269,7 +275,7 @@ } - + {#snippet child({ props })}