Skip to content
Open
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
50 changes: 50 additions & 0 deletions src/Exceptionless.Web/ClientApp/e2e/tests/saved-views.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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, '\\$&');
}
Expand All @@ -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<void> {
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 });
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
let isRenameDialogOpen = $state(false);
let isDeleteDialogOpen = $state(false);
let isColumnDialogOpen = $state(false);
let isMenuOpen = $state(false);
let viewToDelete = $state<null | SavedView>(null);

const organizationId = $derived(organization.current);
Expand Down Expand Up @@ -166,6 +167,11 @@
isDeleteDialogOpen = true;
}

function handleResetToSaved(): void {
isMenuOpen = false;
onResetToSaved();
}

async function handleSave(name: string, slug: string, isPrivate: boolean) {
if (!organizationId) {
return;
Expand Down Expand Up @@ -269,7 +275,7 @@
}
</script>

<DropdownMenu.Root>
<DropdownMenu.Root bind:open={isMenuOpen}>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} class="relative gap-x-1.5 px-3" size="lg" variant="outline" title="Manage View Settings">
Expand Down Expand Up @@ -299,7 +305,7 @@
<Pencil class="mr-2 size-4" aria-hidden="true" />
Rename
</DropdownMenu.Item>
<DropdownMenu.Item disabled={!isModified} onclick={onResetToSaved}>
<DropdownMenu.Item disabled={!isModified} onclick={handleResetToSaved}>
<Undo2 class="mr-2 size-4" aria-hidden="true" />
Reset to Saved
</DropdownMenu.Item>
Expand Down
112 changes: 61 additions & 51 deletions src/Exceptionless.Web/ClientApp/src/routes/(app)/event/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@

import {
ALL_TIME_QUERY_VALUE,
clearListFilterQueryParams,
deserializeTimeQueryParam,
getEventsNavigationOptionsForFilter,
getListFilterQueryParams,
type ListFilterQueryParams,
redirectToEventsWithFilter,
serializeTimeQueryParam
} from '../redirect-to-events.svelte';
Expand Down Expand Up @@ -115,13 +118,13 @@
return buildFilterCacheKey(organization.current, page.url.pathname, filter);
}

