Skip to content

Commit bb51999

Browse files
authored
Next: Table Bulk Actions
Next: Table Bulk Actions
2 parents d58e81e + 4dfa16b commit bb51999

38 files changed

+985
-342
lines changed

src/Exceptionless.Web/ClientApp/.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
},
5151
"editor.formatOnSave": true,
5252
"eslint.validate": ["javascript", "svelte"],
53-
"search.exclude": {
53+
"files.exclude": {
5454
".svelte-kit": true,
5555
"build": true
5656
},

src/Exceptionless.Web/ClientApp/api-templates/class-data-contract.ejs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
const { contract, utils } = it;
33
const { formatDescription, require, _ } = utils;
44
%>
5-
export class <%~ contract.name %> {
5+
class <%~ contract.name %> {
66
<% for (const field of contract.$content) { %>
77
<%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %>
88
<%~ includeFile('./object-field-class-validator.ejs', { ...it, field }) %>
9-
<%~ field.name %><%~ field.isRequired || !field.nullable ? '!' : '' %><%~ field.nullable ? '?' : '' %>: <%~ field.value.replaceAll('any', 'unknown') %>;
9+
<%~ field.name %><%~ (field.isRequired && !field.nullable) || !field.nullable || !field.nullable ? '!' : '' %><%~ field.nullable ? '?' : '' %>: <%~ field.value.replaceAll('any', 'unknown') %>;
1010
<% } %>
1111
}
12-

src/Exceptionless.Web/ClientApp/api-templates/data-contracts.ejs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const dataContractTemplates = {
2828
},
2929
type: (contract) => {
3030
return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
31-
},
31+
}
3232
}
3333
%>
3434

