+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/data-table/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/data-table/index.ts
index 33caf4941c..c907a941d5 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/data-table/index.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/data-table/index.ts
@@ -1,8 +1,6 @@
import Root from './data-table.svelte';
import Body from './data-table-body.svelte';
import Toolbar from './data-table-toolbar.svelte';
-import FacetedFilterContainer from './data-table-faceted-filter-container.svelte';
-import FacetedFilter from './data-table-faceted-filter.svelte';
import PageSize from './data-table-page-size.svelte';
import Pagination from './data-table-pagination.svelte';
@@ -10,16 +8,12 @@ export {
Root,
Body,
Toolbar,
- FacetedFilterContainer,
- FacetedFilter,
PageSize,
Pagination,
//
Root as DataTable,
Body as DataTableBody,
Toolbar as DataTableToolbar,
- FacetedFilterContainer as DataTableFacetedFilterContainer,
- FacetedFilter as DataTableFacetedFilter,
PageSize as DataTablePageSize,
Pagination as DataTablePagination
};
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/events/EventsDrawer.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/events/EventsDrawer.svelte
index 43de4fa582..d63628ae3c 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/events/EventsDrawer.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/events/EventsDrawer.svelte
@@ -21,6 +21,7 @@
import * as Table from '$comp/ui/table';
import * as Tabs from '$comp/ui/tabs';
import P from '$comp/typography/P.svelte';
+ import ClickableProjectFilter from '$comp/filters/ClickableProjectFilter.svelte';
export let id: string;
@@ -136,7 +137,9 @@
Project
{$projectResponse.data.name}{$projectResponse.data.name}
{/if}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsDataTable.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsDataTable.svelte
index 52d9e8c1a9..cac5f2d721 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsDataTable.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsDataTable.svelte
@@ -10,11 +10,10 @@
import * as DataTable from '$comp/data-table';
import { getOptions } from './options';
import { DEFAULT_LIMIT } from '$lib/helpers/api';
- import SearchInput from '$comp/SearchInput.svelte';
- import { limit, onFilterInputChanged } from '$lib/stores/events';
export let filter: Readable
;
export let pageFilter: string | undefined = undefined;
+ export let limit: Readable;
export let time: Readable;
export let mode: GetEventsMode = 'summary';
@@ -63,9 +62,7 @@
-
-
-
+
dispatch('rowclick', event.detail)}>
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsTailLogDataTable.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsTailLogDataTable.svelte
index 30f925bfb4..3df27ca97d 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsTailLogDataTable.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/EventsTailLogDataTable.svelte
@@ -11,11 +11,10 @@
import CustomEventMessage from '$comp/messaging/CustomEventMessage.svelte';
import Muted from '$comp/typography/Muted.svelte';
import { getOptions } from './options';
- import SearchInput from '$comp/SearchInput.svelte';
- import { limit, onFilterInputChanged } from '$lib/stores/events';
import { DEFAULT_LIMIT } from '$lib/helpers/api';
export let filter: Readable;
+ export let limit: Readable;
const parameters = writable({ mode: 'summary', limit: $limit });
const options = getOptions>(parameters, (options) => ({
@@ -93,9 +92,7 @@
-
-
-
+
dispatch('rowclick', event.detail)}>
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/options.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/options.ts
index d4fbea9a13..07021c5815 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/options.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/events/table/options.ts
@@ -9,7 +9,7 @@ import {
renderComponent,
type RowSelectionState
} from '@tanstack/svelte-table';
-import { persisted } from 'svelte-local-storage-store';
+import { persisted } from 'svelte-persisted-store';
import { get, writable, type Writable } from 'svelte/store';
import type { EventSummaryModel, GetEventsMode, IGetEventsParams, StackSummaryModel, SummaryModel, SummaryTemplateKeys } from '$lib/models/api';
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/events/views/Overview.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/events/views/Overview.svelte
index 342a9f81ad..9241cba105 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/events/views/Overview.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/events/views/Overview.svelte
@@ -27,6 +27,7 @@
import { Button } from '$comp/ui/button';
import H4 from '$comp/typography/H4.svelte';
import A from '$comp/typography/A.svelte';
+ import ClickableTypeFilter from '$comp/filters/ClickableTypeFilter.svelte';
export let event: PersistentEvent;
@@ -97,9 +98,9 @@
Reference
{#if isSessionStart}
- {event.reference_id}
+ {event.reference_id}
{:else}
- {event.reference_id}
+ {event.reference_id}
{/if}
@@ -107,7 +108,7 @@
{#each references as reference (reference.id)}
{reference.name}
- {reference.id}
+ {reference.id}
{/each}
{#if level}
@@ -119,7 +120,7 @@
{#if event.type !== 'error'}
Event Type
- {event.type}
+ {event.type}
{/if}
{#if hasError}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-actions.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-actions.svelte
new file mode 100644
index 0000000000..9089a5fecd
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-actions.svelte
@@ -0,0 +1,24 @@
+
+
+
+
+
+ {#if showApply}
+ dispatch('apply')}>Apply filter
+
+ {/if}
+ {#if showClear}
+ dispatch('clear')}>Clear filter
+ {/if}
+ dispatch('remove')}>Remove filter
+ dispatch('close')}>Close
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-loading.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-loading.svelte
new file mode 100644
index 0000000000..2e2593e786
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-loading.svelte
@@ -0,0 +1,8 @@
+
+
+
+ Loading
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-value.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-value.svelte
new file mode 100644
index 0000000000..9c2fda156a
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-value.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-values.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-values.svelte
new file mode 100644
index 0000000000..7b970274ea
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-badge-values.svelte
@@ -0,0 +1,24 @@
+
+
+{#if values.length > 0}
+
+ {values.length}
+
+
+ {#if values.length > 2}
+
+ {values.length} Selected
+
+ {:else}
+ {#each values as value (value)}
+
+
+
+ {/each}
+ {/if}
+
+{/if}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-builder.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-builder.svelte
new file mode 100644
index 0000000000..04102b3748
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/faceted-filter-builder.svelte
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+ No results found.
+
+ {#each $facets as facet (facet.filter.key)}
+ onFacetSelected(facet)}>{facet.title}
+ {/each}
+
+
+
+
+
+
+ {#if visible.length > 0}
+ Clear filters
+ {/if}
+ Close
+
+
+
+
+
+{#each $facets as facet (facet.filter.key)}
+ {#if visible.includes(facet.filter.key)}
+
+ {/if}
+{/each}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/index.ts
new file mode 100644
index 0000000000..cf0cfba87b
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/faceted-filter/index.ts
@@ -0,0 +1,27 @@
+import type { ComponentType } from 'svelte';
+import type { Writable } from 'svelte/store';
+
+import type { IFilter } from '$comp/filters/filters';
+
+import Root from './faceted-filter-builder.svelte';
+import Actions from './faceted-filter-actions.svelte';
+
+import BadgeLoading from './faceted-filter-badge-loading.svelte';
+import BadgeValue from './faceted-filter-badge-value.svelte';
+import BadgeValues from './faceted-filter-badge-values.svelte';
+
+export type FacetedFilter = { title: string; component: ComponentType; filter: IFilter; open: Writable };
+
+export {
+ Root,
+ Actions,
+ BadgeLoading,
+ BadgeValue,
+ BadgeValues,
+ //
+ Root as FacetedFilterBuilder,
+ Actions as FacetedFilterActions,
+ BadgeLoading as FacetedFilterBadgeLoading,
+ BadgeValue as FacetedFilterBadgeValue,
+ BadgeValues as FacetedFilterBadgeValues
+};
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableOrganizationFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableOrganizationFilter.svelte
new file mode 100644
index 0000000000..0d2c9d0408
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableOrganizationFilter.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableProjectFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableProjectFilter.svelte
new file mode 100644
index 0000000000..81df1b0fc5
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableProjectFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableReferenceFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableReferenceFilter.svelte
index ae14db186d..682da0c18d 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableReferenceFilter.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableReferenceFilter.svelte
@@ -2,15 +2,15 @@
import A from '$comp/typography/A.svelte';
import { ReferenceFilter } from './filters';
- export let referenceId: string;
+ export let value: string;
- const title = `Search reference:${referenceId}`;
+ const title = `Search reference:${value}`;
function onSearchClick(e: Event) {
e.preventDefault();
document.dispatchEvent(
new CustomEvent('filter', {
- detail: new ReferenceFilter(referenceId)
+ detail: new ReferenceFilter(value)
})
);
}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableSessionFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableSessionFilter.svelte
index debfa8e89a..53b9d0c25b 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableSessionFilter.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableSessionFilter.svelte
@@ -2,15 +2,15 @@
import A from '$comp/typography/A.svelte';
import { SessionFilter } from './filters';
- export let sessionId: string;
+ export let value: string;
- const title = `Search ref.session:${sessionId}`;
+ const title = `Search ref.session:${value}`;
function onSearchClick(e: Event) {
e.preventDefault();
document.dispatchEvent(
new CustomEvent('filter', {
- detail: new SessionFilter(sessionId)
+ detail: new SessionFilter(value)
})
);
}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableStatusFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableStatusFilter.svelte
new file mode 100644
index 0000000000..91ac4dc3b5
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableStatusFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableTypeFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableTypeFilter.svelte
new file mode 100644
index 0000000000..fd4ed8843d
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/ClickableTypeFilter.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/BooleanFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/BooleanFacetedFilter.svelte
new file mode 100644
index 0000000000..555821c2bd
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/BooleanFacetedFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/DateFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/DateFacetedFilter.svelte
new file mode 100644
index 0000000000..abf6196e8d
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/DateFacetedFilter.svelte
@@ -0,0 +1,49 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/KeywordFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/KeywordFacetedFilter.svelte
new file mode 100644
index 0000000000..688e6a7f43
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/KeywordFacetedFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/NumberFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/NumberFacetedFilter.svelte
new file mode 100644
index 0000000000..757043c16d
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/NumberFacetedFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/OrganizationFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/OrganizationFacetedFilter.svelte
new file mode 100644
index 0000000000..6d70d09b2f
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/OrganizationFacetedFilter.svelte
@@ -0,0 +1,54 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/ProjectFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/ProjectFacetedFilter.svelte
new file mode 100644
index 0000000000..839fc1b544
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/ProjectFacetedFilter.svelte
@@ -0,0 +1,57 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/ReferenceFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/ReferenceFacetedFilter.svelte
new file mode 100644
index 0000000000..2dcfa24813
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/ReferenceFacetedFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/SessionFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/SessionFacetedFilter.svelte
new file mode 100644
index 0000000000..dbeef15b99
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/SessionFacetedFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/StatusFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/StatusFacetedFilter.svelte
new file mode 100644
index 0000000000..4b12f0f169
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/StatusFacetedFilter.svelte
@@ -0,0 +1,24 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/StringFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/StringFacetedFilter.svelte
new file mode 100644
index 0000000000..5f1a2b89dd
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/StringFacetedFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/TypeFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/TypeFacetedFilter.svelte
new file mode 100644
index 0000000000..d50579465f
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/TypeFacetedFilter.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/VersionFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/VersionFacetedFilter.svelte
new file mode 100644
index 0000000000..6ea571f503
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/VersionFacetedFilter.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/BooleanFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/BooleanFacetedFilter.svelte
new file mode 100644
index 0000000000..b52fb4c0a9
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/BooleanFacetedFilter.svelte
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+ open.set(false)}
+ >
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/DropDownFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/DropDownFacetedFilter.svelte
new file mode 100644
index 0000000000..4a0b648ddc
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/DropDownFacetedFilter.svelte
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+ {#if options.length > 10}
+
+ {/if}
+
+ {#if loading}
+ Loading...
+ {/if}
+ {noOptionsText}
+ {#if options.length > 0}
+
+ {#each options as option (option.value)}
+ onValueSelected(option.value)}>
+
+
+
+
+ {option.label}
+
+
+ {/each}
+ {/if}
+
+
+ open.set(false)}
+ >
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/KeywordFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/KeywordFacetedFilter.svelte
new file mode 100644
index 0000000000..c3d57087f7
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/KeywordFacetedFilter.svelte
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+ open.set(false)}
+ >
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/MultiselectFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/MultiselectFacetedFilter.svelte
new file mode 100644
index 0000000000..0f27688e7f
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/MultiselectFacetedFilter.svelte
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+ {#if options.length > 10}
+
+ {/if}
+
+ {#if loading}
+ Loading...
+ {/if}
+ {noOptionsText}
+ {#if options.length > 0}
+
+ {#each options as option (option.value)}
+ onValueSelected(option.value)}>
+
+
+
+
+ {option.label}
+
+
+ {/each}
+
+ {/if}
+
+
+ 0}
+ on:clear={onClearFilter}
+ on:remove={onRemoveFilter}
+ on:close={() => open.set(false)}
+ >
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/NumberFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/NumberFacetedFilter.svelte
new file mode 100644
index 0000000000..4e0f8106d7
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/NumberFacetedFilter.svelte
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+ open.set(false)}
+ >
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/StringFacetedFilter.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/StringFacetedFilter.svelte
new file mode 100644
index 0000000000..9f4cf83c5f
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/base/StringFacetedFilter.svelte
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+ open.set(false)}
+ >
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/index.ts
new file mode 100644
index 0000000000..98579dc24a
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/facets/index.ts
@@ -0,0 +1,68 @@
+import type { FacetedFilter } from '$comp/faceted-filter';
+import { writable } from 'svelte/store';
+import { type BooleanFilter, type DateFilter, type IFilter, type NumberFilter, type StringFilter, type VersionFilter } from '../filters';
+
+import BooleanFacetedFilter from './BooleanFacetedFilter.svelte';
+import DateFacetedFilter from './DateFacetedFilter.svelte';
+import KeywordFacetedFilter from './KeywordFacetedFilter.svelte';
+import NumberFacetedFilter from './NumberFacetedFilter.svelte';
+import OrganizationFacetedFilter from './OrganizationFacetedFilter.svelte';
+import ProjectFacetedFilter from './ProjectFacetedFilter.svelte';
+import ReferenceFacetedFilter from './ReferenceFacetedFilter.svelte';
+import SessionFacetedFilter from './SessionFacetedFilter.svelte';
+import StatusFacetedFilter from './StatusFacetedFilter.svelte';
+import StringFacetedFilter from './StringFacetedFilter.svelte';
+import TypeFacetedFilter from './TypeFacetedFilter.svelte';
+import VersionFacetedFilter from './VersionFacetedFilter.svelte';
+
+export function toFacetedFilters(filters: IFilter[]): FacetedFilter[] {
+ return filters.map((filter) => {
+ switch (filter.type) {
+ case 'boolean': {
+ const booleanFilter = filter as BooleanFilter;
+ return { title: (booleanFilter.term as string) ?? 'Boolean', component: BooleanFacetedFilter, filter, open: writable(false) };
+ }
+ case 'date': {
+ const dateFilter = filter as DateFilter;
+ const title = dateFilter.term === 'date' ? 'Date Range' : dateFilter.term ?? 'Date';
+ return { title, component: DateFacetedFilter, filter, open: writable(false) };
+ }
+ case 'keyword': {
+ return { title: 'Keyword', component: KeywordFacetedFilter, filter, open: writable(false) };
+ }
+ case 'number': {
+ const numberFilter = filter as NumberFilter;
+ return { title: (numberFilter.term as string) ?? 'Number', component: NumberFacetedFilter, filter: numberFilter, open: writable(false) };
+ }
+ case 'organization': {
+ return { title: 'Organization', component: OrganizationFacetedFilter, filter, open: writable(false) };
+ }
+ case 'project': {
+ return { title: 'Project', component: ProjectFacetedFilter, filter, open: writable(false) };
+ }
+ case 'reference': {
+ return { title: 'Reference', component: ReferenceFacetedFilter, filter, open: writable(false) };
+ }
+ case 'session': {
+ return { title: 'Session', component: SessionFacetedFilter, filter, open: writable(false) };
+ }
+ case 'status': {
+ return { title: 'Status', component: StatusFacetedFilter, filter, open: writable(false) };
+ }
+ case 'string': {
+ const stringFilter = filter as StringFilter;
+ return { title: (stringFilter.term as string) ?? 'String', component: StringFacetedFilter, filter, open: writable(false) };
+ }
+ case 'type': {
+ return { title: 'Type', component: TypeFacetedFilter, filter, open: writable(false) };
+ }
+ case 'version': {
+ const versionFilter = filter as VersionFilter;
+ return { title: (versionFilter.term as string) ?? 'Version', component: VersionFacetedFilter, filter, open: writable(false) };
+ }
+ default: {
+ throw new Error(`Unknown filter type: ${filter.type}`);
+ }
+ }
+ });
+}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/filters.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/filters.ts
index 72716bc26e..bc52cc6986 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/filters/filters.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/filters/filters.ts
@@ -1,27 +1,41 @@
import type { PersistentEventKnownTypes } from '$lib/models/api';
import type { StackStatus } from '$lib/models/api';
-import type { Serializer } from 'svelte-local-storage-store';
+import type { Serializer } from 'svelte-persisted-store';
+import { get, type Writable } from 'svelte/store';
export interface IFilter {
readonly type: string;
+ readonly key: string;
+ isEmpty(): boolean;
+ reset(): void;
toFilter(): string;
}
-export interface IFacetedFilter extends IFilter {
- term: string;
- values: unknown[];
- faceted: boolean;
-}
-
export class BooleanFilter implements IFilter {
constructor(
- public term: string,
+ public term?: string,
public value?: boolean
) {}
public type: string = 'boolean';
+ public get key(): string {
+ return `${this.type}:${this.term}`;
+ }
+
+ public isEmpty(): boolean {
+ return this.value === undefined;
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
+ if (this.term === undefined) {
+ return '';
+ }
+
if (this.value === undefined) {
return `_missing_:${this.term}`;
}
@@ -32,13 +46,29 @@ export class BooleanFilter implements IFilter {
export class DateFilter implements IFilter {
constructor(
- public term: string,
+ public term?: string,
public value?: Date | string
) {}
public type: string = 'date';
+ public get key(): string {
+ return `${this.type}:${this.term}`;
+ }
+
+ public isEmpty(): boolean {
+ return this.value === undefined;
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
+ if (this.term === undefined) {
+ return '';
+ }
+
if (this.value === undefined) {
return `_missing_:${this.term}`;
}
@@ -49,24 +79,56 @@ export class DateFilter implements IFilter {
}
export class KeywordFilter implements IFilter {
- constructor(public keyword: string) {}
+ constructor(public value?: string) {}
public type: string = 'keyword';
+ public get key(): string {
+ return this.type;
+ }
+
+ public isEmpty(): boolean {
+ return !this.value?.trim();
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
- return this.keyword;
+ if (this.isEmpty()) {
+ return '';
+ }
+
+ return this.value!.trim();
}
}
export class NumberFilter implements IFilter {
constructor(
- public term: string,
+ public term?: string,
public value?: number
) {}
public type: string = 'number';
+ public get key(): string {
+ return `${this.type}:${this.term}`;
+ }
+
+ public isEmpty(): boolean {
+ return this.value === undefined;
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
+ if (this.term === undefined) {
+ return '';
+ }
+
if (this.value === undefined) {
return `_missing_:${this.term}`;
}
@@ -75,56 +137,173 @@ export class NumberFilter implements IFilter {
}
}
+export class OrganizationFilter implements IFilter {
+ constructor(public value?: string) {}
+
+ public type: string = 'organization';
+
+ public get key(): string {
+ return this.type;
+ }
+
+ public isEmpty(): boolean {
+ return !this.value?.trim();
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
+ public toFilter(): string {
+ if (this.isEmpty()) {
+ return '';
+ }
+
+ return `organization:${this.value}`;
+ }
+}
+
+export class ProjectFilter implements IFilter {
+ constructor(
+ public organization: string | undefined,
+ public value: string[]
+ ) {}
+
+ public type: string = 'project';
+
+ public get key(): string {
+ return this.type;
+ }
+
+ public isEmpty(): boolean {
+ return this.value.length === 0;
+ }
+
+ public reset(): void {
+ this.value = [];
+ }
+
+ public toFilter(): string {
+ if (this.value.length == 0) {
+ return '';
+ }
+
+ if (this.value.length == 1) {
+ return `project:${this.value[0]}`;
+ }
+
+ return `(${this.value.map((val) => `project:${val}`).join(' OR ')})`;
+ }
+}
+
export class ReferenceFilter implements IFilter {
- constructor(public referenceId: string) {}
+ constructor(public value?: string) {}
public type: string = 'reference';
+ public get key(): string {
+ return this.type;
+ }
+
+ public isEmpty(): boolean {
+ return !this.value?.trim();
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
- return `reference:${quoteIfSpecialCharacters(this.referenceId)}`;
+ if (this.isEmpty()) {
+ return '';
+ }
+
+ return `reference:${quoteIfSpecialCharacters(this.value)}`;
}
}
export class SessionFilter implements IFilter {
- constructor(public sessionId: string) {}
+ constructor(public value?: string) {}
public type: string = 'session';
+ public get key(): string {
+ return this.type;
+ }
+
+ public isEmpty(): boolean {
+ return !this.value?.trim();
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
- const session = quoteIfSpecialCharacters(this.sessionId);
+ if (this.isEmpty()) {
+ return '';
+ }
+
+ const session = quoteIfSpecialCharacters(this.value);
return `(reference:${session} OR ref.session:${session})`;
}
}
-export class StatusFilter implements IFacetedFilter {
- constructor(public values: StackStatus[]) {}
+export class StatusFilter implements IFilter {
+ constructor(public value: StackStatus[]) {}
- public term: string = 'status';
public type: string = 'status';
- public faceted: boolean = true;
+
+ public get key(): string {
+ return this.type;
+ }
+
+ public isEmpty(): boolean {
+ return this.value.length === 0;
+ }
+
+ public reset(): void {
+ this.value = [];
+ }
public toFilter(): string {
- if (this.values.length == 0) {
+ if (this.value.length == 0) {
return '';
}
- if (this.values.length == 1) {
- return `${this.term}:${this.values[0]}`;
+ if (this.value.length == 1) {
+ return `status:${this.value[0]}`;
}
- return `(${this.values.map((val) => `${this.term}:${val}`).join(' OR ')})`;
+ return `(${this.value.map((val) => `status:${val}`).join(' OR ')})`;
}
}
export class StringFilter implements IFilter {
constructor(
- public term: string,
- public value?: string | null
+ public term?: string,
+ public value?: string
) {}
public type: string = 'string';
+ public get key(): string {
+ return `${this.type}:${this.term}`;
+ }
+
+ public isEmpty(): boolean {
+ return this.value === undefined;
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
+ if (this.term === undefined) {
+ return '';
+ }
+
if (this.value === undefined) {
return `_missing_:${this.term}`;
}
@@ -133,35 +312,61 @@ export class StringFilter implements IFilter {
}
}
-export class TypeFilter implements IFacetedFilter {
- constructor(public values: PersistentEventKnownTypes[]) {}
+export class TypeFilter implements IFilter {
+ constructor(public value: PersistentEventKnownTypes[]) {}
- public term: string = 'type';
public type: string = 'type';
- public faceted: boolean = true;
+
+ public get key(): string {
+ return this.type;
+ }
+
+ public isEmpty(): boolean {
+ return this.value.length === 0;
+ }
+
+ public reset(): void {
+ this.value = [];
+ }
public toFilter(): string {
- if (this.values.length == 0) {
+ if (this.value.length == 0) {
return '';
}
- if (this.values.length == 1) {
- return `${this.term}:${this.values[0]}`;
+ if (this.value.length == 1) {
+ return `type:${this.value[0]}`;
}
- return `(${this.values.map((val) => `${this.term}:${val}`).join(' OR ')})`;
+ return `(${this.value.map((val) => `type:${val}`).join(' OR ')})`;
}
}
export class VersionFilter implements IFilter {
constructor(
- public term: string,
+ public term?: string,
public value?: string
) {}
public type: string = 'version';
+ public get key(): string {
+ return `${this.type}:${this.term}`;
+ }
+
+ public isEmpty(): boolean {
+ return this.value === undefined;
+ }
+
+ public reset(): void {
+ this.value = undefined;
+ }
+
public toFilter(): string {
+ if (this.term === undefined) {
+ return '';
+ }
+
if (this.value === undefined) {
return `_missing_:${this.term}`;
}
@@ -185,154 +390,59 @@ export function quote(value?: string | null): string | undefined {
return value ? `"${value}"` : undefined;
}
-export function toFilter(filters: IFilter[], includeFaceted: boolean = false): string {
+export function toFilter(filters: IFilter[]): string {
return filters
- .filter((f) => includeFaceted || !isFaceted(f))
.map((f) => f.toFilter())
+ .filter(Boolean)
.join(' ')
.trim();
}
-/**
- * Update the filters with the given filter. If the filter already exists, it will be removed.
- * @param filters The filters
- * @param filter The filter to add or remove
- * @returns The updated filters
- */
-export function toggleFilter(filters: IFilter[], filter: IFilter): IFilter[] {
- const index = filters.findIndex((f) => (f.type === filter.type && isFaceted(f) && isFaceted(filter)) || f.toFilter() === filter.toFilter());
-
- if (index >= 0) {
- filters.splice(index, 1);
- } else {
- filters.push(filter);
- }
-
- return filters;
-}
-
-/**
- * Adds or updates a given faceted filter if it has values, otherwise it will be removed
- * @param filters The filters
- * @param filter The filter to add, update or remove.
- * @returns true if filters has been modified
- */
-export function upsertOrRemoveFacetFilter(filters: IFilter[], filter: IFacetedFilter): boolean {
- const index = filters.findIndex((f) => f.type === filter.type && isFaceted(f));
-
- // If the filter has no values, remove it.
- if (!filter.values || filter.values.length == 0) {
- if (index >= 0) {
- filters.splice(index, 1);
- return true;
- }
-
- return false;
- }
-
- if (index >= 0) {
- if (filter.toFilter() === filters[index].toFilter()) {
- return false;
- }
-
- filters[index] = filter;
- } else {
- filters.push(filter);
- }
-
- return true;
-}
-
-/**
- * Given the existing filters try and parse out any existing filters while adding new user filters as a keyword filter.
- * @param filters The current filters
- * @param filter The current filter string that was modified by the user
- * @returns The updated filter
- */
-export function parseFilter(filters: IFilter[], input: string): IFilter[] {
- const resolvedFilters: IFilter[] = [];
-
- const keywordFilterParts = [];
- for (const filter of filters) {
- if (isFaceted(filter)) {
- resolvedFilters.push(filter);
- continue;
- }
-
- input = input?.trim();
- if (!input) {
- continue;
- }
-
- // NOTE: This is a super naive implementation...
- const part = filter.toFilter();
- if (part) {
- // Check for whole word / phrase match
- const regex = new RegExp(`(^|\\s)${part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`);
- if (regex.test(input)) {
- input = input.replace(regex, '');
- if (filter instanceof KeywordFilter) {
- keywordFilterParts.push(part);
- } else {
- resolvedFilters.push(filter);
- }
- }
- }
- }
-
- input = `${keywordFilterParts.join(' ')} ${input ?? ''}`.trim();
- if (input) {
- resolvedFilters.push(new KeywordFilter(input));
- }
-
- return resolvedFilters;
-}
-
-export function getFilter(filter: Record): IFilter | undefined {
+export function getFilter(filter: Omit & Record): IFilter | undefined {
switch (filter.type) {
case 'boolean':
return new BooleanFilter(filter.term as string, filter.value as boolean);
case 'date':
return new DateFilter(filter.term as string, filter.value as Date);
case 'keyword':
- return new KeywordFilter(filter.keyword as string);
+ return new KeywordFilter(filter.value as string);
case 'number':
return new NumberFilter(filter.term as string, filter.value as number);
+ case 'organization':
+ return new OrganizationFilter(filter.value as string);
+ case 'project':
+ return new ProjectFilter(filter.organization as string, filter.value as string[]);
case 'reference':
- return new ReferenceFilter(filter.referenceId as string);
+ return new ReferenceFilter(filter.value as string);
case 'session':
- return new SessionFilter(filter.sessionId as string);
+ return new SessionFilter(filter.value as string);
case 'status':
- return new StatusFilter(filter.values as StackStatus[]);
+ return new StatusFilter(filter.value as StackStatus[]);
case 'string':
return new StringFilter(filter.term as string, filter.value as string);
case 'type':
- return new TypeFilter(filter.values as PersistentEventKnownTypes[]);
+ return new TypeFilter(filter.value as PersistentEventKnownTypes[]);
case 'version':
return new VersionFilter(filter.term as string, filter.value as string);
+ default:
+ throw new Error(`Unknown filter type: ${filter.type}`);
}
}
-function isFaceted(filter: IFilter): filter is IFacetedFilter {
- return 'faceted' in filter;
-}
-
-const FACETED_FILTER_TYPES = ['status', 'type'];
-export function toFacetedValues(filters: IFilter[]): Record {
- const values: Record = {};
- for (const filterType of FACETED_FILTER_TYPES) {
- const filter = filters.find((f) => f.type === filterType && isFaceted(f)) as IFacetedFilter | undefined;
- values[filterType] = filter?.values ?? [];
- }
-
- return values;
-}
-
-export function resetFacetedValues(filters: IFilter[]): IFilter[] {
- for (const filter of filters) {
- if (isFaceted(filter)) {
- upsertOrRemoveFacetFilter(filters, { ...filter, values: [] });
+export function setFilter(filters: IFilter[], filter: IFilter): IFilter[] {
+ const existingFilter = filters.find((f) => f.key === filter.key && ('term' in f && 'term' in filter ? f.term === filter.term : true));
+ if (existingFilter) {
+ if ('value' in existingFilter && 'value' in filter) {
+ if (Array.isArray(existingFilter.value) && Array.isArray(filter.value)) {
+ existingFilter.value = [...new Set([...existingFilter.value, ...filter.value])];
+ } else {
+ existingFilter.value = filter.value;
+ }
+ } else {
+ Object.assign(existingFilter, filter);
}
+ } else {
+ filters.push(filter);
}
return filters;
@@ -347,7 +457,7 @@ export class FilterSerializer implements Serializer {
const data: unknown[] = JSON.parse(text);
const filters: IFilter[] = [];
for (const filterData of data) {
- const filter = getFilter(filterData as Record);
+ const filter = getFilter(filterData as Omit);
if (filter) {
filters.push(filter);
}
@@ -360,3 +470,71 @@ export class FilterSerializer implements Serializer {
return JSON.stringify(object);
}
}
+
+export function getDefaultFilters(includeDateFilter = true): IFilter[] {
+ return [
+ new OrganizationFilter(),
+ new ProjectFilter(undefined, []),
+ new StatusFilter([]),
+ new TypeFilter([]),
+ new DateFilter('date', 'last week'),
+ new ReferenceFilter(),
+ new SessionFilter(),
+ new KeywordFilter()
+ ].filter((f) => includeDateFilter || f.type !== 'date');
+}
+
+export function filterChanged(filters: Writable, updated: IFilter): void {
+ filters.set(processFilterRules(setFilter(get(filters), updated), updated));
+}
+
+export function filterRemoved(filters: Writable, defaultFilters: IFilter[], removed?: IFilter): void {
+ // If detail is undefined, remove all filters.
+ if (!removed) {
+ filters.set(defaultFilters);
+ } else if (defaultFilters.find((f) => f.key === removed.key)) {
+ filters.set(processFilterRules(setFilter(get(filters), removed), removed));
+ } else {
+ filters.set(
+ processFilterRules(
+ get(filters).filter((f) => f.key !== removed.key),
+ removed
+ )
+ );
+ }
+}
+
+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);
+ const filtered: IFilter[] = [];
+ Object.entries(groupedFilters).forEach(([, items]) => {
+ if (items && items.length > 0) {
+ filtered.push(items[0]);
+ }
+ });
+
+ const projectFilter = filtered.find((f) => f.type === 'project') as ProjectFilter;
+ if (projectFilter) {
+ let organizationFilter = filtered.find((f) => f.type === 'organization') as OrganizationFilter;
+
+ // If there is a project filter, verify the organization filter is set
+ if (!organizationFilter) {
+ organizationFilter = new OrganizationFilter(projectFilter.organization);
+ filtered.push(organizationFilter);
+ }
+
+ // If the organization filter changes and organization is not set on the project filter, clear the project filter
+ if (changed?.type === 'organization' && projectFilter.organization !== organizationFilter.value) {
+ projectFilter.organization = organizationFilter.value;
+ projectFilter.value = [];
+ }
+
+ // If the project filter changes and the organization filter is not set, set it
+ if (organizationFilter.value !== projectFilter.organization) {
+ organizationFilter.value = projectFilter.organization;
+ }
+ }
+
+ return filtered;
+}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/avatar/avatar-fallback.svelte
index 76c244f0cf..b2f87391bd 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/avatar/avatar-fallback.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/avatar/avatar-fallback.svelte
@@ -1,6 +1,6 @@
-
+
import { Command as CommandPrimitive } from 'cmdk-sv';
- import { cn } from '$lib/utils';
+ import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.EmptyProps;
let className: string | undefined | null = undefined;
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-group.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-group.svelte
index b286709bc9..1ec78d4c41 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-group.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-group.svelte
@@ -1,6 +1,6 @@
-
+
import { Command as CommandPrimitive } from 'cmdk-sv';
- import { cn } from '$lib/utils';
+ import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.ItemProps;
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-list.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-list.svelte
index 42732c8ecd..82ba18b33c 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-list.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/command/command-list.svelte
@@ -1,6 +1,6 @@
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-description.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-description.svelte
new file mode 100644
index 0000000000..bad00a1251
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-description.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-element-field.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-element-field.svelte
new file mode 100644
index 0000000000..270f011870
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-element-field.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-field-errors.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-field-errors.svelte
new file mode 100644
index 0000000000..cb19b63805
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-field-errors.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+ {#each errors as error}
+ {error}
+ {/each}
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-field.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-field.svelte
new file mode 100644
index 0000000000..6f6f50ed71
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-field.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-fieldset.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-fieldset.svelte
new file mode 100644
index 0000000000..3fbd692e17
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-fieldset.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-label.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-label.svelte
new file mode 100644
index 0000000000..4d19eedf2b
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-label.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-legend.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-legend.svelte
new file mode 100644
index 0000000000..344692a2a3
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/form-legend.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/index.ts
new file mode 100644
index 0000000000..e550824d47
--- /dev/null
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/form/index.ts
@@ -0,0 +1,33 @@
+import * as FormPrimitive from 'formsnap';
+import Description from './form-description.svelte';
+import Label from './form-label.svelte';
+import FieldErrors from './form-field-errors.svelte';
+import Field from './form-field.svelte';
+import Button from './form-button.svelte';
+import Fieldset from './form-fieldset.svelte';
+import Legend from './form-legend.svelte';
+import ElementField from './form-element-field.svelte';
+
+const Control = FormPrimitive.Control;
+
+export {
+ Field,
+ Control,
+ Label,
+ FieldErrors,
+ Description,
+ Fieldset,
+ Legend,
+ ElementField,
+ Button,
+ //
+ Field as FormField,
+ Control as FormControl,
+ Description as FormDescription,
+ Label as FormLabel,
+ FieldErrors as FormFieldErrors,
+ Fieldset as FormFieldset,
+ Legend as FormLegend,
+ ElementField as FormElementField,
+ Button as FormButton
+};
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/index.ts
index 5722094fc9..11a6791f38 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/index.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/index.ts
@@ -1,6 +1,6 @@
import Root from './input.svelte';
-type FormInputEvent = T & {
+export type FormInputEvent = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/input.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/input.svelte
index 182148bc52..4a0e9496f2 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/input.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/input/input.svelte
@@ -1,7 +1,7 @@
import { Label as LabelPrimitive } from 'bits-ui';
- import { cn } from '$lib/utils';
+ import { cn } from '$lib/utils.js';
type $$Props = LabelPrimitive.Props;
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/index.ts
index 57c9d6d258..ab5acc49fb 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/index.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/index.ts
@@ -2,13 +2,16 @@ import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
+const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
+ Close,
//
Root as Popover,
Content as PopoverContent,
- Trigger as PopoverTrigger
+ Trigger as PopoverTrigger,
+ Close as PopoverClose
};
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/popover-content.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/popover-content.svelte
index 5a0ff3ada8..f9f0a01951 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/popover-content.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/popover/popover-content.svelte
@@ -1,6 +1,6 @@
@@ -32,7 +32,7 @@
-
+
Close
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/sheet/sheet-description.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/sheet/sheet-description.svelte
index 1b977d638d..f2e7b90135 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/sheet/sheet-description.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/sheet/sheet-description.svelte
@@ -1,6 +1,6 @@
-
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/tabs/tabs-content.svelte b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/tabs/tabs-content.svelte
index bb0f017161..a05d3057bb 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/components/ui/tabs/tabs-content.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/components/ui/tabs/tabs-content.svelte
@@ -1,6 +1,6 @@
@@ -36,30 +45,13 @@
Events
-
+
-
-
-
-
-
-
-
-
-
+
-
-
(selectedEventId = null)}>
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 09dc3168b5..4377643175 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte
@@ -1,17 +1,21 @@
@@ -31,11 +54,9 @@
Issues
-
+
-
-
-
+
@@ -45,7 +66,9 @@
selectedStackId.set(null)}>
- Event Details
+ Event Details
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 800dbd7941..8f1f407352 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/stream/+page.svelte
@@ -1,17 +1,43 @@
@@ -19,14 +45,21 @@
Event Stream
-
+
+
+
+
+
(selectedEventId = null)}>
- Event Details
+ Event Details
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/logout/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/logout/+page.svelte
index 77ae799a97..fb81cfbae2 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/logout/+page.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/logout/+page.svelte
@@ -8,7 +8,7 @@
import H2 from '$comp/typography/H2.svelte';
$: if (!$isAuthenticated) {
- goto('next/login', { replaceState: true });
+ goto('/next/login', { replaceState: true });
}
let problem = new ProblemDetails();
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/routes.ts b/src/Exceptionless.Web/ClientApp/src/routes/routes.ts
index cf22b77c87..170e853a2f 100644
--- a/src/Exceptionless.Web/ClientApp/src/routes/routes.ts
+++ b/src/Exceptionless.Web/ClientApp/src/routes/routes.ts
@@ -1,4 +1,5 @@
import type { User } from '$lib/models/api';
+import type { ComponentType } from 'svelte';
import { routes as appRoutes } from './(app)/routes';
import { routes as authRoutes } from './(auth)/routes';
@@ -11,7 +12,7 @@ export type NavigationItem = {
group: string;
title: string;
href: string;
- icon: ConstructorOfATypedSvelteComponent;
+ icon: ComponentType;
show?: (context: NavigationItemContext) => boolean;
};
diff --git a/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs b/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs
index ed192a45f4..4f0eb6b013 100644
--- a/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs
+++ b/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs
@@ -20,6 +20,7 @@ public abstract class ExceptionlessApiController : Controller
protected const int DEFAULT_LIMIT = 10;
protected const int MAXIMUM_LIMIT = 100;
protected const int MAXIMUM_SKIP = 1000;
+ protected static readonly char[] TIME_PARTS = ['|'];
protected TimeSpan GetOffset(string? offset)
{
@@ -35,9 +36,9 @@ protected TimeSpan GetOffset(string? offset)
protected virtual TimeInfo GetTimeInfo(string? time, string? offset, DateTime? minimumUtcStartDate = null)
{
string field = DefaultDateField;
- if (!String.IsNullOrEmpty(time) && time.Contains("|"))
+ if (!String.IsNullOrEmpty(time) && time.Contains('|'))
{
- string[] parts = time.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+ string[] parts = time.Split(TIME_PARTS, StringSplitOptions.RemoveEmptyEntries);
field = parts.Length > 0 && AllowedDateFields.Contains(parts[0]) ? parts[0] : DefaultDateField;
time = parts.Length > 1 ? parts[1] : null;
}
@@ -56,8 +57,7 @@ protected virtual TimeInfo GetTimeInfo(string? time, string? offset, DateTime? m
protected int GetLimit(int limit, int maximumLimit = MAXIMUM_LIMIT)
{
- if (maximumLimit < MAXIMUM_LIMIT)
- throw new ArgumentOutOfRangeException(nameof(maximumLimit));
+ ArgumentOutOfRangeException.ThrowIfLessThan(maximumLimit, MAXIMUM_LIMIT);
if (limit < 1)
limit = DEFAULT_LIMIT;
diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs
index 7ce8ba5db1..832e8ebada 100644
--- a/src/Exceptionless.Web/Controllers/EventController.cs
+++ b/src/Exceptionless.Web/Controllers/EventController.cs
@@ -88,7 +88,7 @@ ILoggerFactory loggerFactory
/// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// Invalid filter.
[HttpGet("count")]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
@@ -111,7 +111,7 @@ public async Task> GetCountAsync(string? filter = null
/// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// Invalid filter.
[HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/events/count")]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
@@ -137,7 +137,7 @@ public async Task> GetCountByOrganizationAsync(string
/// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// Invalid filter.
[HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/count")]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
@@ -198,7 +198,7 @@ public async Task> GetAsync(string id, string? tim
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -210,7 +210,7 @@ public async Task> GetAsync(string id, string? tim
[ProducesResponseType(typeof(ICollection), 200)]
[ProducesResponseType(typeof(ICollection), 200)]
[ProducesResponseType(typeof(ICollection), 200)]
- public async Task>> GetAsync(string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null)
+ public async Task>> GetAllAsync(string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int? page = null, int limit = 10, string? before = null, string? after = null)
{
var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter);
if (organizations.All(o => o.IsSuspended))
@@ -421,7 +421,7 @@ private Task> GetEventsInternalAsync(AppFilter sf,
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -456,7 +456,7 @@ public async Task>> GetByOrganizationA
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -495,7 +495,7 @@ public async Task>> GetByProjectAsync(
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -531,7 +531,7 @@ public async Task>> GetByStackAsync(st
///
/// An identifier used that references an event instance.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -560,7 +560,7 @@ public async Task>> GetByReferenceIdAs
/// An identifier used that references an event instance.
/// The identifier of the project.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -599,7 +599,7 @@ public async Task>> GetByReferenceIdAs
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -631,7 +631,7 @@ public async Task>> GetBySessionIdAsyn
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -669,7 +669,7 @@ public async Task>> GetBySessionIdAndP
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -699,7 +699,7 @@ public async Task>> GetSessionsAsync(s
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
@@ -734,7 +734,7 @@ public async Task>> GetSessionByOrgani
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// The before parameter is a cursor used for pagination and defines your place in the list of results.
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 4ca0afbf1f..52453d2876 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -81,7 +81,7 @@ public OrganizationController(
///
/// If no mode is set then a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.
[HttpGet]
- public async Task> GetAsync(string? mode = null)
+ public async Task>> GetAllAsync(string? mode = null)
{
var organizations = await GetModelsAsync(GetAssociatedOrganizationIds().ToArray());
var viewOrganizations = await MapCollectionAsync(organizations, true);
@@ -120,7 +120,7 @@ public async Task> PlanStatsAsync()
/// Get by id
///
/// The identifier of the organization.
- /// If no mode is set then the a light weight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.
+ /// If no mode is set then the a lightweight organization object will be returned. If the mode is set to stats than the fully populated object will be returned.
/// The organization could not be found.
[HttpGet("{id:objectid}", Name = "GetOrganizationById")]
public async Task> GetAsync(string id, string? mode = null)
diff --git a/src/Exceptionless.Web/Controllers/ProjectController.cs b/src/Exceptionless.Web/Controllers/ProjectController.cs
index 08bc8e9586..a2ecb99c7e 100644
--- a/src/Exceptionless.Web/Controllers/ProjectController.cs
+++ b/src/Exceptionless.Web/Controllers/ProjectController.cs
@@ -75,10 +75,10 @@ ILoggerFactory loggerFactory
/// Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
- /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned.
+ /// If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.
[HttpGet]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
- public async Task>> GetAsync(string? filter = null, string? sort = null, int page = 1, int limit = 10, string? mode = null)
+ public async Task>> GetAllAsync(string? filter = null, string? sort = null, int page = 1, int limit = 10, string? mode = null)
{
var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter);
if (organizations.Count == 0)
@@ -105,7 +105,7 @@ public async Task>> GetAsync(strin
/// The identifier of the organization.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
- /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned.
+ /// If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.
/// The organization could not be found.
[HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/projects")]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
@@ -131,7 +131,7 @@ public async Task>> GetByOrganizat
/// Get by id
///
/// The identifier of the project.
- /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned.
+ /// If no mode is set then the lightweight project object will be returned. If the mode is set to stats than the fully populated object will be returned.
/// The project could not be found.
[HttpGet("{id:objectid}", Name = "GetProjectById")]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
@@ -212,7 +212,7 @@ protected override async Task> DeleteModelsAsync(ICollection
#endregion
- [Obsolete]
+ [Obsolete("Use /api/v2/projects/config instead")]
[HttpGet("~/api/v1/project/config")]
public Task> GetV1ConfigAsync(int? v = null)
{
@@ -331,7 +331,7 @@ public async Task> ResetDataAsync(string id)
ProjectId = project.Id
});
- return WorkInProgress(new[] { workItemId });
+ return WorkInProgress([workItemId]);
}
[HttpGet("{id:objectid}/notifications")]
@@ -473,9 +473,8 @@ public async Task DeleteNotificationSettingsAsync(string id, stri
if (!Request.IsGlobalAdmin() && !String.Equals(user.Id, userId))
return NotFound();
- if (project.NotificationSettings.ContainsKey(userId))
+ if (project.NotificationSettings.Remove(userId))
{
- project.NotificationSettings.Remove(userId);
await _repository.SaveAsync(project, o => o.Cache());
}
@@ -560,7 +559,7 @@ private async Task IsProjectNameAvailableInternalAsync(string? organizatio
if (String.IsNullOrWhiteSpace(name))
return false;
- var organizationIds = IsInOrganization(organizationId) ? new List { organizationId } : GetAssociatedOrganizationIds();
+ var organizationIds = IsInOrganization(organizationId) ? [organizationId] : GetAssociatedOrganizationIds();
var projects = await _repository.GetByOrganizationIdsAsync(organizationIds);
string decodedName = Uri.UnescapeDataString(name).Trim().ToLowerInvariant();
@@ -580,7 +579,7 @@ private async Task IsProjectNameAvailableInternalAsync(string? organizatio
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
public async Task PostDataAsync(string id, string key, ValueFromBody value)
{
- if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value) || key.StartsWith("-"))
+ if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value) || key.StartsWith('-'))
return BadRequest();
var project = await GetModelAsync(id, false);
@@ -605,7 +604,7 @@ public async Task PostDataAsync(string id, string key, ValueFromB
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
public async Task DeleteDataAsync(string id, string key)
{
- if (String.IsNullOrWhiteSpace(key) || key.StartsWith("-"))
+ if (String.IsNullOrWhiteSpace(key) || key.StartsWith('-'))
return BadRequest();
var project = await GetModelAsync(id, false);
diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs
index 3f91a51276..e7fad1f095 100644
--- a/src/Exceptionless.Web/Controllers/StackController.cs
+++ b/src/Exceptionless.Web/Controllers/StackController.cs
@@ -464,13 +464,13 @@ public Task> DeleteAsync(string ids)
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// Invalid filter.
[HttpGet]
[Authorize(Policy = AuthorizationRoles.UserPolicy)]
- public async Task>> GetAsync(string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int page = 1, int limit = 10)
+ public async Task>> GetAllAsync(string? filter = null, string? sort = null, string? time = null, string? offset = null, string? mode = null, int page = 1, int limit = 10)
{
var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter);
if (organizations.All(o => o.IsSuspended))
@@ -522,7 +522,7 @@ private async Task>> GetInternalAsync(Ap
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// Invalid filter.
@@ -552,7 +552,7 @@ public async Task>> GetByOrganizationAsy
/// Controls the sort order that the data is returned in. In this example -date returns the results descending by date.
/// The time filter that limits the data being returned to a specific date range.
/// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support.
- /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned.
+ /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a lightweight object will be returned.
/// The page parameter is used for pagination. This value must be greater than 0.
/// A limit on the number of objects to be returned. Limit can range between 1 and 100 items.
/// Invalid filter.
diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj
index 81120cf8c8..505f5664e7 100644
--- a/src/Exceptionless.Web/Exceptionless.Web.csproj
+++ b/src/Exceptionless.Web/Exceptionless.Web.csproj
@@ -16,11 +16,11 @@
-
+
-
-
-
+
+
+
@@ -29,15 +29,15 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs
index d85aeb6c44..439eda9ac4 100644
--- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs
+++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs
@@ -503,7 +503,7 @@ await CreateDataAsync(d =>
[Fact]
public async Task WillGetStackEvents()
{
- (List? stacks, _) = await CreateDataAsync(d =>
+ (var stacks, _) = await CreateDataAsync(d =>
{
d.Event().TestProject();
});
diff --git a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs
index 3c6781b533..ed05e404a4 100644
--- a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs
+++ b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs
@@ -118,7 +118,7 @@ public async Task CanGetProjectListStats()
Assert.Equal(0, project.StackCount);
Assert.Equal(0, project.EventCount);
- (List? stacks, List? events) = await CreateDataAsync(d =>
+ (var stacks, var events) = await CreateDataAsync(d =>
{
d.Event().Message("test");
});
diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj
index 531b59c563..eb979c2adf 100644
--- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj
+++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj
@@ -6,14 +6,14 @@
-
+
-
+
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/tests/Exceptionless.Tests/GlobalSuppressions.cs b/tests/Exceptionless.Tests/GlobalSuppressions.cs
index 4a0a73ae1c..6ba421abe2 100644
--- a/tests/Exceptionless.Tests/GlobalSuppressions.cs
+++ b/tests/Exceptionless.Tests/GlobalSuppressions.cs
@@ -6,6 +6,6 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:Exceptionless.Tests.Extensions.StringExtensionsTests.LowerUnderscoredWords")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:Exceptionless.Tests.Pipeline.EventPipelineTests.GeneratePerformanceDataAsync~System.Threading.Tasks.Task")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:Exceptionless.Tests.Repositories.EventRepositoryTests.GetAsync~System.Threading.Tasks.Task")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:Exceptionless.Tests.Repositories.EventRepositoryTests.GetAllAsync~System.Threading.Tasks.Task")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:Exceptionless.Tests.Repositories.EventRepositoryTests.GetAsyncPerformanceAsync~System.Threading.Tasks.Task")]
diff --git a/tests/Exceptionless.Tests/IntegrationTestsBase.cs b/tests/Exceptionless.Tests/IntegrationTestsBase.cs
index 213bbabd05..2052829e3e 100644
--- a/tests/Exceptionless.Tests/IntegrationTestsBase.cs
+++ b/tests/Exceptionless.Tests/IntegrationTestsBase.cs
@@ -41,7 +41,7 @@ public abstract class IntegrationTestsBase : TestWithLoggingBase, Xunit.IAsyncLi
public IntegrationTestsBase(ITestOutputHelper output, AppWebHostFactory factory) : base(output)
{
- Log.MinimumLevel = LogLevel.Information;
+ Log.DefaultMinimumLevel = LogLevel.Information;
Log.SetLogLevel(LogLevel.Warning);
Log.SetLogLevel(LogLevel.Warning);
Log.SetLogLevel(LogLevel.Warning);
@@ -125,8 +125,6 @@ protected virtual void RegisterServices(IServiceCollection services)
await stackRepository.AddAsync(stacks, o => o.ImmediateConsistency());
await eventRepository.AddAsync(events, o => o.ImmediateConsistency());
- await RefreshDataAsync();
-
return (stacks.ToList(), events.ToList());
}
@@ -135,8 +133,8 @@ protected virtual async Task ResetDataAsync()
await _semaphoreSlim.WaitAsync();
try
{
- var oldLoggingLevel = Log.MinimumLevel;
- Log.MinimumLevel = LogLevel.Warning;
+ var oldLoggingLevel = Log.DefaultMinimumLevel;
+ Log.DefaultMinimumLevel = LogLevel.Warning;
await RefreshDataAsync();
if (!_indexesHaveBeenConfigured)
@@ -169,7 +167,7 @@ await _configuration.Client.DeleteByQueryAsync(new DeleteByQueryRequest(indexes)
await GetService>().DeleteQueueAsync();
- Log.MinimumLevel = oldLoggingLevel;
+ Log.DefaultMinimumLevel = oldLoggingLevel;
}
finally
{
diff --git a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs
index 35b10bd535..42d2871cc7 100644
--- a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs
+++ b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs
@@ -12,6 +12,7 @@
using Exceptionless.Core.Queues.Models;
using Exceptionless.Core.Repositories;
using Exceptionless.Core.Repositories.Configuration;
+using Exceptionless.Core.Utility;
using Exceptionless.DateTimeExtensions;
using Exceptionless.Tests.Utility;
using Foundatio.Repositories;
@@ -243,7 +244,7 @@ public async Task CloseExistingAutoSessionAsync()
var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject());
Assert.DoesNotContain(contexts, c => c.HasError);
- Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded));
+ Assert.Equal(1, contexts.Count(c => c is { IsCancelled: true, IsDiscarded: true }));
Assert.Contains(contexts, c => c.IsProcessed);
await RefreshDataAsync();
@@ -304,7 +305,7 @@ public async Task WillMarkAutoSessionHeartbeatStackHiddenAsync()
var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject());
Assert.DoesNotContain(contexts, c => c.HasError);
- Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded));
+ Assert.Equal(1, contexts.Count(c => c is { IsCancelled: true, IsDiscarded: true }));
Assert.Equal(0, contexts.Count(c => c.IsProcessed));
await RefreshDataAsync();
@@ -459,7 +460,7 @@ public async Task CloseExistingManualSessionAsync()
var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject());
Assert.DoesNotContain(contexts, c => c.HasError);
- Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded));
+ Assert.Equal(1, contexts.Count(c => c is { IsCancelled: true, IsDiscarded: true }));
Assert.Contains(contexts, c => c.IsProcessed);
events =
@@ -518,7 +519,7 @@ public async Task WillMarkManualSessionHeartbeatStackHiddenAsync()
var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject());
Assert.DoesNotContain(contexts, c => c.HasError);
- Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded));
+ Assert.Equal(1, contexts.Count(c => c is { IsCancelled: true, IsDiscarded: true }));
Assert.Equal(0, contexts.Count(c => c.IsProcessed));
await RefreshDataAsync();
@@ -929,7 +930,7 @@ public async Task WillHandleDiscardedStack()
var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans);
var project = ProjectData.GenerateSampleProject();
- var ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
+ var ev = EventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
var context = await _pipeline.RunAsync(ev, organization, project);
Assert.True(context.IsProcessed);
Assert.False(context.HasError);
@@ -944,8 +945,7 @@ public async Task WillHandleDiscardedStack()
stack.Status = StackStatus.Discarded;
stack = await _stackRepository.SaveAsync(stack, o => o.ImmediateConsistency());
-
- ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, stackId: ev.StackId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
+ ev = EventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
context = await _pipeline.RunAsync(ev, organization, project);
Assert.False(context.IsProcessed);
Assert.False(context.HasError);
@@ -953,13 +953,93 @@ public async Task WillHandleDiscardedStack()
Assert.True(context.IsDiscarded);
await RefreshDataAsync();
- ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
+ ev = EventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
context = await _pipeline.RunAsync(ev, organization, project);
Assert.False(context.IsProcessed);
Assert.False(context.HasError);
Assert.True(context.IsCancelled);
Assert.True(context.IsDiscarded);
+ }
+
+ [Theory]
+ [InlineData(StackStatus.Regressed, false, null, null)]
+ [InlineData(StackStatus.Fixed, true, "1.0.0", null)] // A fixed stack should not be marked as regressed if the event has no version.
+ [InlineData(StackStatus.Regressed, false, null, "1.0.0")]
+ [InlineData(StackStatus.Regressed, false, "1.0.0", "1.0.0")] // A fixed stack should not be marked as regressed if the event has the same version.
+ [InlineData(StackStatus.Fixed, true, "2.0.0", "1.0.0")]
+ [InlineData(StackStatus.Regressed, false, null, "1.0.1")]
+ [InlineData(StackStatus.Regressed, false, "1.0.0", "1.0.1")]
+ public async Task CanDiscardStackEventsBasedOnEventVersion(StackStatus expectedStatus, bool expectedDiscard, string? stackFixedInVersion, string? eventSemanticVersion)
+ {
+ var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId, o => o.Cache());
+ var project = await _projectRepository.GetByIdAsync(TestConstants.ProjectId, o => o.Cache());
+
+ var ev = EventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
+ var context = await _pipeline.RunAsync(ev, organization, project);
+
+ var stack = context.Stack;
+ Assert.NotNull(stack);
+ Assert.Equal(StackStatus.Open, stack.Status);
+
+ Assert.True(context.IsProcessed);
+ Assert.False(context.HasError);
+ Assert.False(context.IsCancelled);
+ Assert.False(context.IsDiscarded);
+
+ var semanticVersionParser = GetService();
+ var fixedInVersion = semanticVersionParser.Parse(stackFixedInVersion);
+ stack.MarkFixed(fixedInVersion);
+ await _stackRepository.SaveAsync(stack, o => o.ImmediateConsistency());
+
+ await RefreshDataAsync();
+ ev = EventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow, semver: eventSemanticVersion);
+ context = await _pipeline.RunAsync(ev, organization, project);
+
+ stack = context.Stack;
+ Assert.NotNull(stack);
+ Assert.Equal(expectedStatus, stack.Status);
+ Assert.Equal(expectedDiscard, context.IsCancelled);
+ Assert.Equal(expectedDiscard, context.IsDiscarded);
+ }
+
+ [Theory]
+ [InlineData("1.0.0", null)] // A fixed stack should not be marked as regressed if the event has no version.
+ [InlineData("2.0.0", "1.0.0")]
+ public async Task WillNotDiscardStackEventsBasedOnEventVersionWithFreePlan(string stackFixedInVersion, string? eventSemanticVersion)
+ {
+ var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId3, o => o.Cache());
+
+ var plans = GetService();
+ Assert.Equal(plans.FreePlan.Id, organization.PlanId);
+
+ var project = await _projectRepository.AddAsync(ProjectData.GenerateProject(organizationId: organization.Id), o => o.ImmediateConsistency().Cache());
+
+ var ev = EventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow);
+ var context = await _pipeline.RunAsync(ev, organization, project);
+
+ var stack = context.Stack;
+ Assert.NotNull(stack);
+ Assert.Equal(StackStatus.Open, stack.Status);
+
+ Assert.True(context.IsProcessed);
+ Assert.False(context.HasError);
+ Assert.False(context.IsCancelled);
+ Assert.False(context.IsDiscarded);
+
+ var semanticVersionParser = GetService();
+ var fixedInVersion = semanticVersionParser.Parse(stackFixedInVersion);
+ stack.MarkFixed(fixedInVersion);
+ await _stackRepository.SaveAsync(stack, o => o.ImmediateConsistency());
+
await RefreshDataAsync();
+ ev = EventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow, semver: eventSemanticVersion);
+ context = await _pipeline.RunAsync(ev, organization, project);
+
+ stack = context.Stack;
+ Assert.NotNull(stack);
+ Assert.Equal(StackStatus.Fixed, stack.Status);
+ Assert.False(context.IsCancelled);
+ Assert.False(context.IsDiscarded);
}
[Theory]
@@ -1164,10 +1244,10 @@ private async Task CreateProjectDataAsync(BillingPlan? plan = null)
organization.SuspensionDate = SystemClock.UtcNow;
}
- await _organizationRepository.AddAsync(organization, o => o.Cache());
+ await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache());
}
- await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.Cache());
+ await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.ImmediateConsistency().Cache());
foreach (var user in UserData.GenerateSampleUsers())
{
@@ -1180,10 +1260,8 @@ private async Task CreateProjectDataAsync(BillingPlan? plan = null)
if (!user.IsEmailAddressVerified)
user.CreateVerifyEmailAddressToken();
- await _userRepository.AddAsync(user, o => o.Cache());
+ await _userRepository.AddAsync(user, o => o.ImmediateConsistency().Cache());
}
-
- await RefreshDataAsync();
}
private static PersistentEvent GenerateEvent(DateTimeOffset? occurrenceDate = null, string? userIdentity = null, string? type = null, string? sessionId = null)
diff --git a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs
index b5e3b8434c..cba7c1fd38 100644
--- a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs
+++ b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs
@@ -133,7 +133,6 @@ public async Task GetNextEventIdInStackTestAsync()
_logger.LogDebug("");
_logger.LogDebug("Tests:");
- await RefreshDataAsync();
Assert.Equal(_ids.Count, await _repository.CountAsync());
for (int i = 0; i < sortedIds.Count; i++)
{
@@ -164,9 +163,8 @@ public async Task CanGetPreviousAndNExtEventIdWithFilterTestAsync()
public async Task GetByReferenceIdAsync()
{
string referenceId = ObjectId.GenerateNewId().ToString();
- await _repository.AddAsync(EventData.GenerateEvents(3, TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, referenceId: referenceId).ToList());
+ await _repository.AddAsync(EventData.GenerateEvents(3, TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, referenceId: referenceId).ToList(), o => o.ImmediateConsistency());
- await RefreshDataAsync();
var results = await _repository.GetByReferenceIdAsync(TestConstants.ProjectId, referenceId);
Assert.True(results.Total > 0);
Assert.NotNull(results.Documents.First());
@@ -193,9 +191,8 @@ public async Task GetOpenSessionsAsync()
closedSession
};
- await _repository.AddAsync(events);
+ await _repository.AddAsync(events, o => o.ImmediateConsistency());
- await RefreshDataAsync();
var results = await _repository.GetOpenSessionsAsync(SystemClock.UtcNow.SubtractMinutes(30));
Assert.Equal(3, results.Total);
}
@@ -204,13 +201,12 @@ public async Task GetOpenSessionsAsync()
public async Task RemoveAllByClientIpAndDateAsync()
{
const string _clientIpAddress = "123.123.12.255";
-
const int NUMBER_OF_EVENTS_TO_CREATE = 50;
+
var events = EventData.GenerateEvents(NUMBER_OF_EVENTS_TO_CREATE, TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, startDate: SystemClock.UtcNow.SubtractDays(2), endDate: SystemClock.UtcNow).ToList();
events.ForEach(e => e.AddRequestInfo(new RequestInfo { ClientIpAddress = _clientIpAddress }));
- await _repository.AddAsync(events);
+ await _repository.AddAsync(events, o => o.ImmediateConsistency());
- await RefreshDataAsync();
events = (await _repository.GetByProjectIdAsync(TestConstants.ProjectId, o => o.PageLimit(NUMBER_OF_EVENTS_TO_CREATE))).Documents.ToList();
Assert.Equal(NUMBER_OF_EVENTS_TO_CREATE, events.Count);
events.ForEach(e =>
@@ -220,9 +216,8 @@ public async Task RemoveAllByClientIpAndDateAsync()
Assert.Equal(_clientIpAddress, ri.ClientIpAddress);
});
- await _repository.RemoveAllAsync(TestConstants.OrganizationId, _clientIpAddress, SystemClock.UtcNow.SubtractDays(3), SystemClock.UtcNow.AddDays(2));
+ await _repository.RemoveAllAsync(TestConstants.OrganizationId, _clientIpAddress, SystemClock.UtcNow.SubtractDays(3), SystemClock.UtcNow.AddDays(2), o => o.ImmediateConsistency());
- await RefreshDataAsync();
events = (await _repository.GetByProjectIdAsync(TestConstants.ProjectId, o => o.PageLimit(NUMBER_OF_EVENTS_TO_CREATE))).Documents.ToList();
Assert.Empty(events);
}
@@ -236,7 +231,7 @@ private async Task CreateDataAsync()
var occurrenceDateMid = baseDate;
var occurrenceDateEnd = baseDate.AddMinutes(30);
- await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId));
+ await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId), o => o.ImmediateConsistency());
var occurrenceDates = new List {
occurrenceDateStart,
@@ -255,10 +250,8 @@ private async Task CreateDataAsync()
foreach (var date in occurrenceDates)
{
- var ev = await _repository.AddAsync(EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, stackId: TestConstants.StackId, occurrenceDate: date));
+ var ev = await _repository.AddAsync(EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, stackId: TestConstants.StackId, occurrenceDate: date), o => o.ImmediateConsistency());
_ids.Add(Tuple.Create(ev.Id, date));
}
-
- await RefreshDataAsync();
}
}
diff --git a/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs
index 5332a85310..7b92db1318 100644
--- a/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs
+++ b/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs
@@ -31,8 +31,7 @@ public async Task CanCreateUpdateRemoveAsync()
var organization = new Organization { Name = "Test Organization", PlanId = _plans.FreePlan.Id };
Assert.Null(organization.Id);
- await _repository.AddAsync(organization);
- await RefreshDataAsync();
+ await _repository.AddAsync(organization, o => o.ImmediateConsistency());
Assert.NotNull(organization.Id);
organization = await _repository.GetByIdAsync(organization.Id);
@@ -40,7 +39,6 @@ public async Task CanCreateUpdateRemoveAsync()
organization.Name = "New organization";
await _repository.SaveAsync(organization);
-
await _repository.RemoveAsync(organization.Id);
}
@@ -51,8 +49,7 @@ public async Task CanAddAndGetByCachedAsync()
Assert.Null(organization.Id);
Assert.Equal(0, _cache.Count);
- await _repository.AddAsync(organization, o => o.Cache());
- await RefreshDataAsync();
+ await _repository.AddAsync(organization, o => o.ImmediateConsistency().Cache());
Assert.NotNull(organization.Id);
Assert.Equal(1, _cache.Count);
@@ -62,8 +59,7 @@ public async Task CanAddAndGetByCachedAsync()
Assert.NotNull(organization.Id);
Assert.Equal(1, _cache.Count);
- await _repository.RemoveAllAsync();
- await RefreshDataAsync();
+ await _repository.RemoveAllAsync(o => o.ImmediateConsistency());
Assert.Equal(0, _cache.Count);
}
}
diff --git a/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs
index 60c6057fb1..81042f8b5d 100644
--- a/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs
+++ b/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs
@@ -36,22 +36,22 @@ await _repository.AddAsync(new List {
Assert.Equal(3, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total);
await _repository.RemoveAllByProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectId);
-
await RefreshDataAsync();
+
Assert.Equal(4, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total);
Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total);
Assert.Equal(3, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total);
await _repository.RemoveAllByProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectIdWithNoRoles);
-
await RefreshDataAsync();
+
Assert.Equal(3, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total);
Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total);
Assert.Equal(2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total);
await _repository.RemoveAllByOrganizationIdAsync(TestConstants.OrganizationId);
-
await RefreshDataAsync();
+
Assert.Equal(0, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total);
Assert.Equal(0, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total);
Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total);
@@ -68,8 +68,7 @@ await _repository.AddAsync(new List {
Assert.Equal(1, (await _repository.GetByTypeAndUserIdAsync(TokenType.Access, TestConstants.UserId)).Total);
Assert.Equal(1, (await _repository.GetByTypeAndUserIdAsync(TokenType.Authentication, TestConstants.UserId)).Total);
- await _repository.RemoveAllByUserIdAsync(TestConstants.UserId);
- await RefreshDataAsync();
+ await _repository.RemoveAllByUserIdAsync(TestConstants.UserId, o => o.ImmediateConsistency());
Assert.Equal(0, (await _repository.GetByTypeAndUserIdAsync(TokenType.Access, TestConstants.UserId)).Total);
Assert.Equal(0, (await _repository.GetByTypeAndUserIdAsync(TokenType.Authentication, TestConstants.UserId)).Total);
Assert.Equal(1, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total);
diff --git a/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs
index 81c1718a60..7918fe2a0f 100644
--- a/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs
+++ b/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs
@@ -1,6 +1,7 @@
using Exceptionless.Core.Models;
using Exceptionless.Core.Repositories;
using Exceptionless.Tests.Utility;
+using Foundatio.Repositories;
using Xunit;
using Xunit.Abstractions;
@@ -18,34 +19,33 @@ public WebHookRepositoryTests(ITestOutputHelper output, AppWebHostFactory factor
[Fact]
public async Task GetByOrganizationIdOrProjectIdAsync()
{
- await _repository.AddAsync(new WebHook
+ await _repository.AddAsync([new WebHook
{
OrganizationId = TestConstants.OrganizationId,
Url = "http://localhost:40000/test",
EventTypes =
[WebHook.KnownEventTypes.StackPromoted],
Version = WebHook.KnownVersions.Version2
- });
- await _repository.AddAsync(new WebHook
- {
- OrganizationId = TestConstants.OrganizationId,
- ProjectId = TestConstants.ProjectId,
- Url = "http://localhost:40000/test1",
- EventTypes =
+ },
+ new WebHook
+ {
+ OrganizationId = TestConstants.OrganizationId,
+ ProjectId = TestConstants.ProjectId,
+ Url = "http://localhost:40000/test1",
+ EventTypes =
[WebHook.KnownEventTypes.StackPromoted],
- Version = WebHook.KnownVersions.Version2
- });
- await _repository.AddAsync(new WebHook
- {
- OrganizationId = TestConstants.OrganizationId,
- ProjectId = TestConstants.ProjectIdWithNoRoles,
- Url = "http://localhost:40000/test1",
- EventTypes =
+ Version = WebHook.KnownVersions.Version2
+ },
+ new WebHook
+ {
+ OrganizationId = TestConstants.OrganizationId,
+ ProjectId = TestConstants.ProjectIdWithNoRoles,
+ Url = "http://localhost:40000/test1",
+ EventTypes =
[WebHook.KnownEventTypes.StackPromoted],
- Version = WebHook.KnownVersions.Version2
- });
+ Version = WebHook.KnownVersions.Version2
+ }], o => o.ImmediateConsistency());
- await RefreshDataAsync();
Assert.Equal(3, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total);
Assert.Equal(2, (await _repository.GetByOrganizationIdOrProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectId)).Total);
Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total);
@@ -55,7 +55,7 @@ await _repository.AddAsync(new WebHook
[Fact]
public async Task CanSaveWebHookVersionAsync()
{
- await _repository.AddAsync(new WebHook
+ await _repository.AddAsync([new WebHook
{
OrganizationId = TestConstants.OrganizationId,
ProjectId = TestConstants.ProjectId,
@@ -63,18 +63,17 @@ await _repository.AddAsync(new WebHook
EventTypes =
[WebHook.KnownEventTypes.StackPromoted],
Version = WebHook.KnownVersions.Version1
- });
- await _repository.AddAsync(new WebHook
- {
- OrganizationId = TestConstants.OrganizationId,
- ProjectId = TestConstants.ProjectIdWithNoRoles,
- Url = "http://localhost:40000/test1",
- EventTypes =
+ },
+ new WebHook
+ {
+ OrganizationId = TestConstants.OrganizationId,
+ ProjectId = TestConstants.ProjectIdWithNoRoles,
+ Url = "http://localhost:40000/test1",
+ EventTypes =
[WebHook.KnownEventTypes.StackPromoted],
- Version = WebHook.KnownVersions.Version2
- });
+ Version = WebHook.KnownVersions.Version2
+ }], o => o.ImmediateConsistency());
- await RefreshDataAsync();
Assert.Equal(WebHook.KnownVersions.Version1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Documents.First().Version);
Assert.Equal(WebHook.KnownVersions.Version2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Documents.First().Version);
}
diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs
index 5b77391d6e..d4766cc34f 100644
--- a/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs
+++ b/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs
@@ -31,13 +31,13 @@ protected override async Task ResetDataAsync()
{
await base.ResetDataAsync();
- var oldLoggingLevel = Log.MinimumLevel;
- Log.MinimumLevel = LogLevel.Warning;
+ var oldLoggingLevel = Log.DefaultMinimumLevel;
+ Log.DefaultMinimumLevel = LogLevel.Warning;
await StackData.CreateSearchDataAsync(_stackRepository, GetService());
await EventData.CreateSearchDataAsync(GetService(), _eventRepository, GetService());
- Log.MinimumLevel = oldLoggingLevel;
+ Log.DefaultMinimumLevel = oldLoggingLevel;
}
[Theory]
diff --git a/tests/Exceptionless.Tests/Services/StackServiceTests.cs b/tests/Exceptionless.Tests/Services/StackServiceTests.cs
index 8a2abdfa54..c19ba11037 100644
--- a/tests/Exceptionless.Tests/Services/StackServiceTests.cs
+++ b/tests/Exceptionless.Tests/Services/StackServiceTests.cs
@@ -43,8 +43,8 @@ public async Task IncrementUsage_OnlyChangeCache()
Assert.True(occurrenceSet.IsNull || !occurrenceSet.HasValue || occurrenceSet.Value.Count == 0);
var firstUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1));
- await RefreshDataAsync();
await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, firstUtcNow, firstUtcNow, 1);
+ await RefreshDataAsync();
// Assert stack state has no change after increment usage
stack = await _stackRepository.GetByIdAsync(TestConstants.StackId);
@@ -60,7 +60,6 @@ public async Task IncrementUsage_OnlyChangeCache()
Assert.Single(occurrenceSet.Value);
var secondUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1));
- await RefreshDataAsync();
await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, secondUtcNow, secondUtcNow, 2);
// Assert state in cache has been changed after increment usage again
diff --git a/tests/Exceptionless.Tests/Stats/AggregationTests.cs b/tests/Exceptionless.Tests/Stats/AggregationTests.cs
index f33e5cb0ce..e15204fd5c 100644
--- a/tests/Exceptionless.Tests/Stats/AggregationTests.cs
+++ b/tests/Exceptionless.Tests/Stats/AggregationTests.cs
@@ -207,16 +207,14 @@ public async Task CanGetSessionAggregationsAsync()
private async Task CreateDataAsync(int eventCount = 0, bool multipleProjects = true)
{
- var orgs = OrganizationData.GenerateSampleOrganizations(_billingManager, _plans);
- await _organizationRepository.AddAsync(orgs, o => o.Cache());
+ var organizations = OrganizationData.GenerateSampleOrganizations(_billingManager, _plans);
+ await _organizationRepository.AddAsync(organizations, o => o.ImmediateConsistency().Cache());
var projects = ProjectData.GenerateSampleProjects();
- await _projectRepository.AddAsync(projects, o => o.Cache());
- await RefreshDataAsync();
+ await _projectRepository.AddAsync(projects, o => o.ImmediateConsistency().Cache());
if (eventCount > 0)
- await CreateEventsAsync(eventCount, multipleProjects ? projects.Select(p => p.Id).ToArray() : [TestConstants.ProjectId
- ]);
+ await CreateEventsAsync(eventCount, multipleProjects ? projects.Select(p => p.Id).ToArray() : [TestConstants.ProjectId]);
}
private async Task CreateEventsAsync(int eventCount, string[]? projectIds, decimal? value = -1)
diff --git a/tests/Exceptionless.Tests/TestWithServices.cs b/tests/Exceptionless.Tests/TestWithServices.cs
index 760522b953..eff5eb0e1b 100644
--- a/tests/Exceptionless.Tests/TestWithServices.cs
+++ b/tests/Exceptionless.Tests/TestWithServices.cs
@@ -23,7 +23,7 @@ public class TestWithServices : TestWithLoggingBase, IAsyncLifetime
public TestWithServices(ITestOutputHelper output) : base(output)
{
- Log.MinimumLevel = LogLevel.Information;
+ Log.DefaultMinimumLevel = LogLevel.Information;
Log.SetLogLevel(LogLevel.Warning);
Log.SetLogLevel(LogLevel.Warning);
Log.SetLogLevel(LogLevel.Warning);