function getQueryTime(): null | string {
if (queryParams.time != null) {
if (queryParams.time === ALL_TIME_QUERY_VALUE) {
function getQueryTime(params: ListFilterQueryParams = queryParams): null | string {
if (params.time != null) {
if (params.time === ALL_TIME_QUERY_VALUE) {
return null;
}

return queryParams.time ? deserializeTimeQueryParam(queryParams.time) : null;
return params.time ? deserializeTimeQueryParam(params.time) : null;
}

return savedViewsState.activeSavedView?.time ?? DEFAULT_TIME_RANGE;
Expand All @@ -132,53 +135,53 @@
return filter || null;
}

function getQueryFilters(): FacetedFilter.IFilter[] | null {
function getQueryFilters(params: ListFilterQueryParams = queryParams): FacetedFilter.IFilter[] | null {
const filters: FacetedFilter.IFilter[] = [];

if (queryParams.project) {
filters.push(new ProjectFilter(splitQueryParam(queryParams.project)));
if (params.project) {
filters.push(new ProjectFilter(splitQueryParam(params.project)));
}

if (queryParams.stack) {
filters.push(new StringFilter('stack', queryParams.stack));
if (params.stack) {
filters.push(new StringFilter('stack', params.stack));
}

const bot = parseBooleanQueryParam(queryParams.bot);
const bot = parseBooleanQueryParam(params.bot);
if (bot !== undefined) {
filters.push(new BooleanFilter('bot', bot));
}

const first = parseBooleanQueryParam(queryParams.first);
const first = parseBooleanQueryParam(params.first);
if (first !== undefined) {
filters.push(new BooleanFilter('first', first));
}

if (queryParams.level) {
filters.push(new LevelFilter(splitQueryParam(queryParams.level) as never[]));
if (params.level) {
filters.push(new LevelFilter(splitQueryParam(params.level) as never[]));
}

if (queryParams.reference) {
filters.push(new ReferenceFilter(queryParams.reference));
if (params.reference) {
filters.push(new ReferenceFilter(params.reference));
}

if (queryParams.session) {
filters.push(new SessionFilter(queryParams.session));
if (params.session) {
filters.push(new SessionFilter(params.session));
}

if (queryParams.status) {
filters.push(new StatusFilter(splitQueryParam(queryParams.status) as never[]));
if (params.status) {
filters.push(new StatusFilter(splitQueryParam(params.status) as never[]));
}

if (queryParams.tag) {
filters.push(new TagFilter(splitQueryParam(queryParams.tag) as never[]));
if (params.tag) {
filters.push(new TagFilter(splitQueryParam(params.tag) as never[]));
}

if (queryParams.type) {
filters.push(new TypeFilter(splitQueryParam(queryParams.type) as never[]));
if (params.type) {
filters.push(new TypeFilter(splitQueryParam(params.type) as never[]));
}

if (queryParams.version) {
filters.push(new VersionFilter('version', queryParams.version));
if (params.version) {
filters.push(new VersionFilter('version', params.version));
}

return filters.length > 0 ? filters : null;
Expand Down Expand Up @@ -292,23 +295,21 @@
{ lazy: true }
);

function getCurrentFilters(): FacetedFilter.IFilter[] {
return applyTimeFilter(getCurrentFiltersWithoutTime(), getQueryTime());
function getCurrentFilters(params: ListFilterQueryParams = queryParams): FacetedFilter.IFilter[] {
return applyTimeFilter(getCurrentFiltersWithoutTime(params), getQueryTime(params));
}

function getCurrentFiltersWithoutTime(): FacetedFilter.IFilter[] {
function getCurrentFiltersWithoutTime(params: ListFilterQueryParams = queryParams): FacetedFilter.IFilter[] {
const savedViewFilters = getSavedViewFilters();
const queryFilters = getQueryFilters() ?? [];
const queryFilters = getQueryFilters(params) ?? [];
const expressionFilters =
queryParams.filter != null
? getFiltersFromCache(filterCacheKey(queryParams.filter), queryParams.filter).filter((filter) => filter.type !== 'date')
: [];
params.filter != null ? getFiltersFromCache(filterCacheKey(params.filter), params.filter).filter((filter) => filter.type !== 'date') : [];

if (savedViewFilters) {
return mergeFilterOverrides(
savedViewFilters.filter((filter) => filter.type !== 'date'),
[...expressionFilters, ...queryFilters],
getQueryFilterRemovalKeys(savedViewFilters)
getQueryFilterRemovalKeys(savedViewFilters, params)
);
}

Expand All @@ -329,54 +330,54 @@
return deserializeFilters(savedView.filter_definitions);
}

function getQueryFilterRemovalKeys(savedViewFilters: FacetedFilter.IFilter[]): string[] {
function getQueryFilterRemovalKeys(savedViewFilters: FacetedFilter.IFilter[], params: ListFilterQueryParams = queryParams): string[] {
const removedKeys: string[] = [];

if (queryParams.bot === '') {
if (params.bot === '') {
removedKeys.push('boolean-bot');
}

if (queryParams.first === '') {
if (params.first === '') {
removedKeys.push('boolean-first');
}

if (queryParams.filter === '') {
if (params.filter === '') {
removedKeys.push(...savedViewFilters.filter((filter) => filter.type !== 'date' && !isQueryParamFilter(filter)).map((filter) => filter.key));
}

if (queryParams.level === '') {
if (params.level === '') {
removedKeys.push('level');
}

if (queryParams.project === '') {
if (params.project === '') {
removedKeys.push('project');
}

if (queryParams.reference === '') {
if (params.reference === '') {
removedKeys.push('reference');
}

if (queryParams.session === '') {
if (params.session === '') {
removedKeys.push('session');
}

if (queryParams.stack === '') {
if (params.stack === '') {
removedKeys.push('string-stack');
}

if (queryParams.status === '') {
if (params.status === '') {
removedKeys.push('status');
}

if (queryParams.tag === '') {
if (params.tag === '') {
removedKeys.push('tag');
}

if (queryParams.type === '') {
if (params.type === '') {
removedKeys.push('type');
}

if (queryParams.version === '') {
if (params.version === '') {
removedKeys.push('version-version');
}

Expand All @@ -400,17 +401,26 @@
let isInternalFilterUpdate = false;
watch(
[() => page.url.pathname, () => page.url.search, () => savedViewsState.activeSavedView],
() => {
if (isInternalFilterUpdate) {
([pathname, , activeSavedView], [previousPathname, , previousSavedView]) => {
const savedViewChanged = pathname !== previousPathname || activeSavedView?.id !== previousSavedView?.id;
if (isInternalFilterUpdate && !savedViewChanged) {
isInternalFilterUpdate = false;
return;
}

filters = getCurrentFilters();
isInternalFilterUpdate = false;
filters = getCurrentFilters(getListFilterQueryParams(page.url.searchParams));
},
{ lazy: true }
);

function handleResetToSaved(): void {
isInternalFilterUpdate = false;
clearListFilterQueryParams(queryParams);
savedViewsState.handleResetToSaved();
filters = getCurrentFilters();
}

async function onFilterChanged(addedOrUpdated: FacetedFilter.IFilter): Promise<void> {
const navigationOptions = getEventsNavigationOptionsForFilter(addedOrUpdated);
if (navigationOptions) {
Expand Down Expand Up @@ -507,7 +517,7 @@
}

untrack(() => {
updateFilters(getCurrentFilters(), { clearPagination: false });
updateFilters(getCurrentFilters(getListFilterQueryParams(page.url.searchParams)), { clearPagination: false });
});
});

Expand Down Expand Up @@ -839,7 +849,7 @@
isModified={savedViewsState.isModified}
onLoadView={savedViewsState.handleLoadView}
onClearSavedView={savedViewsState.handleClearSavedView}
onResetToSaved={savedViewsState.handleResetToSaved}
onResetToSaved={handleResetToSaved}
savedViews={savedViewsState.savedViews}
{showChart}
{showStats}
Expand Down
Loading
Loading