From fbf716ec1282f5d60f2d4e2a050069f6ed9dfe36 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 22 Nov 2024 07:54:04 -0600 Subject: [PATCH 01/12] WIP - Debounce grid updates based on filtering --- .../components/table/EventsDataTable.svelte | 80 ++++++++++++++++++- .../table/EventsTailLogDataTable.svelte | 71 +++++++++++++++- .../components/filters/filters.svelte.ts | 16 ++++ .../src/lib/features/websockets/models.ts | 6 +- .../ClientApp/src/routes/(app)/+page.svelte | 2 +- .../src/routes/(app)/issues/+page.svelte | 2 +- .../src/routes/(app)/stream/+page.svelte | 2 +- 7 files changed, 168 insertions(+), 11 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte index d071cb363..5a6a2a7fc 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte @@ -2,10 +2,13 @@ import type { Snippet } from 'svelte'; import * as DataTable from '$comp/data-table'; + import { getKeywordFilter, getOrganizationFilter, getProjectFilter, getStackFilter, type IFilter } from "$comp/filters/filters.svelte"; + import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models'; import { DEFAULT_LIMIT } from '$shared/api'; import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient'; import { createTable } from '@tanstack/svelte-table'; import { useEventListener } from 'runed'; + import { debounce } from 'throttle-debounce'; import type { GetEventsMode } from '../../api.svelte'; import type { EventSummaryModel, SummaryTemplateKeys } from '../summary/index'; @@ -14,6 +17,7 @@ interface Props { filter: string; + filters: IFilter[]; limit: number; mode?: GetEventsMode; pageFilter?: string; @@ -22,7 +26,7 @@ toolbarChildren?: Snippet; } - let { filter, limit = $bindable(DEFAULT_LIMIT), mode = 'summary', pageFilter = undefined, rowclick, time, toolbarChildren }: Props = $props(); + let { filter, filters, limit = $bindable(DEFAULT_LIMIT), mode = 'summary', pageFilter = undefined, rowclick, time, toolbarChildren }: Props = $props(); const context = getTableContext>({ limit, mode }); const table = createTable(context.options); @@ -53,8 +57,82 @@ table.resetRowSelection(); } } + const debouncedLoadData = debounce(10000, loadData); + + async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) { + const shouldRefresh = () => { + if (!filter) { + return true; + } + + const { id, organization_id, project_id, stack_id } = message; + if (id) { + // Check to see if any records on the page match + if (mode === "summary" && table.options.data.some((doc) => doc.id === id)) { + return true; + } + + // This could match any kind of lucene query (even must not filtering) + const keywordFilter = getKeywordFilter(filters); + if (keywordFilter && !keywordFilter.isEmpty()) { + if (keywordFilter.value!.includes(id)) { + return true; + } + } + } + + if (stack_id) { + // Check to see if any records on the page match + if (mode !== "summary" && table.options.data.some((doc) => doc.id === stack_id)) { + return true; + } + + const stackFilter = getStackFilter(filters); + if (stackFilter && !stackFilter.isEmpty()) { + return stackFilter.value === stack_id; + } + } + + if (project_id) { + const projectFilter = getProjectFilter(filters); + if (projectFilter && !projectFilter.isEmpty()) { + return projectFilter.value.includes(project_id); + } + } + + if (organization_id) { + const organizationFilter = getOrganizationFilter(filters); + if (organizationFilter && !organizationFilter.isEmpty()) { + return organizationFilter.value === organization_id; + } + } + + return true; + }; + + switch (message.change_type) { + case ChangeType.Added: + case ChangeType.Saved: + if (shouldRefresh()) { + await debouncedLoadData(); + } + + break; + case ChangeType.Removed: + if (shouldRefresh()) { + if (message.id && mode === "summary") { + table.options.data = table.options.data.filter((doc) => doc.id !== message.id); + } + + await debouncedLoadData(); + } + + break; + } + } useEventListener(document, 'refresh', async () => await loadData()); + useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail)); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte index 2803c900d..3bfcdbe12 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte @@ -3,12 +3,14 @@ import * as DataTable from '$comp/data-table'; import ErrorMessage from '$comp/ErrorMessage.svelte'; + import { getKeywordFilter, getOrganizationFilter, getProjectFilter, getStackFilter, type IFilter } from "$comp/filters/filters.svelte"; import { Muted } from '$comp/typography'; import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models'; import { DEFAULT_LIMIT } from '$shared/api'; import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient'; import { createTable } from '@tanstack/svelte-table'; import { useEventListener } from 'runed'; + import { debounce } from "throttle-debounce"; import type { EventSummaryModel, SummaryTemplateKeys } from '../summary/index'; @@ -16,12 +18,13 @@ interface Props { filter: string; + filters: IFilter[]; limit: number; rowclick?: (row: EventSummaryModel) => void; toolbarChildren?: Snippet; } - let { filter, limit = $bindable(DEFAULT_LIMIT), rowclick, toolbarChildren }: Props = $props(); + let { filter, filters, limit = $bindable(DEFAULT_LIMIT), rowclick, toolbarChildren }: Props = $props(); const context = getTableContext>({ limit, mode: 'summary' }, (options) => ({ ...options, columns: options.columns.filter((c) => c.id !== 'select').map((c) => ({ ...c, enableSorting: false })), @@ -58,7 +61,9 @@ }); if (response.ok) { - before = response.meta.links.previous?.before; + if (response.meta.links.previous?.before) { + before = response.meta.links.previous?.before; + } const data = filterChanged ? [] : [...context.data]; for (const summary of response.data?.reverse() || []) { @@ -70,13 +75,71 @@ } } + const debouncedLoadData = debounce(5000, loadData); + async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) { + const shouldRefresh = () => { + if (!filter) { + return true; + } + + const { id, organization_id, project_id, stack_id } = message; + if (id) { + // Check to see if any records on the page match + if (table.options.data.some((doc) => doc.id === id)) { + return true; + } + + // This could match any kind of lucene query (even must not filtering) + const keywordFilter = getKeywordFilter(filters); + if (keywordFilter && !keywordFilter.isEmpty()) { + if (keywordFilter.value!.includes(id)) { + return true; + } + } + } + + if (stack_id) { + const stackFilter = getStackFilter(filters); + if (stackFilter && !stackFilter.isEmpty()) { + return stackFilter.value === stack_id; + } + } + + if (project_id) { + const projectFilter = getProjectFilter(filters); + if (projectFilter && !projectFilter.isEmpty()) { + return projectFilter.value.includes(project_id); + } + } + + if (organization_id) { + const organizationFilter = getOrganizationFilter(filters); + if (organizationFilter && !organizationFilter.isEmpty()) { + return organizationFilter.value === organization_id; + } + } + + return true; + }; + switch (message.change_type) { case ChangeType.Added: case ChangeType.Saved: - return await loadData(); + if (shouldRefresh()) { + await debouncedLoadData(); + } + + break; case ChangeType.Removed: - table.options.data = table.options.data.filter((doc) => doc.id !== message.id); + if (shouldRefresh()) { + if (message.id) { + table.options.data = table.options.data.filter((doc) => doc.id !== message.id); + } + + await debouncedLoadData(); + } + break; } } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts index 49b5ab298..8528903b2 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts @@ -590,6 +590,22 @@ export function getFilter(filter: Omit f.type === 'keyword') as KeywordFilter; +} + +export function getOrganizationFilter(filters: IFilter[]): OrganizationFilter | undefined { + return filters.find((f) => f.type === 'organization') as OrganizationFilter; +} + +export function getProjectFilter(filters: IFilter[]): ProjectFilter { + return filters.find((f) => f.type === 'project') as ProjectFilter; +} + +export function getStackFilter(filters: IFilter[]): StringFilter | undefined { + return filters.find((f) => f.type === 'string') as StringFilter; +} + export function processFilterRules(filters: IFilter[], changed?: IFilter): IFilter[] { // Allow only one filter per type and term. const groupedFilters: Partial> = Object.groupBy(filters, (f: IFilter) => f.key); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/websockets/models.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/websockets/models.ts index 4e1805b2b..51bd565c6 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/websockets/models.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/websockets/models.ts @@ -8,10 +8,10 @@ export interface EntityChanged { change_type: ChangeType; data: Record; - id: string; + id?: string; organization_id?: string; - projectId?: string; - stackId?: string; + project_id?: string; + stack_id?: string; type: string; } diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte index 6a7eb8d32..2d2c1d2bb 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte @@ -44,7 +44,7 @@ Events - + {#snippet toolbarChildren()} {/snippet} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte index 8b5edcc22..3e19a0409 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte @@ -59,7 +59,7 @@ Issues - + {#snippet toolbarChildren()} {/snippet} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte index b9b8198f0..f8c2aee5e 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte @@ -44,7 +44,7 @@ Event Stream - + {#snippet toolbarChildren()} {/snippet} From 99b673794f283c72671771336089eafe203667c2 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 27 Nov 2024 19:26:03 -0600 Subject: [PATCH 02/12] Fixed grid filtering --- .../lib/features/events/components/filters.ts | 47 +++++ .../components/table/EventsDataTable.svelte | 127 +------------ .../table/EventsTailLogDataTable.svelte | 167 ------------------ .../events/components/table/options.svelte.ts | 63 +++---- .../data-table/data-table-page-size.svelte | 11 +- .../ClientApp/src/routes/(app)/+page.svelte | 68 ++++++- .../src/routes/(app)/issues/+page.svelte | 48 ++++- .../src/routes/(app)/stream/+page.svelte | 105 ++++++++++- 8 files changed, 300 insertions(+), 336 deletions(-) create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters.ts delete mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters.ts new file mode 100644 index 000000000..480438854 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters.ts @@ -0,0 +1,47 @@ +import { getKeywordFilter, getOrganizationFilter, getProjectFilter, getStackFilter, type IFilter } from '$comp/filters/filters.svelte'; + +export function shouldRefreshPersistentEventChanged( + filters: IFilter[], + filter: string, + organization_id?: string, + project_id?: string, + stack_id?: string, + id?: string +) { + if (!filter) { + return true; + } + + if (id) { + // This could match any kind of lucene query (even must not filtering) + const keywordFilter = getKeywordFilter(filters); + if (keywordFilter && !keywordFilter.isEmpty()) { + if (keywordFilter.value!.includes(id)) { + return true; + } + } + } + + if (stack_id) { + const stackFilter = getStackFilter(filters); + if (stackFilter && !stackFilter.isEmpty()) { + return stackFilter.value === stack_id; + } + } + + if (project_id) { + const projectFilter = getProjectFilter(filters); + if (projectFilter && !projectFilter.isEmpty()) { + return projectFilter.value.includes(project_id); + } + } + + if (organization_id) { + const organizationFilter = getOrganizationFilter(filters); + if (organizationFilter && !organizationFilter.isEmpty()) { + return organizationFilter.value === organization_id; + } + } + + return true; +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte index 5a6a2a7fc..cf2ef272f 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte @@ -2,137 +2,18 @@ import type { Snippet } from 'svelte'; import * as DataTable from '$comp/data-table'; - import { getKeywordFilter, getOrganizationFilter, getProjectFilter, getStackFilter, type IFilter } from "$comp/filters/filters.svelte"; - import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models'; - import { DEFAULT_LIMIT } from '$shared/api'; - import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient'; - import { createTable } from '@tanstack/svelte-table'; - import { useEventListener } from 'runed'; - import { debounce } from 'throttle-debounce'; + import { type Table } from '@tanstack/svelte-table'; - import type { GetEventsMode } from '../../api.svelte'; import type { EventSummaryModel, SummaryTemplateKeys } from '../summary/index'; - import { getTableContext } from './options.svelte'; - interface Props { - filter: string; - filters: IFilter[]; limit: number; - mode?: GetEventsMode; - pageFilter?: string; rowclick?: (row: EventSummaryModel) => void; - time: string; + table: Table>; toolbarChildren?: Snippet; } - let { filter, filters, limit = $bindable(DEFAULT_LIMIT), mode = 'summary', pageFilter = undefined, rowclick, time, toolbarChildren }: Props = $props(); - const context = getTableContext>({ limit, mode }); - const table = createTable(context.options); - - const client = useFetchClient(); - let response: FetchClientResponse[]>; - - $effect(() => { - limit = Number(context.limit); - loadData(); - }); - - async function loadData() { - if (client.loading) { - return; - } - - response = await client.getJSON[]>('events', { - params: { - ...context.parameters, - filter: [pageFilter, filter].filter(Boolean).join(' '), - time - } - }); - - if (response.ok) { - context.data = response.data || []; - context.meta = response.meta; - table.resetRowSelection(); - } - } - const debouncedLoadData = debounce(10000, loadData); - - async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) { - const shouldRefresh = () => { - if (!filter) { - return true; - } - - const { id, organization_id, project_id, stack_id } = message; - if (id) { - // Check to see if any records on the page match - if (mode === "summary" && table.options.data.some((doc) => doc.id === id)) { - return true; - } - - // This could match any kind of lucene query (even must not filtering) - const keywordFilter = getKeywordFilter(filters); - if (keywordFilter && !keywordFilter.isEmpty()) { - if (keywordFilter.value!.includes(id)) { - return true; - } - } - } - - if (stack_id) { - // Check to see if any records on the page match - if (mode !== "summary" && table.options.data.some((doc) => doc.id === stack_id)) { - return true; - } - - const stackFilter = getStackFilter(filters); - if (stackFilter && !stackFilter.isEmpty()) { - return stackFilter.value === stack_id; - } - } - - if (project_id) { - const projectFilter = getProjectFilter(filters); - if (projectFilter && !projectFilter.isEmpty()) { - return projectFilter.value.includes(project_id); - } - } - - if (organization_id) { - const organizationFilter = getOrganizationFilter(filters); - if (organizationFilter && !organizationFilter.isEmpty()) { - return organizationFilter.value === organization_id; - } - } - - return true; - }; - - switch (message.change_type) { - case ChangeType.Added: - case ChangeType.Saved: - if (shouldRefresh()) { - await debouncedLoadData(); - } - - break; - case ChangeType.Removed: - if (shouldRefresh()) { - if (message.id && mode === "summary") { - table.options.data = table.options.data.filter((doc) => doc.id !== message.id); - } - - await debouncedLoadData(); - } - - break; - } - } - - useEventListener(document, 'refresh', async () => await loadData()); - useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail)); + let { limit = $bindable(), rowclick, table, toolbarChildren }: Props = $props(); @@ -143,6 +24,6 @@ - + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte deleted file mode 100644 index 3bfcdbe12..000000000 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsTailLogDataTable.svelte +++ /dev/null @@ -1,167 +0,0 @@ - - - - - {#if toolbarChildren} - {@render toolbarChildren()} - {/if} - - - - - - {#if response?.problem?.errors.general} - - {/if} - -
-
-
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 ffb1f8a82..3236355af 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 @@ -141,17 +141,17 @@ export function getTableContext) => TableOptions = (options) => options ) { - let parameters = $state(params); - let pageCount = $state(0); - let data = $state([] as TSummaryModel[]); - let loading = $state(false); - let meta = $state({} as FetchClientResponse['meta']); + let _parameters = $state(params); + let _pageCount = $state(0); + let _data = $state([] as TSummaryModel[]); + let _loading = $state(false); + let _meta = $state({} as FetchClientResponse['meta']); - const columns = getColumns(parameters.mode); + const columns = getColumns(_parameters.mode); const [columnVisibility, setColumnVisibility] = createPersistedTableState('events-column-visibility', {}); const [pagination, setPagination] = createTableState({ pageIndex: 0, - pageSize: parameters.limit ?? DEFAULT_LIMIT + pageSize: _parameters.limit ?? DEFAULT_LIMIT }); const [sorting, setSorting] = createTableState([ { @@ -161,11 +161,11 @@ export function getTableContext({}); const onPaginationChange = (updaterOrValue: Updater) => { - if (loading) { + if (_loading) { return; } - loading = true; + _loading = true; const previousPageIndex = pagination().pageIndex; setPagination(updaterOrValue); @@ -173,10 +173,10 @@ export function getTableContext previousPageIndex ? (meta.links.next?.after as string) : undefined, - before: currentPageInfo.pageIndex < previousPageIndex && currentPageInfo.pageIndex > 0 ? (meta.links.previous?.before as string) : undefined, + _parameters = { + ..._parameters, + after: currentPageInfo.pageIndex > previousPageIndex ? (_meta.links.next?.after as string) : undefined, + before: currentPageInfo.pageIndex < previousPageIndex && currentPageInfo.pageIndex > 0 ? (_meta.links.previous?.before as string) : undefined, limit: currentPageInfo.pageSize }; }; @@ -184,8 +184,8 @@ export function getTableContext) => { setSorting(updaterOrValue); - parameters = { - ...parameters, + _parameters = { + ..._parameters, after: undefined, before: undefined, sort: @@ -200,7 +200,7 @@ export function getTableContext; - value: string; + value: number; } let { table, value = $bindable() }: Props = $props(); - const items = [ + type Item = { label: string; value: string }; + const items: Item[] = [ { label: '5', value: '5' }, { label: '10', value: '10' }, { label: '20', value: '20' }, @@ -23,15 +24,17 @@ { label: '50', value: '50' } ]; - let selected = $derived(items.find((item) => item.value === value) || items[0]); + let valueString = $derived(value + ''); + let selected = $derived((items.find((item) => item.value === valueString) || items[0]) as Item); function onValueChange(newValue: string) { + value = Number(newValue); table.setPageSize(Number(newValue)); }

Rows per page

- + {selected.label} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte index 2d2c1d2bb..a0c7663af 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte @@ -8,8 +8,15 @@ import * as Card from '$comp/ui/card'; import * as Sheet from '$comp/ui/sheet'; import EventsDrawer from '$features/events/components/EventsDrawer.svelte'; + import { shouldRefreshPersistentEventChanged } from '$features/events/components/filters'; import EventsDataTable from '$features/events/components/table/EventsDataTable.svelte'; + import { getTableContext } from '$features/events/components/table/options.svelte'; + import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models'; import { persisted } from '$shared/persisted.svelte'; + import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient'; + import { createTable } from '@tanstack/svelte-table'; + import { useEventListener } from 'runed'; + import { debounce } from 'throttle-debounce'; import IconOpenInNew from '~icons/mdi/open-in-new'; let selectedEventId: null | string = $state(null); @@ -38,13 +45,72 @@ function onFilterRemoved(filter?: IFilter): void { persistedFilters.value = filterRemoved(persistedFilters.value, defaultFilters, filter); } + + const context = getTableContext>({ limit: limit.value, mode: 'summary' }); + const table = createTable(context.options); + + const client = useFetchClient(); + let response: FetchClientResponse[]>; + + async function loadData() { + if (client.loading) { + return; + } + + response = await client.getJSON[]>('events', { + params: { + ...context.parameters, + filter, + time + } + }); + + if (response.ok) { + context.data = response.data || []; + context.meta = response.meta; + table.resetRowSelection(); + } + } + const debouncedLoadData = debounce(10000, loadData); + + async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) { + const shouldRefresh = () => + shouldRefreshPersistentEventChanged(persistedFilters.value, filter, message.organization_id, message.project_id, message.stack_id, message.id); + + switch (message.change_type) { + case ChangeType.Added: + case ChangeType.Saved: + if (shouldRefresh()) { + await debouncedLoadData(); + } + + break; + case ChangeType.Removed: + if (shouldRefresh()) { + if (message.id) { + table.options.data = table.options.data.filter((doc) => doc.id !== message.id); + } + + await debouncedLoadData(); + } + + break; + } + } + + useEventListener(document, 'refresh', async () => await loadData()); + useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail)); + + $effect(() => { + loadData(); + });
Events - + {#snippet toolbarChildren()} {/snippet} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte index 3e19a0409..85a1922a8 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte @@ -9,8 +9,15 @@ import * as Sheet from '$comp/ui/sheet'; import { getEventsByStackIdQuery } from '$features/events/api.svelte'; import EventsDrawer from '$features/events/components/EventsDrawer.svelte'; + import { shouldRefreshPersistentEventChanged } from '$features/events/components/filters'; import EventsDataTable from '$features/events/components/table/EventsDataTable.svelte'; + import { getTableContext } from '$features/events/components/table/options.svelte'; + import { type WebSocketMessageValue } from '$features/websockets/models'; import { persisted } from '$shared/persisted.svelte'; + import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient'; + import { createTable } from '@tanstack/svelte-table'; + import { useEventListener } from 'runed'; + import { debounce } from 'throttle-debounce'; import IconOpenInNew from '~icons/mdi/open-in-new'; let selectedStackId = $state(); @@ -53,13 +60,52 @@ function onFilterRemoved(filter?: IFilter): void { persistedFilters.value = filterRemoved(persistedFilters.value, defaultFilters, filter); } + + const context = getTableContext>({ limit: limit.value, mode: 'stack_frequent' }); + const table = createTable(context.options); + + const client = useFetchClient(); + let response: FetchClientResponse[]>; + + async function loadData() { + if (client.loading) { + return; + } + + response = await client.getJSON[]>('events', { + params: { + ...context.parameters, + filter: ['(type:404 OR type:error)', filter].filter(Boolean).join(' '), + time + } + }); + + if (response.ok) { + context.data = response.data || []; + context.meta = response.meta; + table.resetRowSelection(); + } + } + const debouncedLoadData = debounce(10000, loadData); + async function onPersistentEvent(message: WebSocketMessageValue<'PersistentEventChanged'>) { + if (shouldRefreshPersistentEventChanged(persistedFilters.value, filter, message.organization_id, message.project_id, message.stack_id, message.id)) { + await debouncedLoadData(); + } + } + + useEventListener(document, 'refresh', async () => await loadData()); + useEventListener(document, 'PersistentEventChanged', async (event) => await onPersistentEvent((event as CustomEvent).detail)); + + $effect(() => { + loadData(); + });
Issues - + {#snippet toolbarChildren()} {/snippet} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte index f8c2aee5e..8cbe32a66 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte @@ -1,15 +1,24 @@ Event Stream - - {#snippet toolbarChildren()} + + - {/snippet} - + + + + +
+ +
+
+
From 3e308b2203682568142b628dc984d0c6b332ffc7 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 27 Nov 2024 19:27:04 -0600 Subject: [PATCH 03/12] Fixed transitions between when page content is replaced during routing, previously you could see the page contents on the screen. --- .../ClientApp/src/routes/(app)/+layout.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte index 8383d9870..96656e7e7 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte @@ -10,6 +10,7 @@ import { validate } from '$shared/validation'; import { setModelValidator, useMiddleware } from '@exceptionless/fetchclient'; import { useQueryClient } from '@tanstack/svelte-query'; + import { fade } from 'svelte/transition'; import { type NavigationItemContext, routes } from '../routes'; import FooterLayout from './(components)/layouts/Footer.svelte'; @@ -130,7 +131,11 @@
- {@render children()} + {#key $page.url.pathname} +
+ {@render children()} +
+ {/key}
From 8ad07878bbd6f2e586f4e1fdff3d9a7a2910d42f Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 27 Nov 2024 19:28:52 -0600 Subject: [PATCH 04/12] greatly improved client code gen --- .../api-templates/class-data-contract.ejs | 2 +- .../object-field-class-validator.ejs | 4 +- .../ClientApp/src/lib/generated/api.ts | 343 ++++++++++-------- 3 files changed, 189 insertions(+), 160 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/api-templates/class-data-contract.ejs b/src/Exceptionless.Web/ClientApp/api-templates/class-data-contract.ejs index fdfc70db4..c523cc8fe 100644 --- a/src/Exceptionless.Web/ClientApp/api-templates/class-data-contract.ejs +++ b/src/Exceptionless.Web/ClientApp/api-templates/class-data-contract.ejs @@ -6,7 +6,7 @@ export class <%~ contract.name %> { <% for (const field of contract.$content) { %> <%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %> <%~ includeFile('./object-field-class-validator.ejs', { ...it, field }) %> - <%~ field.name %><%~ field.isRequired || field.nullable !== true ? '!' : '' %>: <%~ field.value.replaceAll('any', 'unknown') %><%~ field.type === "object" && field.nullable === true ? ' | null' : '' %><%~ field.nullable === true ? ' = null' : '' %>; + <%~ field.name %><%~ field.isRequired || !field.nullable ? '!' : '' %><%~ field.nullable ? '?' : '' %>: <%~ field.value.replaceAll('any', 'unknown') %>; <% } %> } diff --git a/src/Exceptionless.Web/ClientApp/api-templates/object-field-class-validator.ejs b/src/Exceptionless.Web/ClientApp/api-templates/object-field-class-validator.ejs index 986146359..d0c3c4b22 100644 --- a/src/Exceptionless.Web/ClientApp/api-templates/object-field-class-validator.ejs +++ b/src/Exceptionless.Web/ClientApp/api-templates/object-field-class-validator.ejs @@ -22,7 +22,7 @@ function getFormatValidation(field) { } const validationDecorators = _.compact([ - !field.isRequired && "@IsOptional()", + !field.isRequired && field.nullable && "@IsOptional()", (field.name === "id" || field.name.endsWith("_id")) && `@IsMongoId({ message: '${field.name} must be a valid ObjectId.' })`, field.name === "url" && `@IsUrl({}, { message: '${field.name} must be a valid URL.' })`, !_.isUndefined(field.format) && getFormatValidation(field), @@ -32,7 +32,7 @@ const validationDecorators = _.compact([ !_.isUndefined(field.type) && (field.type === "object" || (field.type === "array" && field.items.$ref)) && `@ValidateNested({ message: '${field.name} must be a valid nested object.' })`, ]); -if (!validationDecorators.length && field.isRequired) { +if (!validationDecorators.length && (field.isRequired || !field.nullable)) { validationDecorators.push(`@IsDefined({ message: '${field.name} is required.' })`); } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts b/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts index c815aba15..730409eb6 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts @@ -18,17 +18,17 @@ export class BillingPlan { @IsDefined({ message: 'name is required.' }) name!: string; @IsDefined({ message: 'description is required.' }) description!: string; /** @format double */ - @IsOptional() @IsNumber({}, { message: 'price must be a numeric value.' }) price?: number; + @IsNumber({}, { message: 'price must be a numeric value.' }) price!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'max_projects must be a whole number.' }) max_projects?: number; + @IsInt({ message: 'max_projects must be a whole number.' }) max_projects!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'max_users must be a whole number.' }) max_users?: number; + @IsInt({ message: 'max_users must be a whole number.' }) max_users!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'retention_days must be a whole number.' }) retention_days?: number; + @IsInt({ message: 'retention_days must be a whole number.' }) retention_days!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'max_events_per_month must be a whole number.' }) max_events_per_month?: number; - @IsOptional() has_premium_features?: boolean; - @IsOptional() is_hidden?: boolean; + @IsInt({ message: 'max_events_per_month must be a whole number.' }) max_events_per_month!: number; + @IsDefined({ message: 'has_premium_features is required.' }) has_premium_features!: boolean; + @IsDefined({ message: 'is_hidden is required.' }) is_hidden!: boolean; } /** @@ -54,28 +54,39 @@ export enum BillingStatus { } export class ChangePasswordModel { - @IsOptional() current_password?: string | null; - @IsOptional() password?: string | null; + @MinLength(6, { message: 'current_password must be at least 6 characters long.' }) + @MaxLength(100, { message: 'current_password must be at most 100 characters long.' }) + current_password!: string; + @MinLength(6, { message: 'password must be at least 6 characters long.' }) + @MaxLength(100, { message: 'password must be at most 100 characters long.' }) + password!: string; } export class ChangePlanResult { - @IsOptional() success?: boolean; + @IsDefined({ message: 'success is required.' }) success!: boolean; @IsOptional() message?: string | null; } export class ClientConfiguration { /** @format int32 */ - @IsOptional() @IsInt({ message: 'version must be a whole number.' }) version?: number; - @IsOptional() @ValidateNested({ message: 'settings must be a valid nested object.' }) settings?: Record; + @IsInt({ message: 'version must be a whole number.' }) version!: number; + @ValidateNested({ message: 'settings must be a valid nested object.' }) settings!: Record; } export class CountResult { /** @format int64 */ - @IsOptional() @IsInt({ message: 'total must be a whole number.' }) total?: number; + @IsInt({ message: 'total must be a whole number.' }) total!: number; @IsOptional() @ValidateNested({ message: 'aggregations must be a valid nested object.' }) aggregations?: Record; @IsOptional() @ValidateNested({ message: 'data must be a valid nested object.' }) data?: Record; } +export class ExternalAuthInfo { + @MinLength(1, { message: 'clientId must be at least 1 characters long.' }) clientId!: string; + @MinLength(1, { message: 'code must be at least 1 characters long.' }) code!: string; + @MinLength(1, { message: 'redirectUri must be at least 1 characters long.' }) redirectUri!: string; + @IsOptional() inviteToken?: string | null; +} + export interface IAggregate { data?: Record; } @@ -96,7 +107,7 @@ export class Invoice { @IsDefined({ message: 'paid is required.' }) paid!: boolean; /** @format double */ @IsNumber({}, { message: 'total must be a numeric value.' }) total!: number; - @IsOptional() @ValidateNested({ message: 'items must be a valid nested object.' }) items?: InvoiceLineItem[]; + @ValidateNested({ message: 'items must be a valid nested object.' }) items!: InvoiceLineItem[]; } export class InvoiceGridModel { @@ -114,10 +125,8 @@ export class InvoiceLineItem { } export class Login { - /** @format email */ - @IsEmail({ require_tld: false }, { message: 'email must be a valid email address.' }) - @MinLength(1, { message: 'email must be at least 1 characters long.' }) - email!: string; + /** The email address or domain username */ + @MinLength(1, { message: 'email must be at least 1 characters long.' }) email!: string; @MinLength(6, { message: 'password must be at least 6 characters long.' }) @MaxLength(100, { message: 'password must be at most 100 characters long.' }) password!: string; @@ -128,63 +137,63 @@ export class Login { } export class NewOrganization { - @IsOptional() name?: string; + @IsDefined({ message: 'name is required.' }) name!: string; } export class NewProject { - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() name?: string; - @IsOptional() delete_bot_data_enabled?: boolean; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsDefined({ message: 'name is required.' }) name!: string; + @IsDefined({ message: 'delete_bot_data_enabled is required.' }) delete_bot_data_enabled!: boolean; } export class NewToken { - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id?: string; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id!: string; @IsOptional() @IsMongoId({ message: 'default_project_id must be a valid ObjectId.' }) default_project_id?: string | null; - @IsOptional() scopes?: string[]; + @IsDefined({ message: 'scopes is required.' }) scopes!: string[]; /** @format date-time */ @IsOptional() @IsDate({ message: 'expires_utc must be a valid date and time.' }) expires_utc?: string | null; @IsOptional() notes?: string | null; } export class NewWebHook { - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id?: string; - @IsOptional() @IsUrl({}, { message: 'url must be a valid URL.' }) url?: string; - @IsOptional() event_types?: string[]; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id!: string; + @IsUrl({}, { message: 'url must be a valid URL.' }) url!: string; + @IsDefined({ message: 'event_types is required.' }) event_types!: string[]; /** The schema version that should be used. */ @IsOptional() version?: string | null; } export class NotificationSettings { - @IsOptional() send_daily_summary?: boolean; - @IsOptional() report_new_errors?: boolean; - @IsOptional() report_critical_errors?: boolean; - @IsOptional() report_event_regressions?: boolean; - @IsOptional() report_new_events?: boolean; - @IsOptional() report_critical_events?: boolean; + @IsDefined({ message: 'send_daily_summary is required.' }) send_daily_summary!: boolean; + @IsDefined({ message: 'report_new_errors is required.' }) report_new_errors!: boolean; + @IsDefined({ message: 'report_critical_errors is required.' }) report_critical_errors!: boolean; + @IsDefined({ message: 'report_event_regressions is required.' }) report_event_regressions!: boolean; + @IsDefined({ message: 'report_new_events is required.' }) report_new_events!: boolean; + @IsDefined({ message: 'report_critical_events is required.' }) report_critical_events!: boolean; } export class OAuthAccount { @IsDefined({ message: 'provider is required.' }) provider!: string; @IsMongoId({ message: 'provider_user_id must be a valid ObjectId.' }) provider_user_id!: string; @IsDefined({ message: 'username is required.' }) username!: string; - @IsOptional() @ValidateNested({ message: 'extra_data must be a valid nested object.' }) extra_data?: Record; + @ValidateNested({ message: 'extra_data must be a valid nested object.' }) extra_data!: Record; } export class PersistentEvent { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id?: string; - @IsOptional() @IsMongoId({ message: 'stack_id must be a valid ObjectId.' }) stack_id?: string; - @IsOptional() is_first_occurrence?: boolean; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id!: string; + @IsMongoId({ message: 'stack_id must be a valid ObjectId.' }) stack_id!: string; + @IsDefined({ message: 'is_first_occurrence is required.' }) is_first_occurrence!: boolean; /** @format date-time */ - @IsOptional() @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc?: string; - @IsOptional() @ValidateNested({ message: 'idx must be a valid nested object.' }) idx?: Record; + @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc!: string; + @ValidateNested({ message: 'idx must be a valid nested object.' }) idx!: Record; @IsOptional() type?: string | null; @IsOptional() source?: string | null; /** @format date-time */ - @IsOptional() @IsDate({ message: 'date must be a valid date and time.' }) date?: string; + @IsDate({ message: 'date must be a valid date and time.' }) date!: string; @IsOptional() tags?: string[] | null; @IsOptional() message?: string | null; @IsOptional() geo?: string | null; @@ -197,16 +206,18 @@ export class PersistentEvent { } export class ResetPasswordModel { - @IsOptional() password_reset_token?: string | null; - @IsOptional() password?: string | null; + @MinLength(40, { message: 'password_reset_token must be at least 40 characters long.' }) + @MaxLength(40, { message: 'password_reset_token must be at most 40 characters long.' }) + password_reset_token!: string; + @MinLength(6, { message: 'password must be at least 6 characters long.' }) + @MaxLength(100, { message: 'password must be at most 100 characters long.' }) + password!: string; } export class Signup { @MinLength(1, { message: 'name must be at least 1 characters long.' }) name!: string; - /** @format email */ - @IsEmail({ require_tld: false }, { message: 'email must be a valid email address.' }) - @MinLength(1, { message: 'email must be at least 1 characters long.' }) - email!: string; + /** The email address or domain username */ + @MinLength(1, { message: 'email must be at least 1 characters long.' }) email!: string; @MinLength(6, { message: 'password must be at least 6 characters long.' }) @MaxLength(100, { message: 'password must be at most 100 characters long.' }) password!: string; @@ -217,10 +228,10 @@ export class Signup { } export class Stack { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id?: string; - @IsOptional() type?: string; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id!: string; + @IsDefined({ message: 'type is required.' }) type!: string; /** * * open @@ -230,32 +241,32 @@ export class Stack { * ignored * discarded */ - @IsOptional() status?: StackStatus; + @IsDefined({ message: 'status is required.' }) status!: StackStatus; /** @format date-time */ @IsOptional() @IsDate({ message: 'snooze_until_utc must be a valid date and time.' }) snooze_until_utc?: string | null; - @IsOptional() signature_hash?: string; - @IsOptional() @ValidateNested({ message: 'signature_info must be a valid nested object.' }) signature_info?: Record; + @IsDefined({ message: 'signature_hash is required.' }) signature_hash!: string; + @ValidateNested({ message: 'signature_info must be a valid nested object.' }) signature_info!: Record; @IsOptional() fixed_in_version?: string | null; /** @format date-time */ @IsOptional() @IsDate({ message: 'date_fixed must be a valid date and time.' }) date_fixed?: string | null; - @IsOptional() title?: string; + @IsDefined({ message: 'title is required.' }) title!: string; /** @format int32 */ - @IsOptional() @IsInt({ message: 'total_occurrences must be a whole number.' }) total_occurrences?: number; + @IsInt({ message: 'total_occurrences must be a whole number.' }) total_occurrences!: number; /** @format date-time */ - @IsOptional() @IsDate({ message: 'first_occurrence must be a valid date and time.' }) first_occurrence?: string; + @IsDate({ message: 'first_occurrence must be a valid date and time.' }) first_occurrence!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'last_occurrence must be a valid date and time.' }) last_occurrence?: string; + @IsDate({ message: 'last_occurrence must be a valid date and time.' }) last_occurrence!: string; @IsOptional() description?: string | null; - @IsOptional() occurrences_are_critical?: boolean; - @IsOptional() references?: string[]; - @IsOptional() tags?: string[]; - @IsOptional() duplicate_signature?: string; + @IsDefined({ message: 'occurrences_are_critical is required.' }) occurrences_are_critical!: boolean; + @IsDefined({ message: 'references is required.' }) references!: string[]; + @IsDefined({ message: 'tags is required.' }) tags!: string[]; + @IsDefined({ message: 'duplicate_signature is required.' }) duplicate_signature!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc?: string; + @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'updated_utc must be a valid date and time.' }) updated_utc?: string; - @IsOptional() is_deleted?: boolean; - @IsOptional() allow_notifications?: boolean; + @IsDate({ message: 'updated_utc must be a valid date and time.' }) updated_utc!: string; + @IsDefined({ message: 'is_deleted is required.' }) is_deleted!: boolean; + @IsDefined({ message: 'allow_notifications is required.' }) allow_notifications!: boolean; } /** @@ -284,7 +295,7 @@ export enum StackStatus { export class StringStringValuesKeyValuePair { @IsOptional() key?: string | null; - @IsOptional() value?: string[]; + @IsDefined({ message: 'value is required.' }) value!: string[]; } export class StringValueFromBody { @@ -296,59 +307,62 @@ export class TokenResult { } export class UpdateEmailAddressResult { - @IsOptional() is_verified?: boolean; + @IsDefined({ message: 'is_verified is required.' }) is_verified!: boolean; } export class UsageHourInfo { /** @format date-time */ - @IsOptional() @IsDate({ message: 'date must be a valid date and time.' }) date?: string; + @IsDate({ message: 'date must be a valid date and time.' }) date!: string; /** @format int32 */ - @IsOptional() @IsInt({ message: 'total must be a whole number.' }) total?: number; + @IsInt({ message: 'total must be a whole number.' }) total!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'blocked must be a whole number.' }) blocked?: number; + @IsInt({ message: 'blocked must be a whole number.' }) blocked!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'discarded must be a whole number.' }) discarded?: number; + @IsInt({ message: 'discarded must be a whole number.' }) discarded!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'too_big must be a whole number.' }) too_big?: number; + @IsInt({ message: 'too_big must be a whole number.' }) too_big!: number; } export class UsageInfo { /** @format date-time */ - @IsOptional() @IsDate({ message: 'date must be a valid date and time.' }) date?: string; + @IsDate({ message: 'date must be a valid date and time.' }) date!: string; /** @format int32 */ - @IsOptional() @IsInt({ message: 'limit must be a whole number.' }) limit?: number; + @IsInt({ message: 'limit must be a whole number.' }) limit!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'total must be a whole number.' }) total?: number; + @IsInt({ message: 'total must be a whole number.' }) total!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'blocked must be a whole number.' }) blocked?: number; + @IsInt({ message: 'blocked must be a whole number.' }) blocked!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'discarded must be a whole number.' }) discarded?: number; + @IsInt({ message: 'discarded must be a whole number.' }) discarded!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'too_big must be a whole number.' }) too_big?: number; + @IsInt({ message: 'too_big must be a whole number.' }) too_big!: number; } export class User { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; - @IsOptional() organization_ids?: string[]; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; + @IsDefined({ message: 'organization_ids is required.' }) organization_ids!: string[]; @IsOptional() password?: string | null; @IsOptional() salt?: string | null; @IsOptional() password_reset_token?: string | null; /** @format date-time */ - @IsOptional() @IsDate({ message: 'password_reset_token_expiration must be a valid date and time.' }) password_reset_token_expiration?: string; - @IsOptional() @ValidateNested({ message: 'o_auth_accounts must be a valid nested object.' }) o_auth_accounts?: OAuthAccount[]; - @IsOptional() full_name?: string; - @IsOptional() email_address?: string; - @IsOptional() email_notifications_enabled?: boolean; - @IsOptional() is_email_address_verified?: boolean; + @IsDate({ message: 'password_reset_token_expiration must be a valid date and time.' }) password_reset_token_expiration!: string; + @ValidateNested({ message: 'o_auth_accounts must be a valid nested object.' }) o_auth_accounts!: OAuthAccount[]; + @MinLength(1, { message: 'full_name must be at least 1 characters long.' }) full_name!: string; + /** @format email */ + @IsEmail({ require_tld: false }, { message: 'email_address must be a valid email address.' }) + @MinLength(1, { message: 'email_address must be at least 1 characters long.' }) + email_address!: string; + @IsDefined({ message: 'email_notifications_enabled is required.' }) email_notifications_enabled!: boolean; + @IsDefined({ message: 'is_email_address_verified is required.' }) is_email_address_verified!: boolean; @IsOptional() verify_email_address_token?: string | null; /** @format date-time */ - @IsOptional() @IsDate({ message: 'verify_email_address_token_expiration must be a valid date and time.' }) verify_email_address_token_expiration?: string; - @IsOptional() is_active?: boolean; - @IsOptional() roles?: string[]; + @IsDate({ message: 'verify_email_address_token_expiration must be a valid date and time.' }) verify_email_address_token_expiration!: string; + @IsDefined({ message: 'is_active is required.' }) is_active!: boolean; + @IsDefined({ message: 'roles is required.' }) roles!: string[]; /** @format date-time */ - @IsOptional() @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc?: string; + @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'updated_utc must be a valid date and time.' }) updated_utc?: string; + @IsDate({ message: 'updated_utc must be a valid date and time.' }) updated_utc!: string; } export class UserDescription { @@ -357,14 +371,29 @@ export class UserDescription { @IsOptional() @ValidateNested({ message: 'data must be a valid nested object.' }) data?: Record; } +export class ViewCurrentUser { + @IsOptional() hash?: string | null; + @IsDefined({ message: 'has_local_account is required.' }) has_local_account!: boolean; + @ValidateNested({ message: 'o_auth_accounts must be a valid nested object.' }) o_auth_accounts!: OAuthAccount[]; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; + @IsDefined({ message: 'organization_ids is required.' }) organization_ids!: string[]; + @IsDefined({ message: 'full_name is required.' }) full_name!: string; + @IsDefined({ message: 'email_address is required.' }) email_address!: string; + @IsDefined({ message: 'email_notifications_enabled is required.' }) email_notifications_enabled!: boolean; + @IsDefined({ message: 'is_email_address_verified is required.' }) is_email_address_verified!: boolean; + @IsDefined({ message: 'is_active is required.' }) is_active!: boolean; + @IsDefined({ message: 'is_invite is required.' }) is_invite!: boolean; + @IsDefined({ message: 'roles is required.' }) roles!: string[]; +} + export class ViewOrganization { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc?: string; - @IsOptional() name?: string; - @IsOptional() @IsMongoId({ message: 'plan_id must be a valid ObjectId.' }) plan_id?: string; - @IsOptional() plan_name?: string; - @IsOptional() plan_description?: string; + @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc!: string; + @IsDefined({ message: 'name is required.' }) name!: string; + @IsMongoId({ message: 'plan_id must be a valid ObjectId.' }) plan_id!: string; + @IsDefined({ message: 'plan_name is required.' }) plan_name!: string; + @IsDefined({ message: 'plan_description is required.' }) plan_description!: string; @IsOptional() 'card_last4'?: string | null; /** @format date-time */ @IsOptional() @IsDate({ message: 'subscribe_date must be a valid date and time.' }) subscribe_date?: string | null; @@ -379,101 +408,101 @@ export class ViewOrganization { * 3 = Canceled * 4 = Unpaid */ - @IsOptional() billing_status?: BillingStatus; + @IsDefined({ message: 'billing_status is required.' }) billing_status!: BillingStatus; /** @format double */ - @IsOptional() @IsNumber({}, { message: 'billing_price must be a numeric value.' }) billing_price?: number; + @IsNumber({}, { message: 'billing_price must be a numeric value.' }) billing_price!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'max_events_per_month must be a whole number.' }) max_events_per_month?: number; + @IsInt({ message: 'max_events_per_month must be a whole number.' }) max_events_per_month!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'bonus_events_per_month must be a whole number.' }) bonus_events_per_month?: number; + @IsInt({ message: 'bonus_events_per_month must be a whole number.' }) bonus_events_per_month!: number; /** @format date-time */ @IsOptional() @IsDate({ message: 'bonus_expiration must be a valid date and time.' }) bonus_expiration?: string | null; /** @format int32 */ - @IsOptional() @IsInt({ message: 'retention_days must be a whole number.' }) retention_days?: number; - @IsOptional() is_suspended?: boolean; + @IsInt({ message: 'retention_days must be a whole number.' }) retention_days!: number; + @IsDefined({ message: 'is_suspended is required.' }) is_suspended!: boolean; @IsOptional() suspension_code?: string | null; @IsOptional() suspension_notes?: string | null; /** @format date-time */ @IsOptional() @IsDate({ message: 'suspension_date must be a valid date and time.' }) suspension_date?: string | null; - @IsOptional() has_premium_features?: boolean; + @IsDefined({ message: 'has_premium_features is required.' }) has_premium_features!: boolean; /** @format int32 */ - @IsOptional() @IsInt({ message: 'max_users must be a whole number.' }) max_users?: number; + @IsInt({ message: 'max_users must be a whole number.' }) max_users!: number; /** @format int32 */ - @IsOptional() @IsInt({ message: 'max_projects must be a whole number.' }) max_projects?: number; + @IsInt({ message: 'max_projects must be a whole number.' }) max_projects!: number; /** @format int64 */ - @IsOptional() @IsInt({ message: 'project_count must be a whole number.' }) project_count?: number; + @IsInt({ message: 'project_count must be a whole number.' }) project_count!: number; /** @format int64 */ - @IsOptional() @IsInt({ message: 'stack_count must be a whole number.' }) stack_count?: number; + @IsInt({ message: 'stack_count must be a whole number.' }) stack_count!: number; /** @format int64 */ - @IsOptional() @IsInt({ message: 'event_count must be a whole number.' }) event_count?: number; - @IsOptional() @ValidateNested({ message: 'invites must be a valid nested object.' }) invites?: Invite[]; - @IsOptional() @ValidateNested({ message: 'usage_hours must be a valid nested object.' }) usage_hours?: UsageHourInfo[]; - @IsOptional() @ValidateNested({ message: 'usage must be a valid nested object.' }) usage?: UsageInfo[]; + @IsInt({ message: 'event_count must be a whole number.' }) event_count!: number; + @ValidateNested({ message: 'invites must be a valid nested object.' }) invites!: Invite[]; + @ValidateNested({ message: 'usage_hours must be a valid nested object.' }) usage_hours!: UsageHourInfo[]; + @ValidateNested({ message: 'usage must be a valid nested object.' }) usage!: UsageInfo[]; @IsOptional() @ValidateNested({ message: 'data must be a valid nested object.' }) data?: Record; - @IsOptional() is_throttled?: boolean; - @IsOptional() is_over_monthly_limit?: boolean; - @IsOptional() is_over_request_limit?: boolean; + @IsDefined({ message: 'is_throttled is required.' }) is_throttled!: boolean; + @IsDefined({ message: 'is_over_monthly_limit is required.' }) is_over_monthly_limit!: boolean; + @IsDefined({ message: 'is_over_request_limit is required.' }) is_over_request_limit!: boolean; } export class ViewProject { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc?: string; - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() organization_name?: string; - @IsOptional() name?: string; - @IsOptional() delete_bot_data_enabled?: boolean; + @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc!: string; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsDefined({ message: 'organization_name is required.' }) organization_name!: string; + @IsDefined({ message: 'name is required.' }) name!: string; + @IsDefined({ message: 'delete_bot_data_enabled is required.' }) delete_bot_data_enabled!: boolean; @IsOptional() @ValidateNested({ message: 'data must be a valid nested object.' }) data?: Record; - @IsOptional() promoted_tabs?: string[]; + @IsDefined({ message: 'promoted_tabs is required.' }) promoted_tabs!: string[]; @IsOptional() is_configured?: boolean | null; /** @format int64 */ - @IsOptional() @IsInt({ message: 'stack_count must be a whole number.' }) stack_count?: number; + @IsInt({ message: 'stack_count must be a whole number.' }) stack_count!: number; /** @format int64 */ - @IsOptional() @IsInt({ message: 'event_count must be a whole number.' }) event_count?: number; - @IsOptional() has_premium_features?: boolean; - @IsOptional() has_slack_integration?: boolean; - @IsOptional() @ValidateNested({ message: 'usage_hours must be a valid nested object.' }) usage_hours?: UsageHourInfo[]; - @IsOptional() @ValidateNested({ message: 'usage must be a valid nested object.' }) usage?: UsageInfo[]; + @IsInt({ message: 'event_count must be a whole number.' }) event_count!: number; + @IsDefined({ message: 'has_premium_features is required.' }) has_premium_features!: boolean; + @IsDefined({ message: 'has_slack_integration is required.' }) has_slack_integration!: boolean; + @ValidateNested({ message: 'usage_hours must be a valid nested object.' }) usage_hours!: UsageHourInfo[]; + @ValidateNested({ message: 'usage must be a valid nested object.' }) usage!: UsageInfo[]; } export class ViewToken { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id?: string; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id!: string; @IsOptional() @IsMongoId({ message: 'user_id must be a valid ObjectId.' }) user_id?: string | null; @IsOptional() @IsMongoId({ message: 'default_project_id must be a valid ObjectId.' }) default_project_id?: string | null; - @IsOptional() scopes?: string[]; + @IsDefined({ message: 'scopes is required.' }) scopes!: string[]; /** @format date-time */ @IsOptional() @IsDate({ message: 'expires_utc must be a valid date and time.' }) expires_utc?: string | null; @IsOptional() notes?: string | null; - @IsOptional() is_disabled?: boolean; - @IsOptional() is_suspended?: boolean; + @IsDefined({ message: 'is_disabled is required.' }) is_disabled!: boolean; + @IsDefined({ message: 'is_suspended is required.' }) is_suspended!: boolean; /** @format date-time */ - @IsOptional() @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc?: string; + @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'updated_utc must be a valid date and time.' }) updated_utc?: string; + @IsDate({ message: 'updated_utc must be a valid date and time.' }) updated_utc!: string; } export class ViewUser { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; - @IsOptional() organization_ids?: string[]; - @IsOptional() full_name?: string; - @IsOptional() email_address?: string; - @IsOptional() email_notifications_enabled?: boolean; - @IsOptional() is_email_address_verified?: boolean; - @IsOptional() is_active?: boolean; - @IsOptional() is_invite?: boolean; - @IsOptional() roles?: string[]; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; + @IsDefined({ message: 'organization_ids is required.' }) organization_ids!: string[]; + @IsDefined({ message: 'full_name is required.' }) full_name!: string; + @IsDefined({ message: 'email_address is required.' }) email_address!: string; + @IsDefined({ message: 'email_notifications_enabled is required.' }) email_notifications_enabled!: boolean; + @IsDefined({ message: 'is_email_address_verified is required.' }) is_email_address_verified!: boolean; + @IsDefined({ message: 'is_active is required.' }) is_active!: boolean; + @IsDefined({ message: 'is_invite is required.' }) is_invite!: boolean; + @IsDefined({ message: 'roles is required.' }) roles!: string[]; } export class WebHook { - @IsOptional() @IsMongoId({ message: 'id must be a valid ObjectId.' }) id?: string; - @IsOptional() @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id?: string; - @IsOptional() @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id?: string; - @IsOptional() @IsUrl({}, { message: 'url must be a valid URL.' }) url?: string; - @IsOptional() event_types?: string[]; - @IsOptional() is_enabled?: boolean; - @IsOptional() version?: string; + @IsMongoId({ message: 'id must be a valid ObjectId.' }) id!: string; + @IsMongoId({ message: 'organization_id must be a valid ObjectId.' }) organization_id!: string; + @IsMongoId({ message: 'project_id must be a valid ObjectId.' }) project_id!: string; + @IsUrl({}, { message: 'url must be a valid URL.' }) url!: string; + @IsDefined({ message: 'event_types is required.' }) event_types!: string[]; + @IsDefined({ message: 'is_enabled is required.' }) is_enabled!: boolean; + @IsDefined({ message: 'version is required.' }) version!: string; /** @format date-time */ - @IsOptional() @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc?: string; + @IsDate({ message: 'created_utc must be a valid date and time.' }) created_utc!: string; } From ba222d4db399e4449a3b795b3189a7217bee0c80 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 27 Nov 2024 19:32:17 -0600 Subject: [PATCH 05/12] improved tsconfig strictness https://www.totaltypescript.com/tsconfig-cheat-sheet --- .../lib/features/events/components/views/Request.svelte | 2 +- .../ClientApp/src/lib/features/events/models/index.ts | 4 ++-- .../features/shared/components/DateRangeDropdown.svelte | 5 +++-- .../features/shared/components/filters/filters.svelte.ts | 2 +- .../ClientApp/src/lib/features/users/api.svelte.ts | 2 +- .../ClientApp/src/routes/(app)/account/manage/+page.svelte | 4 ++-- src/Exceptionless.Web/ClientApp/tsconfig.json | 7 +++++-- src/Exceptionless.Web/ClientApp/vite.config.ts | 2 +- 8 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/Request.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/Request.svelte index dd5ed9211..39178f8c2 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/Request.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/Request.svelte @@ -39,7 +39,7 @@ .sort() .reduce( (acc, key) => { - acc[key] = request.headers?.[key].join(',') ?? ''; + acc[key] = request.headers?.[key]?.join(',') ?? ''; return acc; }, {} as Record diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/index.ts index a8f842483..15593f2d2 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/index.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/index.ts @@ -53,6 +53,6 @@ export interface SubmissionClient { } export class PersistentEvent extends PersistentEventBase { - @IsOptional() @ValidateNested() data?: IPersistentEventData = undefined; - @IsOptional() type?: PersistentEventKnownTypes = undefined; + @IsOptional() @ValidateNested() override data?: IPersistentEventData = undefined; + @IsOptional() override type?: PersistentEventKnownTypes = undefined; } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/DateRangeDropdown.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/DateRangeDropdown.svelte index a54cbe47f..03fd4a21a 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/DateRangeDropdown.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/DateRangeDropdown.svelte @@ -7,7 +7,8 @@ let { value = $bindable() }: Props = $props(); - const items = [ + type Item = { label: string; value: string }; + const items: Item[] = [ { label: 'Last Hour', value: 'last hour' }, { label: 'Last 24 Hours', value: 'last 24 hours' }, { label: 'Last Week', value: 'last week' }, @@ -15,7 +16,7 @@ { label: 'All Time', value: '' } ]; - let selected = $derived(items.find((item) => item.value === value) || items[items.length - 1]); + let selected = $derived((items.find((item) => item.value === value) || items[items.length - 1]) as Item); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts index 8528903b2..43f205ab7 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/filters/filters.svelte.ts @@ -612,7 +612,7 @@ export function processFilterRules(filters: IFilter[], changed?: IFilter): IFilt const filtered: IFilter[] = []; Object.entries(groupedFilters).forEach(([, items]) => { if (items && items.length > 0) { - filtered.push(items[0]); + filtered.push(items[0] as IFilter); } }); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts index 910ad9248..c6a2e24c0 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts @@ -50,7 +50,7 @@ export function mutateEmailAddress(props: UpdateEmailAddressProps) { }, mutationKey: queryKeys.idEmailAddress(props.id), onSuccess: (data, variables) => { - const partialUserData: User = { email_address: variables.email_address, is_email_address_verified: data.is_verified }; + const partialUserData: Partial = { email_address: variables.email_address, is_email_address_verified: data.is_verified }; const user = queryClient.getQueryData(queryKeys.id(props.id)); if (user) { diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte index 2bcd1c581..ceb08a68a 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/manage/+page.svelte @@ -31,7 +31,7 @@ } }); - const updateEmailAddressForm = superForm(defaults(userResponse.data ?? {}, classvalidatorClient(User)), { + const updateEmailAddressForm = superForm(defaults(userResponse.data ?? new User(), classvalidatorClient(User)), { dataType: 'json', id: 'update-email-address', async onUpdate({ form, result }) { @@ -58,7 +58,7 @@ validators: classvalidatorClient(User) }); - const updateUserForm = superForm(defaults(userResponse.data ?? {}, classvalidatorClient(UpdateUser)), { + const updateUserForm = superForm(defaults(userResponse.data ?? new UpdateUser(), classvalidatorClient(UpdateUser)), { dataType: 'json', id: 'update-user', async onUpdate({ form, result }) { diff --git a/src/Exceptionless.Web/ClientApp/tsconfig.json b/src/Exceptionless.Web/ClientApp/tsconfig.json index 305f1e901..7ce50b4ea 100644 --- a/src/Exceptionless.Web/ClientApp/tsconfig.json +++ b/src/Exceptionless.Web/ClientApp/tsconfig.json @@ -7,11 +7,14 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "forceConsistentCasingInFileNames": true, + "moduleDetection": "force", + "moduleResolution": "bundler", + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" + "strict": true } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files diff --git a/src/Exceptionless.Web/ClientApp/vite.config.ts b/src/Exceptionless.Web/ClientApp/vite.config.ts index 34e8c47f5..f366b5d31 100644 --- a/src/Exceptionless.Web/ClientApp/vite.config.ts +++ b/src/Exceptionless.Web/ClientApp/vite.config.ts @@ -87,7 +87,7 @@ function getAspNetConfig() { if (aspnetHttpsPort) { url = `https://localhost:${aspnetHttpsPort}`; } else if (aspnetUrls) { - url = aspnetUrls.split(';')[0]; + url = aspnetUrls.split(';')[0] as string; } const wsUrl = url.replace('https://', 'wss://').replace('http://', 'ws://'); From 2040445bcec73bd1fa6f4150e0d743ec4ee67468 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 27 Nov 2024 19:32:39 -0600 Subject: [PATCH 06/12] invalidate all queries when web sockets reconnect --- src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte index 96656e7e7..652ded51c 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte @@ -102,6 +102,7 @@ ws.onMessage = onMessage; ws.onOpen = (_, isReconnect) => { if (isReconnect) { + queryClient.invalidateQueries(); document.dispatchEvent( new CustomEvent('refresh', { bubbles: true, From 0d7c3af040a93fc4ed8d6a149cc97a8b33502974 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 27 Nov 2024 20:34:55 -0600 Subject: [PATCH 07/12] Improved ws query invalidation --- .../src/lib/features/events/api.svelte.ts | 19 ++++++++++++- .../lib/features/organizations/api.svelte.ts | 12 +++++++++ .../src/lib/features/projects/api.svelte.ts | 19 ++++++++++++- .../src/lib/features/stacks/api.svelte.ts | 14 +++++++++- .../src/lib/features/users/api.svelte.ts | 18 ++++++++++++- .../ClientApp/src/routes/(app)/+layout.svelte | 27 +++++++++++++++++-- 6 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts index e6c511272..c0a0c5062 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts @@ -1,9 +1,26 @@ +import type { WebSocketMessageValue } from '$features/websockets/models'; + import { accessToken } from '$features/auth/index.svelte'; import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; -import { createQuery, useQueryClient } from '@tanstack/svelte-query'; +import { createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query'; import type { PersistentEvent } from './models'; +export async function invalidatePersistentEventQueries(queryClient: QueryClient, message: WebSocketMessageValue<'PersistentEventChanged'>) { + const { id, stack_id } = message; + if (id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }); + } + + if (stack_id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.stacks(stack_id) }); + } + + if (!id && !stack_id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + } +} + export const queryKeys = { all: ['PersistentEvent'] as const, allWithFilters: (filters: string) => [...queryKeys.all, { filters }] as const, diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts index 50316750d..02424cdcb 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts @@ -1,9 +1,21 @@ +import type { WebSocketMessageValue } from '$features/websockets/models'; +import type { QueryClient } from '@tanstack/svelte-query'; + import { accessToken } from '$features/auth/index.svelte'; import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; import { createQuery, useQueryClient } from '@tanstack/svelte-query'; import type { ViewOrganization } from './models'; +export async function invalidateOrganizationQueries(queryClient: QueryClient, message: WebSocketMessageValue<'OrganizationChanged'>) { + const { id } = message; + if (id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }); + } else { + await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + } +} + export const queryKeys = { all: ['Organization'] as const, allWithMode: (mode: 'stats' | undefined) => [...queryKeys.all, { mode }] as const, diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts index 4dff69df2..084e1c929 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts @@ -1,9 +1,26 @@ +import type { WebSocketMessageValue } from '$features/websockets/models'; + import { accessToken } from '$features/auth/index.svelte'; import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; -import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'; +import { createMutation, createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query'; import type { ViewProject } from './models'; +export async function invalidateProjectQueries(queryClient: QueryClient, message: WebSocketMessageValue<'ProjectChanged'>) { + const { id, organization_id } = message; + if (id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }); + } + + if (organization_id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.organization(organization_id) }); + } + + if (!id && !organization_id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + } +} + export const queryKeys = { all: ['Project'] as const, allWithFilters: (filters: string) => [...queryKeys.all, { filters }] as const, diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts index 883c5adb1..52f373e09 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts @@ -1,9 +1,21 @@ +import type { WebSocketMessageValue } from '$features/websockets/models'; + import { accessToken } from '$features/auth/index.svelte'; import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; -import { createQuery, useQueryClient } from '@tanstack/svelte-query'; +import { createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query'; import type { Stack } from './models'; +// +export async function invalidateStackQueries(queryClient: QueryClient, message: WebSocketMessageValue<'StackChanged'>) { + const { id } = message; + if (id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }); + } else { + await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + } +} + export const queryKeys = { all: ['Stack'] as const, allWithFilters: (filters: string) => [...queryKeys.all, { filters }] as const, diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts index c6a2e24c0..79426269e 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts @@ -1,9 +1,25 @@ +import type { WebSocketMessageValue } from '$features/websockets/models'; + import { accessToken } from '$features/auth/index.svelte'; import { ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; -import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'; +import { createMutation, createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query'; import { UpdateEmailAddressResult, type UpdateUser, User } from './models'; +export async function invalidateUserQueries(queryClient: QueryClient, message: WebSocketMessageValue<'UserChanged'>) { + const { id } = message; + if (id) { + await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }); + + const currentUser = queryClient.getQueryData(queryKeys.me()); + if (currentUser?.id === id) { + queryClient.invalidateQueries({ queryKey: queryKeys.me() }); + } + } else { + await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + } +} + export const queryKeys = { all: ['User'] as const, id: (id: string | undefined) => [...queryKeys.all, id] as const, diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte index 652ded51c..368c7f2a0 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte @@ -4,7 +4,11 @@ import { page } from '$app/stores'; import { useSidebar } from '$comp/ui/sidebar'; import { accessToken, gotoLogin } from '$features/auth/index.svelte'; - import { getMeQuery } from '$features/users/api.svelte'; + import { invalidatePersistentEventQueries } from '$features/events/api.svelte'; + import { invalidateOrganizationQueries } from '$features/organizations/api.svelte'; + import { invalidateProjectQueries } from '$features/projects/api.svelte'; + import { invalidateStackQueries } from '$features/stacks/api.svelte'; + import { getMeQuery, invalidateUserQueries } from '$features/users/api.svelte'; import { isEntityChangedType, type WebSocketMessageType } from '$features/websockets/models'; import { WebSocketClient } from '$features/websockets/WebSocketClient.svelte'; import { validate } from '$shared/validation'; @@ -63,7 +67,26 @@ ); if (isEntityChangedType(data)) { - await queryClient.invalidateQueries({ queryKey: [data.message.type] }); + switch (data.type) { + case 'OrganizationChanged': + await invalidateOrganizationQueries(queryClient, data.message); + break; + case 'PersistentEventChanged': + await invalidatePersistentEventQueries(queryClient, data.message); + break; + case 'ProjectChanged': + await invalidateProjectQueries(queryClient, data.message); + break; + case 'StackChanged': + await invalidateStackQueries(queryClient, data.message); + break; + case 'UserChanged': + await invalidateUserQueries(queryClient, data.message); + break; + default: + await queryClient.invalidateQueries({ queryKey: [data.message.type] }); + break; + } } // This event is fired when a user is added or removed from an organization. From 68c418cd5e4bfcb017a34d9a1cce7a23bc08dc04 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 27 Nov 2024 20:35:14 -0600 Subject: [PATCH 08/12] Fixed svelte runes issue with getting error data. --- .../ClientApp/src/lib/features/events/persistent-event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/persistent-event.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/persistent-event.ts index 9c5e48e8d..134a99056 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/persistent-event.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/persistent-event.ts @@ -27,7 +27,7 @@ export function getErrorData(event: PersistentEvent): ErrorData[] { return; } - const additionalData = error.data['@ext'] || {}; + const additionalData = { ...error.data['@ext'] }; Object.entries(error.data).forEach(([key, value]) => { if (!key.startsWith('@')) { additionalData[key] = value; From 8812fb707599f7f508c073488e4ad3ad3329cf42 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Thu, 28 Nov 2024 09:55:56 -0600 Subject: [PATCH 09/12] Improved cache invalidation --- .../ClientApp/src/lib/features/events/api.svelte.ts | 10 ++++------ .../src/lib/features/organizations/api.svelte.ts | 13 ++++++++----- .../src/lib/features/projects/api.svelte.ts | 10 ++++------ .../ClientApp/src/lib/features/stacks/api.svelte.ts | 7 +++---- .../ClientApp/src/lib/features/users/api.svelte.ts | 8 ++++---- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts index c0a0c5062..0d8478066 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts @@ -17,16 +17,14 @@ export async function invalidatePersistentEventQueries(queryClient: QueryClient, } if (!id && !stack_id) { - await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + await queryClient.invalidateQueries({ queryKey: queryKeys.type }); } } export const queryKeys = { - all: ['PersistentEvent'] as const, - allWithFilters: (filters: string) => [...queryKeys.all, { filters }] as const, - id: (id: string | undefined) => [...queryKeys.all, id] as const, - stacks: (id: string | undefined) => [...queryKeys.all, 'stacks', id] as const, - stackWithFilters: (id: string | undefined, filters: string) => [...queryKeys.stacks(id), { filters }] as const + id: (id: string | undefined) => [...queryKeys.type, id] as const, + stacks: (id: string | undefined) => [...queryKeys.type, 'stacks', id] as const, + type: ['PersistentEvent'] as const }; export interface GetEventByIdProps { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts index 02424cdcb..5cd8dfded 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts @@ -11,15 +11,18 @@ export async function invalidateOrganizationQueries(queryClient: QueryClient, me const { id } = message; if (id) { await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }); + + // Invalidate regardless of mode + await queryClient.invalidateQueries({ queryKey: queryKeys.list(undefined) }); } else { - await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + await queryClient.invalidateQueries({ queryKey: queryKeys.type }); } } export const queryKeys = { - all: ['Organization'] as const, - allWithMode: (mode: 'stats' | undefined) => [...queryKeys.all, { mode }] as const, - id: (id: string | undefined) => [...queryKeys.all, id] as const + id: (id: string | undefined) => [...queryKeys.type, id] as const, + list: (mode: 'stats' | undefined) => (mode ? ([...queryKeys.type, 'list', { mode }] as const) : ([...queryKeys.type, 'list'] as const)), + type: ['Organization'] as const }; export interface GetOrganizationsProps { @@ -48,6 +51,6 @@ export function getOrganizationQuery(props: GetOrganizationsProps) { return response.data!; }, - queryKey: props.mode ? queryKeys.allWithMode(props.mode) : queryKeys.all + queryKey: queryKeys.list(props.mode ?? undefined) })); } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts index 084e1c929..ad4f49781 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts @@ -17,16 +17,14 @@ export async function invalidateProjectQueries(queryClient: QueryClient, message } if (!id && !organization_id) { - await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + await queryClient.invalidateQueries({ queryKey: queryKeys.type }); } } export const queryKeys = { - all: ['Project'] as const, - allWithFilters: (filters: string) => [...queryKeys.all, { filters }] as const, - id: (id: string | undefined) => [...queryKeys.all, id] as const, - organization: (id: string | undefined) => [...queryKeys.all, 'organization', id] as const, - organizationWithFilters: (id: string | undefined, filters: string) => [...queryKeys.organization(id), { filters }] as const + id: (id: string | undefined) => [...queryKeys.type, id] as const, + organization: (id: string | undefined) => [...queryKeys.type, 'organization', id] as const, + type: ['Project'] as const }; export interface DemoteProjectTabProps { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts index 52f373e09..90717052e 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/stacks/api.svelte.ts @@ -12,14 +12,13 @@ export async function invalidateStackQueries(queryClient: QueryClient, message: if (id) { await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }); } else { - await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + await queryClient.invalidateQueries({ queryKey: queryKeys.type }); } } export const queryKeys = { - all: ['Stack'] as const, - allWithFilters: (filters: string) => [...queryKeys.all, { filters }] as const, - id: (id: string | undefined) => [...queryKeys.all, id] as const + id: (id: string | undefined) => [...queryKeys.type, id] as const, + type: ['Stack'] as const }; export interface GetStackByIdProps { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts index 79426269e..f867d116a 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/users/api.svelte.ts @@ -16,15 +16,15 @@ export async function invalidateUserQueries(queryClient: QueryClient, message: W queryClient.invalidateQueries({ queryKey: queryKeys.me() }); } } else { - await queryClient.invalidateQueries({ queryKey: queryKeys.all }); + await queryClient.invalidateQueries({ queryKey: queryKeys.type }); } } export const queryKeys = { - all: ['User'] as const, - id: (id: string | undefined) => [...queryKeys.all, id] as const, + id: (id: string | undefined) => [...queryKeys.type, id] as const, idEmailAddress: (id?: string) => [...queryKeys.id(id), 'email-address'] as const, - me: () => [...queryKeys.all, 'me'] as const + me: () => [...queryKeys.type, 'me'] as const, + type: ['User'] as const }; export interface UpdateEmailAddressProps { From de670c0b452f244fc473ce7c5161eb78edcaa65e Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Thu, 28 Nov 2024 09:56:32 -0600 Subject: [PATCH 10/12] Simplified plan cancellation. --- .../billing/change-plan-dialog.tpl.html | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp.angular/components/billing/change-plan-dialog.tpl.html b/src/Exceptionless.Web/ClientApp.angular/components/billing/change-plan-dialog.tpl.html index 1be2f1afc..a72b9db0d 100644 --- a/src/Exceptionless.Web/ClientApp.angular/components/billing/change-plan-dialog.tpl.html +++ b/src/Exceptionless.Web/ClientApp.angular/components/billing/change-plan-dialog.tpl.html @@ -184,21 +184,14 @@