@@ -47,6 +47,7 @@ import {
4747
IsNumber,
4848
IsMongoId,
4949
IsUrl,
50+
Matches,
5051
Min,
5152
MinDate,
5253
MinLength,

src/Exceptionless.Web/ClientApp/api-templates/object-field-class-validator.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const validationDecorators = _.compact([
2828
!_.isUndefined(field.format) && getFormatValidation(field),
2929
!_.isUndefined(field.minLength) && `@MinLength(${field.minLength}, { message: '${field.name} must be at least ${field.minLength} characters long.' })`,
3030
!_.isUndefined(field.maxLength) && `@MaxLength(${field.maxLength}, { message: '${field.name} must be at most ${field.maxLength} characters long.' })`,
31-
!_.isUndefined(field.pattern) && `@Matches(${field.pattern}, { message: '${field.name} must match the pattern ${field.pattern}.' })`,
31+
!_.isUndefined(field.pattern) && `@Matches(/${field.pattern}/, { message: '${field.name} must match the pattern ${field.pattern}.' })`,
3232
!_.isUndefined(field.type) && (field.type === "object" || (field.type === "array" && field.items.$ref)) && `@ValidateNested({ message: '${field.name} must be a valid nested object.' })`,
3333
]);
3434

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

Lines changed: 79 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Exceptionless.Web/ClientApp/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,37 @@
2323
"upgrade": "ncu -i"
2424
},
2525
"devDependencies": {
26-
"@iconify-json/lucide": "^1.2.20",
26+
"@iconify-json/lucide": "^1.2.21",
2727
"@playwright/test": "^1.49.1",
2828
"@sveltejs/adapter-static": "^3.0.8",
2929
"@sveltejs/kit": "^2.15.1",
3030
"@sveltejs/vite-plugin-svelte": "^5.0.3",
3131
"@types/eslint": "^9.6.1",
32-
"@types/node": "^22.10.2",
32+
"@types/node": "^22.10.5",
3333
"@types/throttle-debounce": "^5.0.2",
3434
"autoprefixer": "^10.4.20",
3535
"cross-env": "^7.0.3",
3636
"eslint": "^9.17.0",
3737
"eslint-config-prettier": "^9.1.0",
38-
"eslint-plugin-perfectionist": "^4.4.0",
38+
"eslint-plugin-perfectionist": "^4.6.0",
3939
"eslint-plugin-svelte": "^2.46.1",
4040
"npm-run-all": "^4.1.5",
4141
"postcss": "^8.4.49",
4242
"prettier": "^3.4.2",
4343
"prettier-plugin-svelte": "^3.3.2",
4444
"prettier-plugin-tailwindcss": "^0.6.9",
45-
"svelte": "^5.16.0",
45+
"svelte": "^5.16.1",
4646
"svelte-check": "^4.1.1",
4747
"swagger-typescript-api": "^13.0.23",
4848
"tslib": "^2.8.1",
4949
"typescript": "^5.7.2",
50-
"typescript-eslint": "^8.18.2",
51-
"vite": "^6.0.6",
50+
"typescript-eslint": "^8.19.0",
51+
"vite": "^6.0.7",
5252
"vitest": "2.1.6"
5353
},
5454
"dependencies": {
5555
"@exceptionless/browser": "^3.1.0",
56-
"@exceptionless/fetchclient": "^0.31.0",
56+
"@exceptionless/fetchclient": "^0.33.0",
5757
"@iconify-json/mdi": "^1.2.2",
5858
"@tanstack/svelte-query": "https://pkg.pr.new/@tanstack/svelte-query@28f98f9",
5959
"@tanstack/svelte-query-devtools": "https://pkg.pr.new/@tanstack/svelte-query-devtools@28f98f9",
@@ -66,7 +66,7 @@
6666
"mode-watcher": "^0.5.0",
6767
"oidc-client-ts": "^3.1.0",
6868
"pretty-ms": "^9.2.0",
69-
"runed": "^0.22.0",
69+
"runed": "^0.23.0",
7070
"svelte-sonner": "^0.3.28",
7171
"svelte-time": "^0.9.0",
7272
"sveltekit-superforms": "^2.22.1",

src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { WebSocketMessageValue } from '$features/websockets/models';
2-
import type { CountResult } from '$shared/models';
2+
import type { CountResult, WorkInProgressResult } from '$shared/models';
33

44
import { accessToken } from '$features/auth/index.svelte';
5-
import { DEFAULT_OFFSET } from '$features/shared/api/api.svelte';
5+
import { DEFAULT_OFFSET } from '$shared/api/api.svelte';
66
import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
7-
import { createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query';
7+
import { createMutation, createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query';
88

99
import type { PersistentEvent } from './models';
1010

@@ -31,13 +31,20 @@ export async function invalidatePersistentEventQueries(queryClient: QueryClient,
3131

3232
export const queryKeys = {
3333
count: () => [...queryKeys.type, 'count'] as const,
34+
deleteEvent: (ids: string[] | undefined) => [...queryKeys.type, 'delete', ...(ids ?? [])] as const,
3435
id: (id: string | undefined) => [...queryKeys.type, id] as const,
3536
projectsCount: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const,
3637
stacks: (id: string | undefined) => [...queryKeys.type, 'stacks', id] as const,
3738
stacksCount: (id: string | undefined) => [...queryKeys.stacks(id), 'count'] as const,
3839
type: ['PersistentEvent'] as const
3940
};
4041

42+
export interface DeleteEventsRequest {
43+
route: {
44+
ids: string[] | undefined;
45+
};
46+
}
47+
4148
export interface GetCountRequest {
4249
params?: {
4350
aggregations?: string;
@@ -67,6 +74,7 @@ export interface GetEventsParams {
6774
limit?: number;
6875
mode?: GetEventsMode;
6976
offset?: string;
77+
page?: number;
7078
sort?: string;
7179
time?: string;
7280
}
@@ -113,6 +121,26 @@ export interface GetStackEventsRequest {
113121
};
114122
}
115123

124+
export function deleteEvent(request: DeleteEventsRequest) {
125+
const queryClient = useQueryClient();
126+
return createMutation<WorkInProgressResult, ProblemDetails, void>(() => ({
127+
enabled: () => !!accessToken.value && !!request.route.ids?.length,
128+
mutationFn: async () => {
129+
const client = useFetchClient();
130+
const response = await client.deleteJSON<WorkInProgressResult>(`events/${request.route.ids?.join(',')}`);
131+
132+
return response.data!;
133+
},
134+
mutationKey: queryKeys.deleteEvent(request.route.ids),
135+
onError: () => {
136+
request.route.ids?.forEach((id) => queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }));
137+
},
138+
onSuccess: () => {
139+
request.route.ids?.forEach((id) => queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }));
140+
}
141+
}));
142+
}
143+
116144
export function getCountQuery(request: GetCountRequest) {
117145
const queryClient = useQueryClient();
118146

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script lang="ts">
2+
import Number from '$comp/formatters/Number.svelte';
3+
import * as AlertDialog from '$comp/ui/alert-dialog';
4+
import { buttonVariants } from '$comp/ui/button';
5+
6+
interface Props {
7+
count?: number;
8+
open: boolean;
9+
remove: () => Promise<void>;
10+
}
11+
12+
let { count = 1, open = $bindable(), remove }: Props = $props();
13+
14+
async function onSubmit() {
15+
await remove();
16+
open = false;
17+
}
18+
</script>
19+
20+
<AlertDialog.Root bind:open>
21+
<AlertDialog.Content>
22+
<AlertDialog.Header>
23+
<AlertDialog.Title>
24+
Delete
25+
{#if count === 1}
26+
Event
27+
{:else}
28+
<Number value={count} /> Events
29+
{/if}
30+
</AlertDialog.Title>
31+
<AlertDialog.Description>
32+
Are you sure you want to delete
33+
{#if count === 1}
34+
this event
35+
{:else}
36+
<Number value={count} /> events
37+
{/if}?
38+
</AlertDialog.Description>
39+
</AlertDialog.Header>
40+
<AlertDialog.Footer>
41+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
42+
<AlertDialog.Action class={buttonVariants({ variant: 'destructive' })} onclick={onSubmit}>
43+
Delete
44+
{#if count === 1}
45+
Event
46+
{:else}
47+
<Number value={count} /> Events
48+
{/if}
49+
</AlertDialog.Action>
50+
</AlertDialog.Footer>
51+
</AlertDialog.Content>
52+
</AlertDialog.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script module lang="ts">
2+
type TData = unknown;
3+
</script>
4+
5+
<script generics="TData" lang="ts">
6+
import Button from '$comp/ui/button/button.svelte';
7+
import * as DropdownMenu from '$comp/ui/dropdown-menu';
8+
import { deleteEvent } from '$features/events/api.svelte';
9+
import { type Table as SvelteTable } from '@tanstack/svelte-table';
10+
import { toast } from 'svelte-sonner';
11+
import ChevronDown from '~icons/mdi/chevron-down';
12+
13+
import RemoveEventDialog from '../dialogs/RemoveEventDialog.svelte';
14+
15+
interface Props {
16+
table: SvelteTable<TData>;
17+
}
18+
19+
let { table }: Props = $props();
20+
const ids = $derived(table.getSelectedRowModel().flatRows.map((row) => row.id));
21+
22+
let openRemoveEventDialog = $state<boolean>(false);
23+
24+
const removeEvents = deleteEvent({
25+
route: {
26+
get ids() {
27+
return ids;
28+
}
29+
}
30+
});
31+
32+
async function remove() {
33+
await removeEvents.mutateAsync();
34+
35+
if (ids.length === 1) {
36+
toast.success('Successfully deleted event.');
37+
} else {
38+
toast.success(`Successfully deleted ${Intl.NumberFormat().format(ids.length)} events.`);
39+
}
40+
41+
table.resetRowSelection();
42+
}
43+
</script>
44+
45+
<DropdownMenu.Root>
46+
<DropdownMenu.Trigger>
47+
<Button variant="outline">
48+
Bulk Actions
49+
<ChevronDown class="size-4" />
50+
</Button>
51+
</DropdownMenu.Trigger>
52+
<DropdownMenu.Content>
53+
<DropdownMenu.Group>
54+
<DropdownMenu.GroupHeading>Bulk Actions</DropdownMenu.GroupHeading>
55+
<DropdownMenu.Item onclick={() => (openRemoveEventDialog = true)} class="text-destructive" title="Delete event">Delete</DropdownMenu.Item>
56+
</DropdownMenu.Group>
57+
</DropdownMenu.Content>
58+
</DropdownMenu.Root>
59+
60+
{#if openRemoveEventDialog}
61+
<RemoveEventDialog bind:open={openRemoveEventDialog} {remove} count={ids.length} />
62+
{/if}

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
import type { EventSummaryModel, SummaryTemplateKeys } from '../summary/index';
99
1010
interface Props {
11+
bodyChildren?: Snippet;
12+
footerChildren?: Snippet;
1113
isLoading: boolean;
1214
limit: number;
13-
rowclick?: (row: EventSummaryModel<SummaryTemplateKeys>) => void;
15+
rowClick?: (row: EventSummaryModel<SummaryTemplateKeys>) => void;
1416
table: Table<EventSummaryModel<SummaryTemplateKeys>>;
1517
toolbarChildren?: Snippet;
1618
}
1719
18-
let { isLoading, limit = $bindable(), rowclick, table, toolbarChildren }: Props = $props();
20+
let { bodyChildren, footerChildren, isLoading, limit = $bindable(), rowClick, table, toolbarChildren }: Props = $props();
1921
</script>
2022

2123
<DataTable.Root>
@@ -24,16 +26,28 @@
2426
{@render toolbarChildren()}
2527
{/if}
2628
</DataTable.Toolbar>
27-
<DataTable.Body {rowclick} {table}>
29+
<DataTable.Body {rowClick} {table}>
2830
{#if isLoading}
2931
<DelayedRender>
3032
<DataTable.Loading {table} />
3133
</DelayedRender>
3234
{:else}
3335
<DataTable.Empty {table} />
3436
{/if}
37+
{#if bodyChildren}
38+
{@render bodyChildren()}
39+
{/if}
3540
</DataTable.Body>
36-
<DataTable.Pagination {table}>
37-
<DataTable.PageSize bind:value={limit} {table}></DataTable.PageSize>
38-
</DataTable.Pagination>
41+
<DataTable.Footer {table} class="space-x-6 lg:space-x-8">
42+
{#if footerChildren}
43+
{@render footerChildren()}
44+
{:else}
45+
<DataTable.Selection {table} />
46+
<DataTable.PageSize bind:value={limit} {table}></DataTable.PageSize>
47+
<div class="flex items-center space-x-6 lg:space-x-8">
48+
<DataTable.PageCount {table} />
49+
<DataTable.Pagination {table} />
50+
</div>
51+
{/if}
52+
</DataTable.Footer>
3953
</DataTable.Root>

0 commit comments

Comments
 (0)