Skip to content

Commit 53f00d2

Browse files
authored
Next: Organization switcher and filter improvements. (#1821)
* Updated dependencies * Filter updates * Added ability to switch organizations. * Fixed filter builder * Reset page filters on org change * Fixed ci
1 parent bca1f40 commit 53f00d2

26 files changed

+747
-745
lines changed

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

Lines changed: 279 additions & 277 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: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,32 @@
2424
},
2525
"devDependencies": {
2626
"@iconify-json/lucide": "^1.2.25",
27-
"@playwright/test": "^1.50.0",
27+
"@playwright/test": "^1.50.1",
2828
"@sveltejs/adapter-static": "^3.0.8",
29-
"@sveltejs/kit": "^2.16.1",
29+
"@sveltejs/kit": "^2.17.1",
3030
"@sveltejs/vite-plugin-svelte": "^5.0.3",
3131
"@types/eslint": "^9.6.1",
32-
"@types/node": "^22.12.0",
32+
"@types/node": "^22.13.1",
3333
"@types/throttle-debounce": "^5.0.2",
3434
"autoprefixer": "^10.4.20",
3535
"cross-env": "^7.0.3",
36-
"eslint": "^9.19.0",
36+
"eslint": "^9.20.0",
3737
"eslint-config-prettier": "^10.0.1",
38-
"eslint-plugin-perfectionist": "^4.7.0",
38+
"eslint-plugin-perfectionist": "^4.8.0",
3939
"eslint-plugin-svelte": "^2.46.1",
4040
"npm-run-all": "^4.1.5",
4141
"postcss": "^8.5.1",
4242
"prettier": "^3.4.2",
4343
"prettier-plugin-svelte": "^3.3.3",
4444
"prettier-plugin-tailwindcss": "^0.6.11",
45-
"svelte": "^5.19.6",
45+
"svelte": "^5.19.9",
4646
"svelte-check": "^4.1.4",
4747
"swagger-typescript-api": "^13.0.23",
4848
"tslib": "^2.8.1",
4949
"typescript": "^5.7.3",
50-
"typescript-eslint": "^8.22.0",
51-
"vite": "^6.0.11",
52-
"vitest": "3.0.4"
50+
"typescript-eslint": "^8.23.0",
51+
"vite": "^6.1.0",
52+
"vitest": "3.0.5"
5353
},
5454
"dependencies": {
5555
"@exceptionless/browser": "^3.1.0",
@@ -59,18 +59,18 @@
5959
"@tanstack/svelte-query-devtools": "https://pkg.pr.new/@tanstack/svelte-query-devtools@28f98f9",
6060
"@tanstack/svelte-table": "^9.0.0-alpha.10",
6161
"@typeschema/class-validator": "^0.3.0",
62-
"bits-ui": "^1.0.0-next.78",
62+
"bits-ui": "^1.0.0-next.89",
6363
"class-validator": "^0.14.1",
6464
"clsx": "^2.1.1",
6565
"formsnap": "^2.0.0",
66-
"lucide-svelte": "^0.474.0",
66+
"kit-query-params": "^0.0.26",
67+
"lucide-svelte": "^0.475.0",
6768
"mode-watcher": "^0.5.1",
6869
"oidc-client-ts": "^3.1.0",
6970
"pretty-ms": "^9.2.0",
7071
"runed": "^0.23.2",
7172
"svelte-sonner": "^0.3.28",
7273
"svelte-time": "^2.0.0",
73-
"sveltekit-search-params": "^4.0.0-next.0",
7474
"sveltekit-superforms": "^2.23.1",
7575
"tailwind-merge": "^2.6.0",
7676
"tailwind-variants": "^0.3.1",
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script lang="ts">
22
import { builderContext, type FacetFilterBuilder, type IFilter } from '$comp/faceted-filter';
3-
import { onMount } from 'svelte';
43
54
import BooleanFacetedFilter from './boolean-faceted-filter.svelte';
65
import { BooleanFilter } from './models.svelte';
@@ -13,15 +12,12 @@
1312
1413
const { priority = 0, term, title = 'Boolean' }: Props = $props();
1514
16-
onMount(() => {
17-
const builder: FacetFilterBuilder<BooleanFilter> = {
18-
component: BooleanFacetedFilter,
19-
create: (filter?: BooleanFilter) => filter ?? new BooleanFilter(term),
20-
priority,
21-
title
22-
};
15+
const builder: FacetFilterBuilder<BooleanFilter> = {
16+
component: BooleanFacetedFilter,
17+
create: (filter?: BooleanFilter) => filter ?? new BooleanFilter(term),
18+
priority,
19+
title
20+
};
2321
24-
builderContext.set(`boolean-${term}`, builder as unknown as FacetFilterBuilder<IFilter>);
25-
return () => builderContext.delete('boolean');
26-
});
22+
builderContext.set(`boolean-${term}`, builder as unknown as FacetFilterBuilder<IFilter>);
2723
</script>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script lang="ts">
22
import { builderContext, type FacetFilterBuilder, type IFilter } from '$comp/faceted-filter';
3-
import { onMount } from 'svelte';
43
54
import DateFacetedFilter from './date-faceted-filter.svelte';
65
import { DateFilter } from './models.svelte';
@@ -13,15 +12,12 @@
1312
1413
const { priority = 0, term, title = 'Date Range' }: Props = $props();
1514
16-
onMount(() => {
17-
const builder: FacetFilterBuilder<DateFilter> = {
18-
component: DateFacetedFilter,
19-
create: (filter?: DateFilter) => filter ?? new DateFilter(term),
20-
priority,
21-
title
22-
};
15+
const builder: FacetFilterBuilder<DateFilter> = {
16+
component: DateFacetedFilter,
17+
create: (filter?: DateFilter) => filter ?? new DateFilter(term),
18+
priority,
19+
title
20+
};
2321
24-
builderContext.set(`date-${term}`, builder as unknown as FacetFilterBuilder<IFilter>);
25-
return () => builderContext.delete('date');
26-
});
22+
builderContext.set(`date-${term}`, builder as unknown as FacetFilterBuilder<IFilter>);
2723
</script>

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters/helpers.ts

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,88 @@ import type { IFilter } from '$comp/faceted-filter';
22

33
import { organization } from '$features/organizations/context.svelte';
44

5-
import { getKeywordFilter, getProjectFilter, getStackFilter } from './models.svelte';
5+
import { DateFilter, KeywordFilter, type ProjectFilter, type StringFilter } from './models.svelte';
6+
7+
const filterCache = new Map<null | string, IFilter[]>();
8+
9+
export function applyDefaultDateFilter(filters: IFilter[], time: null | string): IFilter[] {
10+
if (!time) {
11+
return filters;
12+
}
13+
14+
const dateFilter = filters.find((f) => f.key === 'date-date');
15+
if (dateFilter) {
16+
return filters;
17+
}
18+
19+
return [...filters, new DateFilter('date', time)];
20+
}
21+
22+
export function clearFilterCache() {
23+
filterCache.clear();
24+
}
25+
26+
export function filterChanged(filters: IFilter[], addedOrUpdated: IFilter): IFilter[] {
27+
const index = filters.findIndex((f) => f.id === addedOrUpdated.id);
28+
if (index === -1) {
29+
return processFilterRules([...filters, addedOrUpdated]);
30+
}
31+
32+
return processFilterRules([...filters.slice(0, index), addedOrUpdated, ...filters.slice(index + 1)]);
33+
}
34+
35+
export function filterRemoved(filters: IFilter[], removed?: IFilter): IFilter[] {
36+
// If detail is undefined, remove all filters.
37+
if (!removed) {
38+
return [];
39+
}
40+
41+
return filters.filter((f) => f.id !== removed.id);
42+
}
43+
44+
export function getFiltersFromCache(filter: null | string): IFilter[] {
45+
if (!filter) {
46+
return [];
47+
}
48+
49+
if (filterCache.has(filter)) {
50+
return filterCache.get(filter) ?? [];
51+
}
52+
53+
// If filter is not in cache, return it in a new KeywordFilter instance.
54+
return [new KeywordFilter(filter)];
55+
}
56+
57+
export function getKeywordFilter(filters: IFilter[]): KeywordFilter | undefined {
58+
return filters.find((f) => f.type === 'keyword') as KeywordFilter;
59+
}
60+
61+
export function getProjectFilter(filters: IFilter[]): ProjectFilter {
62+
return filters.find((f) => f.type === 'project') as ProjectFilter;
63+
}
64+
65+
export function getStackFilter(filters: IFilter[]): StringFilter | undefined {
66+
return filters.find((f) => f.type === 'string') as StringFilter;
67+
}
68+
69+
export function quote(value?: null | string): string | undefined {
70+
return value ? `"${value}"` : undefined;
71+
}
72+
73+
export function quoteIfSpecialCharacters(value?: null | string): null | string | undefined {
74+
// Check for lucene special characters or whitespace
75+
const regex = new RegExp('\\+|\\-|\\&|\\||\\!|\\(|\\)|\\{|\\}|\\[|\\]|\\^|\\"|\\~|\\*|\\?|\\:|\\\\|\\/|\\s', 'g');
76+
77+
if (value && value.match(regex)) {
78+
return quote(value);
79+
}
80+
81+
return value;
82+
}
683

784
export function shouldRefreshPersistentEventChanged(
885
filters: IFilter[],
9-
filter: string,
86+
filter: null | string,
1087
organization_id?: string,
1188
project_id?: string,
1289
stack_id?: string,
@@ -19,7 +96,7 @@ export function shouldRefreshPersistentEventChanged(
1996
if (id) {
2097
// This could match any kind of lucene query (even must not filtering)
2198
const keywordFilter = getKeywordFilter(filters);
22-
if (keywordFilter && !keywordFilter.isEmpty()) {
99+
if (keywordFilter && keywordFilter.value) {
23100
if (keywordFilter.value!.includes(id)) {
24101
return true;
25102
}
@@ -28,14 +105,14 @@ export function shouldRefreshPersistentEventChanged(
28105

29106
if (stack_id) {
30107
const stackFilter = getStackFilter(filters);
31-
if (stackFilter && !stackFilter.isEmpty()) {
108+
if (stackFilter && stackFilter.value) {
32109
return stackFilter.value === stack_id;
33110
}
34111
}
35112

36113
if (project_id) {
37114
const projectFilter = getProjectFilter(filters);
38-
if (projectFilter && !projectFilter.isEmpty()) {
115+
if (projectFilter && projectFilter.value.length) {
39116
return projectFilter.value.includes(project_id);
40117
}
41118
}
@@ -46,3 +123,46 @@ export function shouldRefreshPersistentEventChanged(
46123

47124
return true;
48125
}
126+
127+
export function toFilter(filters: IFilter[]): string {
128+
return filters
129+
.map((f) => f.toFilter())
130+
.filter(Boolean)
131+
.join(' ')
132+
.trim();
133+
}
134+
135+
export function updateFilterCache(filter: null | string, filters: IFilter[]) {
136+
if (filter) {
137+
filterCache.set(filter, filters);
138+
}
139+
}
140+
141+
function processFilterRules(filters: IFilter[]): IFilter[] {
142+
// 1. There can only be one date filter by term at a time.
143+
// 2. There can only be one project filter.
144+
const uniqueFilters = new Map<string, IFilter>();
145+
for (const filter of filters) {
146+
if (filter.type === 'project' || filter.type === 'date') {
147+
const existingFilter = uniqueFilters.get(filter.key);
148+
if (existingFilter) {
149+
existingFilter.id = filter.id;
150+
if ('value' in existingFilter && 'value' in filter) {
151+
if (Array.isArray(existingFilter.value) && Array.isArray(filter.value)) {
152+
existingFilter.value = [...new Set([...existingFilter.value, ...filter.value])];
153+
} else if (filter.value !== undefined) {
154+
existingFilter.value = filter.value;
155+
}
156+
} else {
157+
throw new Error('Unable to merge filters');
158+
}
159+
}
160+
161+
uniqueFilters.set(filter.key, existingFilter ?? filter);
162+
} else {
163+
uniqueFilters.set(filter.id, filter);
164+
}
165+
}
166+
167+
return Array.from(uniqueFilters.values());
168+
}

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters/index.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,6 @@ import DateFacetedFilterTrigger from './date-faceted-filter-trigger.svelte';
66
import DateFacetedFilter from './date-faceted-filter.svelte';
77
import KeywordFacetedFilterBuilder from './keyword-faceted-filter-builder.svelte';
88
import KeywordFacetedFilter from './keyword-faceted-filter.svelte';
9-
export {
10-
type BooleanFilter,
11-
type DateFilter,
12-
KeywordFilter,
13-
type NumberFilter,
14-
ProjectFilter,
15-
ReferenceFilter,
16-
SessionFilter,
17-
StatusFilter,
18-
type StringFilter,
19-
TypeFilter,
20-
type VersionFilter
21-
} from './models.svelte';
229
import NumberFacetedFilterBuilder from './number-faceted-filter-builder.svelte';
2310
import NumberFacetedFilterTrigger from './number-faceted-filter-trigger.svelte';
2411
import NumberFacetedFilter from './number-faceted-filter.svelte';
@@ -110,3 +97,17 @@ export {
11097
VersionFacetedFilterTrigger,
11198
VersionFacetedFilterTrigger as VersionTrigger
11299
};
100+
101+
export {
102+
type BooleanFilter,
103+
type DateFilter,
104+
KeywordFilter,
105+
type NumberFilter,
106+
ProjectFilter,
107+
ReferenceFilter,
108+
SessionFilter,
109+
StatusFilter,
110+
type StringFilter,
111+
TypeFilter,
112+
type VersionFilter
113+
} from './models.svelte';
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script lang="ts">
22
import { builderContext, type FacetFilterBuilder, type IFilter } from '$comp/faceted-filter';
3-
import { onMount } from 'svelte';
43
54
import KeywordFacetedFilter from './keyword-faceted-filter.svelte';
65
import { KeywordFilter } from './models.svelte';
@@ -12,15 +11,12 @@
1211
1312
const { priority = 0, title = 'Keyword' }: Props = $props();
1413
15-
onMount(() => {
16-
const builder: FacetFilterBuilder<KeywordFilter> = {
17-
component: KeywordFacetedFilter,
18-
create: (filter?: KeywordFilter) => filter ?? new KeywordFilter(),
19-
priority,
20-
title
21-
};
14+
const builder: FacetFilterBuilder<KeywordFilter> = {
15+
component: KeywordFacetedFilter,
16+
create: (filter?: KeywordFilter) => filter ?? new KeywordFilter(),
17+
priority,
18+
title
19+
};
2220
23-
builderContext.set('keyword', builder as unknown as FacetFilterBuilder<IFilter>);
24-
return () => builderContext.delete('keyword');
25-
});
21+
builderContext.set('keyword', builder as unknown as FacetFilterBuilder<IFilter>);
2622
</script>

0 commit comments

Comments
 (0)