{{::'Credit Card' | translate}}

- - - - - - +
From 047163c33c6c529455ca772ebf1b43ad4ff99132 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Thu, 28 Nov 2024 10:03:53 -0600 Subject: [PATCH 11/12] Updated deps and fixed lock file --- .../ClientApp/package-lock.json | 415 +++++++++--------- src/Exceptionless.Web/ClientApp/package.json | 24 +- 2 files changed, 230 insertions(+), 209 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/package-lock.json b/src/Exceptionless.Web/ClientApp/package-lock.json index 6099924fc..eb6f56752 100644 --- a/src/Exceptionless.Web/ClientApp/package-lock.json +++ b/src/Exceptionless.Web/ClientApp/package-lock.json @@ -11,18 +11,18 @@ "@exceptionless/browser": "^3.1.0", "@exceptionless/fetchclient": "^0.29.0", "@iconify-json/mdi": "^1.2.1", - "@tanstack/svelte-query": "https://pkg.pr.new/@tanstack/svelte-query@ccce0b8", - "@tanstack/svelte-query-devtools": "https://pkg.pr.new/@tanstack/svelte-query-devtools@ccce0b8", + "@tanstack/svelte-query": "https://pkg.pr.new/@tanstack/svelte-query@28f98f9", + "@tanstack/svelte-query-devtools": "https://pkg.pr.new/@tanstack/svelte-query-devtools@28f98f9", "@tanstack/svelte-table": "^9.0.0-alpha.10", "@typeschema/class-validator": "^0.3.0", - "bits-ui": "^1.0.0-next.63", + "bits-ui": "^1.0.0-next.64", "class-validator": "^0.14.1", "clsx": "^2.1.1", "formsnap": "^2.0.0-next.1", "mode-watcher": "^0.5.0", "oidc-client-ts": "^3.1.0", "pretty-ms": "^9.2.0", - "runed": "^0.15.3", + "runed": "^0.15.4", "svelte-sonner": "^0.3.28", "svelte-time": "^0.9.0", "sveltekit-superforms": "^2.20.1", @@ -31,36 +31,36 @@ "tailwindcss": "^3.4.15", "tailwindcss-animate": "^1.0.7", "throttle-debounce": "^5.0.2", - "unplugin-icons": "^0.20.1" + "unplugin-icons": "^0.20.2" }, "devDependencies": { - "@iconify-json/lucide": "^1.2.16", + "@iconify-json/lucide": "^1.2.17", "@playwright/test": "^1.49.0", "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.8.3", + "@sveltejs/kit": "^2.8.5", "@sveltejs/vite-plugin-svelte": "^4.0.2", "@types/eslint": "^9.6.1", - "@types/node": "^22.9.3", + "@types/node": "^22.10.1", "@types/throttle-debounce": "^5.0.2", "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-perfectionist": "^4.0.3", + "eslint-plugin-perfectionist": "^4.1.2", "eslint-plugin-svelte": "^2.46.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.49", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "prettier-plugin-svelte": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.9", - "svelte": "^5.2.7", + "svelte": "^5.2.10", "svelte-check": "^4.1.0", "swagger-typescript-api": "^13.0.23", "tslib": "^2.8.1", "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0", + "typescript-eslint": "^8.16.0", "vite": "^5.4.11", - "vitest": "^2.1.5" + "vitest": "^2.1.6" } }, "node_modules/@alloc/quick-lru": { @@ -938,9 +938,9 @@ } }, "node_modules/@iconify-json/lucide": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.16.tgz", - "integrity": "sha512-3uZ6/zjAMbAH44Xdfn3ZI+ATKIYK9wuQ0KYVkXYuh3DDU6NZMGq3bCzr9z3/jjAfb3d8AMMAxAzkHSjLGKcq1w==", + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.17.tgz", + "integrity": "sha512-y+4P1DxD2h4d4fGYxikUdMf0o21DD0GIE/YIgixEBIXKbE90LTOFqmoxkGyPpaGk3vT2qE2w/28+sdmBMFsd5w==", "dev": true, "license": "ISC", "dependencies": { @@ -1378,9 +1378,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.3.tgz", - "integrity": "sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.5.tgz", + "integrity": "sha512-5ry1jPd4r9knsphDK2eTYUFPhFZMqF0PHFfa8MdMQCqWaKwLSXdFMU/Vevih1I7C1/VNB5MvTuFl1kXu5vx8UA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1457,9 +1457,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.56.2", - "resolved": "https://pkg.pr.new/TanStack/query/@tanstack/query-core@ccce0b8", - "integrity": "sha512-FZaSePOhtv2t+mvxsDNrDbAHEJsRTUVuxKEQnMB4o/pdK0mFPcUyqZtZ3hRxc1H6NLu23GYhijBECGXCd+8+Sg==", + "version": "5.59.17", + "resolved": "https://pkg.pr.new/TanStack/query/@tanstack/query-core@28f98f9", + "integrity": "sha512-J4DA5i5gsX/DF1A/0E/w9iXiRIXVLFVXrrhJ1AE4lkr9FMXicqCcg9uLL4sKtOhAyXOnnNdIWv1gitwYqIyJGQ==", "license": "MIT", "funding": { "type": "github", @@ -1467,9 +1467,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.58.0", - "resolved": "https://pkg.pr.new/TanStack/query/@tanstack/query-devtools@ccce0b8", - "integrity": "sha512-wNrf2vt2SkGfUOW8ry4ONN/eqnZwqoeXbPrDiIxe3yjHa+sHK4lcNoGJO62i16igKD7j2xOUxASWrKUadcxQkw==", + "version": "5.59.19", + "resolved": "https://pkg.pr.new/TanStack/query/@tanstack/query-devtools@28f98f9", + "integrity": "sha512-zVNNUroHuD6imCVUglg6OHD4Z7x9zCyhO5eWXjbsqlGaTuDWxxDcSDJjT9tUg7YI4i9QACojE5f0a/CuMWV4dQ==", "license": "MIT", "funding": { "type": "github", @@ -1477,28 +1477,28 @@ } }, "node_modules/@tanstack/svelte-query": { - "version": "5.56.2", - "resolved": "https://pkg.pr.new/@tanstack/svelte-query@ccce0b8", - "integrity": "sha512-EuwFCwGe3DNkHZGJ9pi2YefWYgMKYguCX/CWrwaT5O/NOg6ryrItZhf4AUUc0UTdQnkOWhsUlvT2LtrmvQscCg==", + "version": "5.59.17", + "resolved": "https://pkg.pr.new/@tanstack/svelte-query@28f98f9", + "integrity": "sha512-E4WfeFyrBSXRmbn6Al1+lMQJrteEqIoSFDKVCQ8n/WD/PVMQp7VtYXTaLhXxFStTyDa61dmN80s1TdlnqDzLXg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "https://pkg.pr.new/TanStack/query/@tanstack/query-core@ccce0b8" + "@tanstack/query-core": "https://pkg.pr.new/TanStack/query/@tanstack/query-core@28f98f9" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "svelte": "^5.0.0-next.259" + "svelte": "^5.0.0" } }, "node_modules/@tanstack/svelte-query-devtools": { - "version": "5.58.0", - "resolved": "https://pkg.pr.new/@tanstack/svelte-query-devtools@ccce0b8", - "integrity": "sha512-JNtk/5ZlQ7pTQRq4EHCoq2qbSnh6rPHGxYjYC4txlkofopXAJoc+UjW5dZOJrvQVFPtFmw2BPkmb78GgYc7qzg==", + "version": "5.59.19", + "resolved": "https://pkg.pr.new/@tanstack/svelte-query-devtools@28f98f9", + "integrity": "sha512-yaRHSXXABVFhgODkIYmmPkciIXkj7+Ma3MtOPC4xA8tfOxPuFN5B7mLncF8UOLBLRWUYuC9/bd4pDp2/AaNaBA==", "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "https://pkg.pr.new/TanStack/query/@tanstack/query-devtools@ccce0b8", + "@tanstack/query-devtools": "https://pkg.pr.new/TanStack/query/@tanstack/query-devtools@28f98f9", "esm-env": "^1.0.0" }, "funding": { @@ -1506,8 +1506,8 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/svelte-query": "^5.56.2", - "svelte": "^5.0.0-next.259" + "@tanstack/svelte-query": "^5.59.17", + "svelte": "^5.0.0" } }, "node_modules/@tanstack/svelte-table": { @@ -1573,13 +1573,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz", - "integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/swagger-schema-official": { @@ -1634,17 +1634,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", - "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1668,16 +1668,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", - "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4" }, "engines": { @@ -1697,14 +1697,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1715,14 +1715,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", - "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1743,9 +1743,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", - "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", "dev": true, "license": "MIT", "engines": { @@ -1757,14 +1757,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", - "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1812,16 +1812,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", - "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0" + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1840,13 +1840,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", - "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/types": "8.16.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1888,14 +1888,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz", - "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.6.tgz", + "integrity": "sha512-9M1UR9CAmrhJOMoSwVnPh2rELPKhYo0m/CSgqw9PyStpxtkwhmdM6XYlXGKeYyERY1N6EIuzkQ7e3Lm1WKCoUg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.5", - "@vitest/utils": "2.1.5", + "@vitest/spy": "2.1.6", + "@vitest/utils": "2.1.6", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, @@ -1904,13 +1904,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz", - "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.6.tgz", + "integrity": "sha512-MHZp2Z+Q/A3am5oD4WSH04f9B0T7UvwEb+v5W0kCYMhtXGYbdyl2NUk1wdSMqGthmhpiThPDp/hEoVwu16+u1A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.5", + "@vitest/spy": "2.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, @@ -1919,7 +1919,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1931,9 +1931,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", - "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.6.tgz", + "integrity": "sha512-exZyLcEnHgDMKc54TtHca4McV4sKT+NKAe9ix/yhd/qkYb/TP8HTyXRFDijV19qKqTZM0hPL4753zU/U8L/gAA==", "dev": true, "license": "MIT", "dependencies": { @@ -1944,13 +1944,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz", - "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.6.tgz", + "integrity": "sha512-SjkRGSFyrA82m5nz7To4CkRSEVWn/rwQISHoia/DB8c6IHIhaE/UNAo+7UfeaeJRE979XceGl00LNkIz09RFsA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.5", + "@vitest/utils": "2.1.6", "pathe": "^1.1.2" }, "funding": { @@ -1958,13 +1958,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz", - "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.6.tgz", + "integrity": "sha512-5JTWHw8iS9l3v4/VSuthCndw1lN/hpPB+mlgn1BUhFbobeIUj1J1V/Bj2t2ovGEmkXLTckFjQddsxS5T6LuVWw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.5", + "@vitest/pretty-format": "2.1.6", "magic-string": "^0.30.12", "pathe": "^1.1.2" }, @@ -1973,9 +1973,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz", - "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.6.tgz", + "integrity": "sha512-oTFObV8bd4SDdRka5O+mSh5w9irgx5IetrD5i+OsUUsk/shsBoHifwCzy45SAORzAhtNiprUVaK3hSCCzZh1jQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1986,13 +1986,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz", - "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.6.tgz", + "integrity": "sha512-ixNkFy3k4vokOUTU2blIUvOgKq/N2PW8vKIjZZYsGJCMX69MRa9J2sKqX5hY/k5O5Gty3YJChepkqZ3KM9LyIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.5", + "@vitest/pretty-format": "2.1.6", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, @@ -2259,9 +2259,9 @@ } }, "node_modules/bits-ui": { - "version": "1.0.0-next.63", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.63.tgz", - "integrity": "sha512-3z4+N+KudMK8AeBzhy/0568zVoEJCUgL4RkElB41BWGjofk68en2TaAfKFhhc/bn4z+uKrs9r1NtybDdsy0bpA==", + "version": "1.0.0-next.64", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.0.0-next.64.tgz", + "integrity": "sha512-r1JThjUSKwTkaB1onwIs7ZQoqygSsWhjBaUElCS8m8CCbY1RxmTz0HnbN+Xp2oJgJ4YQgIfiXTG3170l80FEgg==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.6.4", @@ -3276,14 +3276,14 @@ } }, "node_modules/eslint-plugin-perfectionist": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-4.0.3.tgz", - "integrity": "sha512-CyafnreF6boy4lf1XaF72U8NbkwrfjU/mOf1y6doaDMS9zGXhUU1DSk+ZPf/rVwCf1PL1m+rhHqFs+IcB8kDmA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-4.1.2.tgz", + "integrity": "sha512-YjXPWB/rKe/gPUsyuxw75wTUrzN5MuJnRV0PH9NoonFvgcdVIXk551mkBKPr59nRZCbu7S3dFHwfo4gA42DB2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "^8.15.0", - "@typescript-eslint/utils": "^8.15.0", + "@typescript-eslint/types": "^8.16.0", + "@typescript-eslint/utils": "^8.16.0", "natural-orderby": "^5.0.0" }, "engines": { @@ -3435,9 +3435,9 @@ } }, "node_modules/esm-env": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz", - "integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.0.tgz", + "integrity": "sha512-OhSQuHL3mUcaQHjGe8UMG8GsJIJHYYz0flR0h9fiTPNMupLMkb7TvcRD0EeJXW5a8GHBgfz08b6FDLNK7kkPQA==", "license": "MIT" }, "node_modules/espree": { @@ -4665,13 +4665,13 @@ } }, "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", "license": "MIT", "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" }, "engines": { "node": ">=14" @@ -4798,14 +4798,14 @@ } }, "node_modules/mlly": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", - "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", + "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", "license": "MIT", "dependencies": { - "acorn": "^8.12.1", + "acorn": "^8.14.0", "pathe": "^1.1.2", - "pkg-types": "^1.2.0", + "pkg-types": "^1.2.1", "ufo": "^1.5.4" } }, @@ -5364,9 +5364,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.2.tgz", - "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.5.tgz", + "integrity": "sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==", "license": "MIT" }, "node_modules/parent-module": { @@ -5792,9 +5792,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", + "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", "dev": true, "license": "MIT", "bin": { @@ -6155,9 +6155,9 @@ } }, "node_modules/runed": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/runed/-/runed-0.15.3.tgz", - "integrity": "sha512-HtayB+loDcGdqJDHW8JFdsZzGQMyVzim6+s3052MkjZnwmwDstmF+cusMeTssBe6TCdt5i9D/Tb+KYXN3L0kXA==", + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.15.4.tgz", + "integrity": "sha512-kmbpstUd7v2FdlBM+MT78IyuOVd38tq/e7MHvVb0fnVCsPSPMD/m2Xh+wUhzg9qCJgxRjBbIKu68DlH/x5VXJA==", "funding": [ "https://github.com/sponsors/huntabyte", "https://github.com/sponsors/tglide" @@ -6796,9 +6796,9 @@ } }, "node_modules/svelte": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.7.tgz", - "integrity": "sha512-cEhPGuLHiH2+Z8B1FwQgiZJgA39uUmJR4516TKrM5zrp0/cuwJkfhUfcTxhAkznanAF5fXUKzvYR4o+Ksx3ZCQ==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.10.tgz", + "integrity": "sha512-ON0OyO7vOmSjTc9mLjusu3vf1I7BvjovbiRB7j84F1WZMXV6dR+Tj4btIzxQxMHfzbGskaFmRa7qjgmBSVBnhQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -6808,7 +6808,7 @@ "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", - "esm-env": "^1.0.0", + "esm-env": "^1.2.0", "esrap": "^1.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", @@ -7066,6 +7066,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/swagger-typescript-api/node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/swagger-typescript-api/node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", @@ -7420,9 +7436,9 @@ "optional": true }, "node_modules/ts-api-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", - "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", + "integrity": "sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==", "dev": true, "license": "MIT", "engines": { @@ -7571,15 +7587,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.15.0.tgz", - "integrity": "sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", + "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.15.0", - "@typescript-eslint/parser": "8.15.0", - "@typescript-eslint/utils": "8.15.0" + "@typescript-eslint/eslint-plugin": "8.16.0", + "@typescript-eslint/parser": "8.16.0", + "@typescript-eslint/utils": "8.16.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7620,46 +7636,38 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "devOptional": true, "license": "MIT" }, "node_modules/unplugin": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.1.tgz", - "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz", + "integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==", "license": "MIT", "dependencies": { - "acorn": "^8.12.1", + "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" }, "engines": { "node": ">=14.0.0" - }, - "peerDependencies": { - "webpack-sources": "^3" - }, - "peerDependenciesMeta": { - "webpack-sources": { - "optional": true - } } }, "node_modules/unplugin-icons": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.20.1.tgz", - "integrity": "sha512-0z5sYGx07Q69ZrJB4kjmx7a5LYLNSWwyq95Ox9OuSG2y/sbhJaHUapRPOJcKmKhOAyToDVRdy9P7gxJ05lYipw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.20.2.tgz", + "integrity": "sha512-Ak6TKAiO812aIUrCelrBSTQbYC4FiqawnFrAusP/hjmB8f9cAug9jr381ItvLl+Asi4IVcjoOiPbpy9CfFGKvQ==", "license": "MIT", "dependencies": { - "@antfu/install-pkg": "^0.4.1", + "@antfu/install-pkg": "^0.5.0", "@antfu/utils": "^0.7.10", - "@iconify/utils": "^2.1.29", - "debug": "^4.3.6", + "@iconify/utils": "^2.1.33", + "debug": "^4.3.7", "kolorist": "^1.8.0", - "local-pkg": "^0.5.0", - "unplugin": "^1.12.0" + "local-pkg": "^0.5.1", + "unplugin": "^1.16.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -7693,6 +7701,19 @@ } } }, + "node_modules/unplugin-icons/node_modules/@antfu/install-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.5.0.tgz", + "integrity": "sha512-dKnk2xlAyC7rvTkpkHmu+Qy/2Zc3Vm/l8PtNyIOGDBtXPY3kThfU4ORNEp3V7SXw5XSOb+tOJaUYpfquPzL/Tg==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^0.2.5", + "tinyexec": "^0.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -7835,9 +7856,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz", - "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.6.tgz", + "integrity": "sha512-DBfJY0n9JUwnyLxPSSUmEePT21j8JZp/sR9n+/gBwQU6DcQOioPdb8/pibWfXForbirSagZCilseYIwaL3f95A==", "dev": true, "license": "MIT", "dependencies": { @@ -7845,13 +7866,13 @@ "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", - "vite": "^5.0.0" + "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -8296,19 +8317,19 @@ } }, "node_modules/vitest": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz", - "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.6.tgz", + "integrity": "sha512-isUCkvPL30J4c5O5hgONeFRsDmlw6kzFEdLQHLezmDdKQHy8Ke/B/dgdTMEgU0vm+iZ0TjW8GuK83DiahBoKWQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.5", - "@vitest/mocker": "2.1.5", - "@vitest/pretty-format": "^2.1.5", - "@vitest/runner": "2.1.5", - "@vitest/snapshot": "2.1.5", - "@vitest/spy": "2.1.5", - "@vitest/utils": "2.1.5", + "@vitest/expect": "2.1.6", + "@vitest/mocker": "2.1.6", + "@vitest/pretty-format": "^2.1.6", + "@vitest/runner": "2.1.6", + "@vitest/snapshot": "2.1.6", + "@vitest/spy": "2.1.6", + "@vitest/utils": "2.1.6", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", @@ -8319,24 +8340,24 @@ "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.5", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "2.1.6", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.5", - "@vitest/ui": "2.1.5", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "2.1.6", + "@vitest/ui": "2.1.6", "happy-dom": "*", "jsdom": "*" }, diff --git a/src/Exceptionless.Web/ClientApp/package.json b/src/Exceptionless.Web/ClientApp/package.json index fe91da501..a39dfeb99 100644 --- a/src/Exceptionless.Web/ClientApp/package.json +++ b/src/Exceptionless.Web/ClientApp/package.json @@ -23,40 +23,40 @@ "upgrade": "ncu -i" }, "devDependencies": { - "@iconify-json/lucide": "^1.2.16", + "@iconify-json/lucide": "^1.2.17", "@playwright/test": "^1.49.0", "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.8.3", + "@sveltejs/kit": "^2.8.5", "@sveltejs/vite-plugin-svelte": "^4.0.2", "@types/eslint": "^9.6.1", - "@types/node": "^22.9.3", + "@types/node": "^22.10.1", "@types/throttle-debounce": "^5.0.2", "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-perfectionist": "^4.0.3", + "eslint-plugin-perfectionist": "^4.1.2", "eslint-plugin-svelte": "^2.46.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.49", - "prettier": "^3.3.3", + "prettier": "^3.4.1", "prettier-plugin-svelte": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.9", - "svelte": "^5.2.7", + "svelte": "^5.2.10", "svelte-check": "^4.1.0", "swagger-typescript-api": "^13.0.23", "tslib": "^2.8.1", "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0", + "typescript-eslint": "^8.16.0", "vite": "^5.4.11", - "vitest": "^2.1.5" + "vitest": "^2.1.6" }, "dependencies": { "@exceptionless/browser": "^3.1.0", "@exceptionless/fetchclient": "^0.29.0", "@iconify-json/mdi": "^1.2.1", - "@tanstack/svelte-query": "https://pkg.pr.new/@tanstack/svelte-query@ccce0b8", - "@tanstack/svelte-query-devtools": "https://pkg.pr.new/@tanstack/svelte-query-devtools@ccce0b8", + "@tanstack/svelte-query": "https://pkg.pr.new/@tanstack/svelte-query@28f98f9", + "@tanstack/svelte-query-devtools": "https://pkg.pr.new/@tanstack/svelte-query-devtools@28f98f9", "@tanstack/svelte-table": "^9.0.0-alpha.10", "@typeschema/class-validator": "^0.3.0", "bits-ui": "^1.0.0-next.64", @@ -66,7 +66,7 @@ "mode-watcher": "^0.5.0", "oidc-client-ts": "^3.1.0", "pretty-ms": "^9.2.0", - "runed": "^0.15.3", + "runed": "^0.15.4", "svelte-sonner": "^0.3.28", "svelte-time": "^0.9.0", "sveltekit-superforms": "^2.20.1", @@ -75,7 +75,7 @@ "tailwindcss": "^3.4.15", "tailwindcss-animate": "^1.0.7", "throttle-debounce": "^5.0.2", - "unplugin-icons": "^0.20.1" + "unplugin-icons": "^0.20.2" }, "type": "module" } From af8cc53fff9da156a191ea96823f13b562800f42 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Thu, 28 Nov 2024 10:05:14 -0600 Subject: [PATCH 12/12] Fixed linting --- src/Exceptionless.Web/ClientApp/resources.md | 154 +++++++++---------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/resources.md b/src/Exceptionless.Web/ClientApp/resources.md index 223aab81c..2eb7bcfc1 100644 --- a/src/Exceptionless.Web/ClientApp/resources.md +++ b/src/Exceptionless.Web/ClientApp/resources.md @@ -4,116 +4,116 @@ ### TODO -- Investigate loading data in - export function load({ url, fetch }) {} +- Investigate loading data in - export function load({ url, fetch }) {} #### shadcn svelte upgrade -- on: -- slots +- on: +- slots #### sveltekit -- page.subscribe -- get( +- page.subscribe +- get( ## General -- -- -- -- +- +- +- +- ## Forms -- -- +- +- ## Data Tables -- -- -- -- -- -- -- -- -- -- -- +- +- +- +- +- +- +- +- +- +- +- ## State -- -- +- +- ## Authentication -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- ## Theming -- -- -- -- -- -- -- -- -- -- -- -- +- +- +- +- +- +- +- +- +- +- +- +- ## Storage -- +- ## i18n -- -- -- +- +- +- ## Validation -- -- -- -- -- +- +- +- +- +- ## Networking -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +-