Skip to content

Commit f1093dc

Browse files
committed
Added client-settings page
1 parent 7e7cd89 commit f1093dc

File tree

12 files changed

+594
-7
lines changed

12 files changed

+594
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<script lang="ts">
2+
import { A, P } from '$comp/typography';
3+
import * as AlertDialog from '$comp/ui/alert-dialog';
4+
import * as Form from '$comp/ui/form';
5+
import { Input } from '$comp/ui/input';
6+
import { defaults, superForm } from 'sveltekit-superforms';
7+
import { classvalidatorClient } from 'sveltekit-superforms/adapters';
8+
9+
import { ClientConfigurationSetting } from '../../models';
10+
11+
interface Props {
12+
open: boolean;
13+
save: (setting: ClientConfigurationSetting) => Promise<void>;
14+
}
15+
let { open = $bindable(), save }: Props = $props();
16+
17+
const form = superForm(defaults(new ClientConfigurationSetting(), classvalidatorClient(ClientConfigurationSetting)), {
18+
dataType: 'json',
19+
async onUpdate({ form }) {
20+
if (!form.valid) {
21+
return;
22+
}
23+
24+
await save(form.data);
25+
open = false;
26+
},
27+
SPA: true,
28+
validators: classvalidatorClient(ClientConfigurationSetting)
29+
});
30+
31+
const { enhance, form: formData } = form;
32+
</script>
33+
34+
<AlertDialog.Root bind:open>
35+
<AlertDialog.Content class="sm:max-w-[425px]">
36+
<form method="POST" use:enhance>
37+
<AlertDialog.Header>
38+
<AlertDialog.Title>Add New Configuration Setting</AlertDialog.Title>
39+
<AlertDialog.Description
40+
>The <A href="https://exceptionless.com/docs/project-settings/#client-configuration" target="_blank">configuration value</A> will be sent to
41+
the Exceptionless clients in real time.</AlertDialog.Description
42+
>
43+
</AlertDialog.Header>
44+
45+
<P class="pb-4">
46+
<Form.Field {form} name="key">
47+
<Form.Control>
48+
{#snippet children({ props })}
49+
<Form.Label>Key</Form.Label>
50+
<Input
51+
{...props}
52+
bind:value={$formData.key}
53+
type="text"
54+
placeholder="Please enter a valid key"
55+
required
56+
autocomplete="off"
57+
oninput={() => ($formData.key = $formData.key.trim())}
58+
/>
59+
{/snippet}
60+
</Form.Control>
61+
<Form.Description />
62+
<Form.FieldErrors />
63+
</Form.Field>
64+
<Form.Field {form} name="value">
65+
<Form.Control>
66+
{#snippet children({ props })}
67+
<Form.Label>Value</Form.Label>
68+
<Input
69+
{...props}
70+
bind:value={$formData.value}
71+
type="text"
72+
placeholder="Please enter a valid value"
73+
required
74+
autocomplete="off"
75+
oninput={() => ($formData.value = $formData.value.trim())}
76+
/>
77+
{/snippet}
78+
</Form.Control>
79+
<Form.Description />
80+
<Form.FieldErrors />
81+
</Form.Field>
82+
</P>
83+
84+
<AlertDialog.Footer>
85+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
86+
<AlertDialog.Action>Add Configuration Setting</AlertDialog.Action>
87+
</AlertDialog.Footer>
88+
</form>
89+
</AlertDialog.Content>
90+
</AlertDialog.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
import * as AlertDialog from '$comp/ui/alert-dialog';
3+
import { buttonVariants } from '$comp/ui/button';
4+
5+
interface Props {
6+
name: string;
7+
open: boolean;
8+
remove: () => Promise<void>;
9+
}
10+
11+
let { name, open = $bindable(false), remove }: Props = $props();
12+
13+
async function onSubmit() {
14+
await remove();
15+
open = false;
16+
}
17+
</script>
18+
19+
<AlertDialog.Root bind:open>
20+
<AlertDialog.Content>
21+
<AlertDialog.Header>
22+
<AlertDialog.Title>Delete Configuration Setting</AlertDialog.Title>
23+
<AlertDialog.Description>
24+
Are you sure you want to delete "{name}"?
25+
</AlertDialog.Description>
26+
</AlertDialog.Header>
27+
<AlertDialog.Footer>
28+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
29+
<AlertDialog.Action class={buttonVariants({ variant: 'destructive' })} onclick={onSubmit}>Delete Configuration Setting</AlertDialog.Action>
30+
</AlertDialog.Footer>
31+
</AlertDialog.Content>
32+
</AlertDialog.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<script lang="ts">
2+
import { A, P } from '$comp/typography';
3+
import * as AlertDialog from '$comp/ui/alert-dialog';
4+
import * as Form from '$comp/ui/form';
5+
import { Input } from '$comp/ui/input';
6+
import { defaults, superForm } from 'sveltekit-superforms';
7+
import { classvalidatorClient } from 'sveltekit-superforms/adapters';
8+
9+
import { ClientConfigurationSetting } from '../../models';
10+
11+
interface Props {
12+
open: boolean;
13+
save: (value: string) => Promise<void>;
14+
setting: ClientConfigurationSetting; // Change type from string to ClientConfigurationSetting
15+
}
16+
let { open = $bindable(), save, setting }: Props = $props();
17+
18+
const defaultValue = new ClientConfigurationSetting();
19+
defaultValue.key = setting.key;
20+
defaultValue.value = setting.value;
21+
22+
const form = superForm(defaults(defaultValue, classvalidatorClient(ClientConfigurationSetting)), {
23+
dataType: 'json',
24+
async onUpdate({ form }) {
25+
if (!form.valid) {
26+
return;
27+
}
28+
29+
await save(form.data.value.trim());
30+
open = false;
31+
},
32+
SPA: true,
33+
validators: classvalidatorClient(ClientConfigurationSetting)
34+
});
35+
36+
const { enhance, form: formData } = form;
37+
</script>
38+
39+
<AlertDialog.Root bind:open>
40+
<AlertDialog.Content class="sm:max-w-[425px]">
41+
<form method="POST" use:enhance>
42+
<AlertDialog.Header>
43+
<AlertDialog.Title>Update {setting.key} Configuration Value</AlertDialog.Title>
44+
<AlertDialog.Description
45+
>The <A href="https://exceptionless.com/docs/project-settings/#client-configuration" target="_blank">configuration value</A> will be sent to
46+
the Exceptionless clients in real time.</AlertDialog.Description
47+
>
48+
</AlertDialog.Header>
49+
50+
<P class="pb-4">
51+
<Form.Field {form} name="value">
52+
<Form.Control>
53+
{#snippet children({ props })}
54+
<Form.Label>Value</Form.Label>
55+
<Input
56+
{...props}
57+
bind:value={$formData.value}
58+
type="text"
59+
placeholder="Please enter a valid value"
60+
required
61+
autocomplete="off"
62+
oninput={() => ($formData.value = $formData.value.trim())}
63+
/>
64+
{/snippet}
65+
</Form.Control>
66+
<Form.Description />
67+
<Form.FieldErrors />
68+
</Form.Field>
69+
</P>
70+
71+
<AlertDialog.Footer>
72+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
73+
<AlertDialog.Action>Save Configuration Value</AlertDialog.Action>
74+
</AlertDialog.Footer>
75+
</form>
76+
</AlertDialog.Content>
77+
</AlertDialog.Root>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { ClientConfigurationSetting } from '$features/projects/models';
2+
3+
import ProjectConfigActionsCell from '$features/projects/components/table/project-config-actions-cell.svelte';
4+
import {
5+
type ColumnDef,
6+
getCoreRowModel,
7+
getPaginationRowModel,
8+
renderComponent,
9+
type TableOptions,
10+
type Updater,
11+
type VisibilityState
12+
} from '@tanstack/svelte-table';
13+
import { PersistedState } from 'runed';
14+
15+
export type ProjectClientConfigurationSettingsParameters = {
16+
limit: number;
17+
projectId: string;
18+
};
19+
20+
export function getColumns<TClientConfigurationSetting extends ClientConfigurationSetting>(
21+
params: ProjectClientConfigurationSettingsParameters
22+
): ColumnDef<TClientConfigurationSetting>[] {
23+
const columns: ColumnDef<TClientConfigurationSetting>[] = [
24+
{
25+
accessorKey: 'key',
26+
cell: (info) => info.getValue(),
27+
enableHiding: false,
28+
header: 'Key',
29+
meta: {
30+
class: 'w-[200px]'
31+
}
32+
},
33+
{
34+
accessorKey: 'value',
35+
cell: (info) => info.getValue(),
36+
enableHiding: false,
37+
header: 'Value',
38+
meta: {
39+
class: 'w-[200px]'
40+
}
41+
},
42+
{
43+
cell: (info) => renderComponent(ProjectConfigActionsCell, { projectId: params.projectId, setting: info.row.original }),
44+
enableHiding: false,
45+
enableSorting: false,
46+
header: 'Actions',
47+
id: 'actions',
48+
meta: {
49+
class: 'w-16'
50+
}
51+
}
52+
];
53+
54+
return columns;
55+
}
56+
57+
export function getTableContext<TClientConfigurationSetting extends ClientConfigurationSetting>(
58+
params: ProjectClientConfigurationSettingsParameters,
59+
configureOptions: (options: TableOptions<TClientConfigurationSetting>) => TableOptions<TClientConfigurationSetting> = (options) => options
60+
) {
61+
let _columns = $state(getColumns<TClientConfigurationSetting>(params));
62+
let _data = $state([] as TClientConfigurationSetting[]);
63+
64+
const [columnVisibility, setColumnVisibility] = createPersistedTableState('project-config-column-visibility', <VisibilityState>{});
65+
const options = configureOptions({
66+
get columns() {
67+
return _columns;
68+
},
69+
set columns(value) {
70+
_columns = value;
71+
},
72+
get data() {
73+
return _data;
74+
},
75+
set data(value) {
76+
_data = value;
77+
},
78+
enableMultiRowSelection: true,
79+
enableRowSelection: true,
80+
enableSortingRemoval: false,
81+
getCoreRowModel: getCoreRowModel(),
82+
getPaginationRowModel: getPaginationRowModel(),
83+
getRowId: (originalRow) => originalRow.key,
84+
onColumnVisibilityChange: setColumnVisibility,
85+
state: {
86+
get columnVisibility() {
87+
return columnVisibility();
88+
}
89+
}
90+
});
91+
92+
return {
93+
get data() {
94+
return _data;
95+
},
96+
set data(value) {
97+
_data = value;
98+
},
99+
options
100+
};
101+
}
102+
103+
function createPersistedTableState<T>(key: string, initialValue: T): [() => T, (updater: Updater<T>) => void] {
104+
const persistedValue = new PersistedState<T>(key, initialValue);
105+
106+
return [
107+
() => persistedValue.current,
108+
(updater: Updater<T>) => {
109+
if (updater instanceof Function) {
110+
persistedValue.current = updater(persistedValue.current);
111+
} else {
112+
persistedValue.current = updater;
113+
}
114+
}
115+
];
116+
}

src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/table/project-actions-cell.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,6 @@
6666
</DropdownMenu.Content>
6767
</DropdownMenu.Root>
6868

69-
<RemoveProjectDialog bind:open={showRemoveProjectDialog} name={project.name} {remove} />
69+
{#if showRemoveProjectDialog}
70+
<RemoveProjectDialog bind:open={showRemoveProjectDialog} name={project.name} {remove} />
71+
{/if}

0 commit comments

Comments
 (0)