Skip to content

Commit 0971585

Browse files
committed
next: added project settings page
1 parent c61a15a commit 0971585

File tree

1 file changed

+234
-0
lines changed
  • src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/settings

1 file changed

+234
-0
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<script lang="ts">
2+
import { page } from '$app/state';
3+
import { A, H3, H4, Large, Muted, P } from '$comp/typography';
4+
import { Input } from '$comp/ui/input';
5+
import { Label } from '$comp/ui/label';
6+
import { Separator } from '$comp/ui/separator';
7+
import { Switch } from '$comp/ui/switch';
8+
import { deleteProjectConfig, getProjectConfig, getProjectQuery, postProjectConfig, updateProject } from '$features/projects/api.svelte';
9+
import ProjectLogLevel from '$features/projects/components/project-log-level.svelte';
10+
import { UpdateProject } from '$features/projects/models';
11+
import { toast } from 'svelte-sonner';
12+
import { debounce } from 'throttle-debounce';
13+
14+
let toastId = $state<number | string>();
15+
const projectId = page.params.projectId || '';
16+
const projectResponse = getProjectQuery({
17+
route: {
18+
get id() {
19+
return projectId;
20+
}
21+
}
22+
});
23+
24+
const update = updateProject({
25+
route: {
26+
get id() {
27+
return projectId;
28+
}
29+
}
30+
});
31+
32+
const projectConfigResponse = getProjectConfig({
33+
route: {
34+
get id() {
35+
return projectId;
36+
}
37+
}
38+
});
39+
40+
const updateProjectConfig = postProjectConfig({
41+
route: {
42+
get id() {
43+
return projectId;
44+
}
45+
}
46+
});
47+
48+
const removeProjectConfig = deleteProjectConfig({
49+
route: {
50+
get id() {
51+
return projectId;
52+
}
53+
}
54+
});
55+
56+
let dataExclusions = $state('');
57+
let excludePrivateInformation = $state(false);
58+
let userNamespaces = $state('');
59+
let commonMethods = $state('');
60+
let userAgents = $state('');
61+
let deleteBotDataEnabled = $state(false);
62+
63+
const dataExclusionsIsDirty = $derived(dataExclusions !== projectConfigResponse.data?.settings['@@DataExclusions']);
64+
const excludePrivateInformationIsDirty = $derived(excludePrivateInformation !== (projectConfigResponse.data?.settings['@@IncludePrivateInformation'] === 'false'));
65+
const userNamespacesIsDirty = $derived(userNamespaces !== projectConfigResponse.data?.settings.UserNamespaces);
66+
const commonMethodsIsDirty = $derived(commonMethods !== projectConfigResponse.data?.settings.CommonMethods);
67+
const userAgentsIsDirty = $derived(userAgents !== projectConfigResponse.data?.settings['@@UserAgentBotPatterns'] as string);
68+
const deleteBotDataEnabledIsDirty = $derived(deleteBotDataEnabled !== projectResponse.data?.delete_bot_data_enabled);
69+
70+
async function updateOrRemoveProjectConfig(key: string, value: null | string, displayName: string) {
71+
toast.dismiss(toastId);
72+
73+
try {
74+
if (value) {
75+
await updateProjectConfig.mutateAsync({ key, value });
76+
} else {
77+
await removeProjectConfig.mutateAsync({ key });
78+
}
79+
80+
toastId = toast.success(`Successfully updated ${displayName} setting.`);
81+
} catch {
82+
toastId = toast.error(`Error updating ${displayName}'s setting. Please try again.`);
83+
}
84+
}
85+
86+
async function saveDataExclusion() {
87+
if (!dataExclusionsIsDirty) {
88+
return;
89+
}
90+
91+
await updateOrRemoveProjectConfig('@@DataExclusions', dataExclusions, 'Data Exclusions');
92+
}
93+
94+
async function saveIncludePrivateInformation() {
95+
if (!excludePrivateInformationIsDirty) {
96+
return;
97+
}
98+
99+
await updateOrRemoveProjectConfig('@@IncludePrivateInformation', excludePrivateInformation ? 'false' : null, 'Exclude Private Information');
100+
}
101+
102+
async function saveUserNamespaces() {
103+
if (!userNamespacesIsDirty) {
104+
return;
105+
}
106+
107+
await updateOrRemoveProjectConfig('UserNamespaces', userNamespaces, 'User Namespaces');
108+
}
109+
110+
async function saveCommonMethods() {
111+
if (!commonMethodsIsDirty) {
112+
return;
113+
}
114+
115+
await updateOrRemoveProjectConfig('CommonMethods', commonMethods, 'Common Methods');
116+
}
117+
118+
async function saveUserAgents() {
119+
if (!userAgentsIsDirty) {
120+
return;
121+
}
122+
123+
await updateOrRemoveProjectConfig('@@UserAgentBotPatterns', userAgents, 'User Agents');
124+
}
125+
126+
async function saveDeleteBotDataEnabled() {
127+
if (!deleteBotDataEnabledIsDirty) {
128+
return;
129+
}
130+
131+
toast.dismiss(toastId);
132+
133+
try {
134+
const data = new UpdateProject();
135+
data.delete_bot_data_enabled = deleteBotDataEnabled;
136+
await update.mutateAsync(data);
137+
138+
toastId = toast.success(`Successfully updated Delete Bot Data Enabled setting.`);
139+
} catch {
140+
toast.dismiss(toastId);
141+
toastId = toast.error(`Error updating Delete Bot Data Enabled setting. Please try again.`);
142+
}
143+
}
144+
145+
const debouncedSaveDataExclusion = debounce(500, saveDataExclusion);
146+
const debouncedSaveIncludePrivateInformation = debounce(500, saveIncludePrivateInformation);
147+
const debouncedSaveUserNamespaces = debounce(500, saveUserNamespaces);
148+
const debouncedSaveCommonMethods = debounce(500, saveCommonMethods);
149+
const debouncedSaveUserAgents = debounce(500, saveUserAgents);
150+
const debouncedSaveDeleteBotDataEnabled = debounce(500, saveDeleteBotDataEnabled);
151+
152+
$effect(() => {
153+
if (projectConfigResponse.dataUpdatedAt) {
154+
dataExclusions = projectConfigResponse.data?.settings['@@DataExclusions'] ?? '';
155+
excludePrivateInformation = projectConfigResponse.data?.settings['@@IncludePrivateInformation'] === 'false';
156+
userNamespaces = projectConfigResponse.data?.settings.UserNamespaces ?? '';
157+
commonMethods = projectConfigResponse.data?.settings.CommonMethods ?? '';
158+
userAgents = projectConfigResponse.data?.settings['@@UserAgentBotPatterns'] ?? '';
159+
}
160+
161+
if (projectResponse.dataUpdatedAt) {
162+
deleteBotDataEnabled = projectResponse.data?.delete_bot_data_enabled ?? false;
163+
}
164+
});
165+
</script>
166+
167+
<div class="space-y-6">
168+
<div>
169+
<H3>Settings</H3>
170+
<Muted>Create and manage API keys for authenticating your applications with Exceptionless.</Muted>
171+
</div>
172+
<Separator />
173+
174+
<section class="space-y-2">
175+
<H4>Default Log Level</H4>
176+
<P
177+
>The default log level controls the minimum log level that should be accepted for log events. Log levels can also be overridden at the log stack
178+
level.</P
179+
>
180+
181+
<ProjectLogLevel source="*" {projectId} />
182+
</section>
183+
184+
<section class="space-y-2">
185+
<H4>Data Exclusions</H4>
186+
<P
187+
>A comma delimited list of field names that should be removed from any error report data (e.g., extended data properties, form fields, cookies and
188+
query parameters). You can also specify a <A href="https://exceptionless.com/docs/security/" target="_blank">field name with wildcards (*)</A> to specify
189+
starts with, ends with, or contains just to be extra safe.</P
190+
>
191+
<Input type="text" placeholder="Example: *Password*, CreditCard*, SSN" bind:value={dataExclusions} onchange={debouncedSaveDataExclusion} />
192+
193+
<div class="flex items-center space-x-2 pt-1">
194+
<Switch id="exclude-private-info" bind:checked={excludePrivateInformation} onCheckedChange={debouncedSaveIncludePrivateInformation} />
195+
<Label for="exclude-private-info">
196+
Automatically remove user identifiable information from events (e.g., Machine Name, User Information, IP Addresses and more...).
197+
</Label>
198+
</div>
199+
</section>
200+
201+
<section class="space-y-2">
202+
<H4>Error Stacking</H4>
203+
<div class="space-y-4">
204+
<div class="space-y-2">
205+
<Large>User Namespaces</Large>
206+
<P
207+
>A comma delimited list of the namespace names that your applications code belongs to. If this value is set, only methods inside of these
208+
namespaces will be considered as stacking targets.</P
209+
>
210+
<Input type="text" placeholder="Example: Contoso" bind:value={userNamespaces} onchange={debouncedSaveUserNamespaces} />
211+
</div>
212+
213+
<div class="space-y-2">
214+
<Large>Common Methods</Large>
215+
<P
216+
>A comma delimited list of common method names that should not be used as stacking targets. This is useful when your code contains shared
217+
utility methods that throw a lot of errors.</P
218+
>
219+
<Input type="text" placeholder="Example: Assert, Writeline" bind:value={commonMethods} onchange={debouncedSaveCommonMethods} />
220+
</div>
221+
</div>
222+
</section>
223+
224+
<section class="space-y-2">
225+
<H4>Spam Detection</H4>
226+
<P>A comma delimited list of user agents that should be ignored.</P>
227+
<Input type="text" placeholder="Example: SpamBot" bind:value={userAgents} onchange={debouncedSaveUserAgents} />
228+
229+
<div class="flex items-center space-x-2 pt-1">
230+
<Switch id="delete-bot-data" bind:checked={deleteBotDataEnabled} onCheckedChange={debouncedSaveDeleteBotDataEnabled} />
231+
<Label for="delete-bot-data">Reduce noise by automatically hiding high volumes of events coming from a single client IP address.</Label>
232+
</div>
233+
</section>
234+
</div>

0 commit comments

Comments
 (0)