@svadmin/ui 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AboutSettings.svelte +21 -96
- package/dist/components/ApiSettings.svelte +54 -234
- package/dist/components/AppearanceSettings.svelte +36 -165
- package/dist/components/AutoForm.svelte +21 -3
- package/dist/components/DetailDrawer.svelte +74 -0
- package/dist/components/DetailDrawer.svelte.d.ts +16 -0
- package/dist/components/FieldRenderer.svelte +70 -2
- package/dist/components/FieldRenderer.svelte.d.ts +2 -0
- package/dist/components/IntegrationsSettings.svelte +22 -61
- package/dist/components/NotificationsSettings.svelte +22 -103
- package/dist/components/PermissionMatrix.svelte +17 -8
- package/dist/components/RowActions.svelte +154 -0
- package/dist/components/RowActions.svelte.d.ts +26 -0
- package/dist/components/SecuritySettings.svelte +18 -58
- package/dist/components/SettingsPage.svelte +12 -2
- package/dist/components/account/CompanyProfilePage.svelte +10 -4
- package/dist/components/account/SettingsEnterprisePage.svelte +1 -1
- package/dist/components/account/UserProfilePage.svelte +5 -4
- package/dist/components/content/ApiKeyList.svelte +30 -5
- package/dist/components/content/ApiKeyList.svelte.d.ts +3 -0
- package/dist/components/content/WorkspaceLayout.svelte +7 -4
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +4 -1
|
@@ -1,108 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { captureAdminContext, getColorTheme, getResolvedTheme } from '@svadmin/core';
|
|
2
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import { captureAdminContext } from '@svadmin/core';
|
|
4
|
-
import { getResolvedTheme, getColorTheme } from '@svadmin/core';
|
|
5
|
-
import * as Card from './ui/card/index.js';
|
|
6
4
|
import { Badge } from './ui/badge/index.js';
|
|
7
|
-
import
|
|
5
|
+
import DescriptionList from './content/DescriptionList.svelte';
|
|
6
|
+
import SettingsGroup from './content/SettingsGroup.svelte';
|
|
8
7
|
|
|
9
8
|
const i18n = useTranslation();
|
|
10
9
|
const adminContext = captureAdminContext();
|
|
11
|
-
|
|
12
10
|
const resources = $derived(adminContext.resources);
|
|
13
11
|
const providerNames = $derived(adminContext.getDataProviderNames());
|
|
14
|
-
|
|
15
|
-
const version = '__SVADMIN_VERSION__'; // replaced at build time or shown as-is
|
|
12
|
+
const version = '__SVADMIN_VERSION__';
|
|
16
13
|
</script>
|
|
17
14
|
|
|
18
15
|
<div class="space-y-6">
|
|
19
|
-
<div>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
</Card.CardContent>
|
|
39
|
-
</Card.Card>
|
|
40
|
-
|
|
41
|
-
<!-- Registered Resources -->
|
|
42
|
-
<Card.Card>
|
|
43
|
-
<Card.CardHeader class="pb-3">
|
|
44
|
-
<Card.CardTitle class="text-base flex items-center gap-2">
|
|
45
|
-
<Layers class="h-4 w-4 text-muted-foreground" />
|
|
46
|
-
{i18n.t('settings.registeredResources')}
|
|
47
|
-
</Card.CardTitle>
|
|
48
|
-
</Card.CardHeader>
|
|
49
|
-
<Card.CardContent>
|
|
50
|
-
<div class="flex flex-wrap gap-2">
|
|
51
|
-
{#each resources as resource, _i (_i)}
|
|
52
|
-
<Badge variant="outline" class="font-mono text-xs">{resource.name}</Badge>
|
|
53
|
-
{/each}
|
|
54
|
-
{#if resources.length === 0}
|
|
55
|
-
<p class="text-sm text-muted-foreground">—</p>
|
|
56
|
-
{/if}
|
|
57
|
-
</div>
|
|
58
|
-
</Card.CardContent>
|
|
59
|
-
</Card.Card>
|
|
60
|
-
|
|
61
|
-
<!-- Data Providers -->
|
|
62
|
-
<Card.Card>
|
|
63
|
-
<Card.CardHeader class="pb-3">
|
|
64
|
-
<Card.CardTitle class="text-base flex items-center gap-2">
|
|
65
|
-
<Database class="h-4 w-4 text-muted-foreground" />
|
|
66
|
-
{i18n.t('settings.dataProvider')}
|
|
67
|
-
</Card.CardTitle>
|
|
68
|
-
</Card.CardHeader>
|
|
69
|
-
<Card.CardContent>
|
|
70
|
-
<div class="flex flex-wrap gap-2">
|
|
71
|
-
{#each providerNames as name, _i (_i)}
|
|
72
|
-
<Badge variant="outline" class="font-mono text-xs">{name}</Badge>
|
|
73
|
-
{/each}
|
|
74
|
-
</div>
|
|
75
|
-
</Card.CardContent>
|
|
76
|
-
</Card.Card>
|
|
77
|
-
|
|
78
|
-
<!-- Current Environment -->
|
|
79
|
-
<Card.Card>
|
|
80
|
-
<Card.CardHeader class="pb-3">
|
|
81
|
-
<Card.CardTitle class="text-base flex items-center gap-2">
|
|
82
|
-
<Globe class="h-4 w-4 text-muted-foreground" />
|
|
83
|
-
{i18n.t('settings.currentEnvironment')}
|
|
84
|
-
</Card.CardTitle>
|
|
85
|
-
</Card.CardHeader>
|
|
86
|
-
<Card.CardContent>
|
|
87
|
-
<div class="grid gap-3 text-sm">
|
|
88
|
-
<div class="flex items-center justify-between">
|
|
89
|
-
<span class="text-muted-foreground">{i18n.t('settings.language')}</span>
|
|
90
|
-
<span class="font-mono text-xs">{i18n.locale}</span>
|
|
91
|
-
</div>
|
|
92
|
-
<div class="h-px bg-border"></div>
|
|
93
|
-
<div class="flex items-center justify-between">
|
|
94
|
-
<span class="text-muted-foreground">{i18n.t('settings.themeMode')}</span>
|
|
95
|
-
<span class="font-mono text-xs">{getResolvedTheme()}</span>
|
|
96
|
-
</div>
|
|
97
|
-
<div class="h-px bg-border"></div>
|
|
98
|
-
<div class="flex items-center justify-between">
|
|
99
|
-
<span class="text-muted-foreground flex items-center gap-1.5">
|
|
100
|
-
<Palette class="h-3.5 w-3.5" />
|
|
101
|
-
{i18n.t('settings.colorAccent')}
|
|
102
|
-
</span>
|
|
103
|
-
<span class="font-mono text-xs">{getColorTheme()}</span>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</Card.CardContent>
|
|
107
|
-
</Card.Card>
|
|
16
|
+
<div><h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.about')}</h2></div>
|
|
17
|
+
<SettingsGroup title="svadmin" description={i18n.t('settings.currentEnvironment')} bodyClass="space-y-6">
|
|
18
|
+
<DescriptionList columns={2} items={[
|
|
19
|
+
{ label: i18n.t('settings.version'), value: version },
|
|
20
|
+
{ label: i18n.t('settings.language'), value: i18n.locale },
|
|
21
|
+
{ label: i18n.t('settings.themeMode'), value: getResolvedTheme() },
|
|
22
|
+
{ label: i18n.t('settings.colorAccent'), value: getColorTheme() },
|
|
23
|
+
]} />
|
|
24
|
+
<div class="border-t border-border pt-5">
|
|
25
|
+
<h3 class="text-sm font-medium text-foreground">{i18n.t('settings.registeredResources')}</h3>
|
|
26
|
+
<div class="mt-3 flex flex-wrap gap-2">{#each resources as resource (resource.name)}<Badge variant="outline" class="font-mono text-xs">{resource.name}</Badge>{:else}<span class="text-sm text-muted-foreground">—</span>{/each}</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="border-t border-border pt-5">
|
|
29
|
+
<h3 class="text-sm font-medium text-foreground">{i18n.t('settings.dataProvider')}</h3>
|
|
30
|
+
<div class="mt-3 flex flex-wrap gap-2">{#each providerNames as name (name)}<Badge variant="outline" class="font-mono text-xs">{name}</Badge>{:else}<span class="text-sm text-muted-foreground">—</span>{/each}</div>
|
|
31
|
+
</div>
|
|
32
|
+
</SettingsGroup>
|
|
108
33
|
</div>
|
|
@@ -1,45 +1,29 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
|
|
4
|
-
import { AlertCircle, Calendar, Check, Copy, Key, Plus, ShieldCheck, Trash2, Webhook } from '@lucide/svelte';
|
|
5
|
-
import * as Card from './ui/card/index.js';
|
|
6
|
-
import { Badge } from './ui/badge/index.js';
|
|
3
|
+
import { Check, Copy, Plus, ShieldCheck, Trash2 } from '@lucide/svelte';
|
|
7
4
|
import { Button } from './ui/button/index.js';
|
|
8
5
|
import { Checkbox } from './ui/checkbox/index.js';
|
|
9
6
|
import { Input } from './ui/input/index.js';
|
|
10
7
|
import { Label } from './ui/label/index.js';
|
|
8
|
+
import ApiKeyList from './content/ApiKeyList.svelte';
|
|
9
|
+
import ContentPageShell from './content/ContentPageShell.svelte';
|
|
10
|
+
import DataState from './content/DataState.svelte';
|
|
11
|
+
import SettingsGroup from './content/SettingsGroup.svelte';
|
|
12
|
+
import WorkspaceLayout from './content/WorkspaceLayout.svelte';
|
|
11
13
|
|
|
12
14
|
const i18n = useTranslation();
|
|
13
|
-
|
|
14
|
-
interface
|
|
15
|
-
id: string;
|
|
16
|
-
name: string;
|
|
17
|
-
maskedToken: string;
|
|
18
|
-
created: string;
|
|
19
|
-
permissions: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
15
|
+
interface ApiKey { id: string; name: string; maskedToken: string; created: string; permissions: string[]; }
|
|
16
|
+
interface WebhookEntry { id: string; name: string; url: string; eventType: string; }
|
|
22
17
|
let keys = $state<ApiKey[]>([
|
|
23
18
|
{ id: 'prod-sync', name: 'Production Sync Service', maskedToken: 'sv_demo_prod_••••_4d3e', created: '2026-03-10', permissions: ['Read', 'Write'] },
|
|
24
19
|
{ id: 'reporting', name: 'Internal Reporting Tool', maskedToken: 'sv_demo_report_••••_9e8f', created: '2026-05-18', permissions: ['Read'] },
|
|
25
20
|
]);
|
|
26
|
-
|
|
27
21
|
let newKeyName = $state('');
|
|
28
22
|
let newKeyPermissions = $state({ Read: true, Write: false, Delete: false, Admin: false });
|
|
29
23
|
let generatedToken = $state('');
|
|
30
24
|
let showCreatedToken = $state(false);
|
|
31
25
|
let copiedKeyId = $state<string | null>(null);
|
|
32
|
-
|
|
33
|
-
interface WebhookEntry {
|
|
34
|
-
id: string;
|
|
35
|
-
name: string;
|
|
36
|
-
url: string;
|
|
37
|
-
eventType: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let webhooks = $state<WebhookEntry[]>([
|
|
41
|
-
{ id: 'wh-1', name: 'Order Events', url: 'https://api.acme.com/hooks/orders', eventType: 'resource.updated' },
|
|
42
|
-
]);
|
|
26
|
+
let webhooks = $state<WebhookEntry[]>([{ id: 'wh-1', name: 'Order Events', url: 'https://api.acme.com/hooks/orders', eventType: 'resource.updated' }]);
|
|
43
27
|
let newWebhookName = $state('');
|
|
44
28
|
let newWebhookUrl = $state('');
|
|
45
29
|
let newWebhookEvent = $state('resource.created');
|
|
@@ -47,233 +31,69 @@
|
|
|
47
31
|
function handleAddWebhook(event: SubmitEvent) {
|
|
48
32
|
event.preventDefault();
|
|
49
33
|
if (!newWebhookName.trim() || !newWebhookUrl.trim()) return;
|
|
50
|
-
webhooks = [
|
|
51
|
-
...webhooks,
|
|
52
|
-
{ id: Date.now().toString(), name: newWebhookName.trim(), url: newWebhookUrl.trim(), eventType: newWebhookEvent },
|
|
53
|
-
];
|
|
34
|
+
webhooks = [...webhooks, { id: Date.now().toString(), name: newWebhookName.trim(), url: newWebhookUrl.trim(), eventType: newWebhookEvent }];
|
|
54
35
|
newWebhookName = '';
|
|
55
36
|
newWebhookUrl = '';
|
|
56
37
|
}
|
|
57
|
-
|
|
58
|
-
function deleteWebhook(id: string) {
|
|
59
|
-
webhooks = webhooks.filter(w => w.id !== id);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function generateDemoToken() {
|
|
63
|
-
const randomHex = Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
64
|
-
return `sv_demo_${randomHex}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
38
|
+
function generateDemoToken() { return `sv_demo_${Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join('')}`; }
|
|
67
39
|
function handleGenerateKey(event: SubmitEvent) {
|
|
68
40
|
event.preventDefault();
|
|
69
41
|
if (!newKeyName.trim()) return;
|
|
70
|
-
|
|
71
42
|
const token = generateDemoToken();
|
|
72
|
-
const permissions = Object.entries(newKeyPermissions)
|
|
73
|
-
|
|
74
|
-
.map(([name]) => name);
|
|
75
|
-
|
|
76
|
-
keys = [
|
|
77
|
-
...keys,
|
|
78
|
-
{
|
|
79
|
-
id: Date.now().toString(),
|
|
80
|
-
name: newKeyName.trim(),
|
|
81
|
-
maskedToken: `${token.slice(0, 14)}••••${token.slice(-4)}`,
|
|
82
|
-
created: new Date().toISOString().split('T')[0],
|
|
83
|
-
permissions,
|
|
84
|
-
},
|
|
85
|
-
];
|
|
43
|
+
const permissions = Object.entries(newKeyPermissions).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
44
|
+
keys = [...keys, { id: Date.now().toString(), name: newKeyName.trim(), maskedToken: `${token.slice(0, 14)}••••${token.slice(-4)}`, created: new Date().toISOString().split('T')[0], permissions }];
|
|
86
45
|
generatedToken = token;
|
|
87
46
|
showCreatedToken = true;
|
|
88
47
|
newKeyName = '';
|
|
89
48
|
newKeyPermissions = { Read: true, Write: false, Delete: false, Admin: false };
|
|
90
49
|
}
|
|
91
|
-
|
|
92
|
-
function deleteKey(id: string) {
|
|
93
|
-
keys = keys.filter((key) => key.id !== id);
|
|
94
|
-
}
|
|
95
|
-
|
|
50
|
+
function deleteKey(id: string) { keys = keys.filter((key) => key.id !== id); }
|
|
96
51
|
async function copyToken(token: string, id: string) {
|
|
97
52
|
try {
|
|
98
53
|
await navigator.clipboard.writeText(token);
|
|
99
54
|
copiedKeyId = id;
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
|
|
102
|
-
}, 2000);
|
|
103
|
-
} catch {
|
|
104
|
-
copiedKeyId = null;
|
|
105
|
-
}
|
|
55
|
+
setTimeout(() => { if (copiedKeyId === id) copiedKeyId = null; }, 2000);
|
|
56
|
+
} catch { copiedKeyId = null; }
|
|
106
57
|
}
|
|
107
58
|
</script>
|
|
108
59
|
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<Card.CardHeader>
|
|
118
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
119
|
-
<Plus class="h-4 w-4" />
|
|
120
|
-
{i18n.t('api.generateTitle')}
|
|
121
|
-
</Card.CardTitle>
|
|
122
|
-
<Card.CardDescription>{i18n.t('api.generateDesc')}</Card.CardDescription>
|
|
123
|
-
</Card.CardHeader>
|
|
124
|
-
<Card.CardContent>
|
|
125
|
-
<form onsubmit={handleGenerateKey} class="space-y-4">
|
|
126
|
-
<div class="space-y-2">
|
|
127
|
-
<Label for="api-key-name">{i18n.t('api.keyName')}</Label>
|
|
128
|
-
<Input id="api-key-name" placeholder="CI deployment token" bind:value={newKeyName} required />
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<div class="space-y-3">
|
|
132
|
-
<Label>{i18n.t('api.permissions')}</Label>
|
|
133
|
-
{#each Object.keys(newKeyPermissions) as permission (permission)}
|
|
134
|
-
<div class="flex items-center gap-2">
|
|
135
|
-
<Checkbox id={`perm-${permission}`} bind:checked={newKeyPermissions[permission as keyof typeof newKeyPermissions]} />
|
|
136
|
-
<Label for={`perm-${permission}`} class="cursor-pointer text-sm font-normal">{permission}</Label>
|
|
137
|
-
</div>
|
|
138
|
-
{/each}
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<Button type="submit" class="w-full">{i18n.t('api.generateButton')}</Button>
|
|
142
|
-
</form>
|
|
143
|
-
</Card.CardContent>
|
|
144
|
-
</Card.Card>
|
|
145
|
-
|
|
146
|
-
<Card.Card class="lg:col-span-2">
|
|
147
|
-
<Card.CardHeader>
|
|
148
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
149
|
-
<Key class="h-4 w-4 text-muted-foreground" />
|
|
150
|
-
{i18n.t('api.activeKeysTitle')}
|
|
151
|
-
</Card.CardTitle>
|
|
152
|
-
<Card.CardDescription>{i18n.t('api.activeKeysDesc')}</Card.CardDescription>
|
|
153
|
-
</Card.CardHeader>
|
|
154
|
-
<Card.CardContent>
|
|
155
|
-
{#if showCreatedToken}
|
|
156
|
-
<div class="mb-6 space-y-3 rounded-lg border border-green-500/20 bg-green-500/5 p-4">
|
|
157
|
-
<div class="flex items-start gap-2 text-sm text-green-700 dark:text-green-300">
|
|
158
|
-
<ShieldCheck class="mt-0.5 h-5 w-5 shrink-0" />
|
|
159
|
-
<div>
|
|
160
|
-
<h4 class="font-semibold">{i18n.t('api.successTitle')}</h4>
|
|
161
|
-
<p class="mt-0.5 text-xs">{i18n.t('api.successHint')}</p>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
<div class="flex items-center gap-2">
|
|
165
|
-
<Input readonly value={generatedToken} class="h-9 font-mono text-xs" />
|
|
166
|
-
<Button size="sm" onclick={() => copyToken(generatedToken, 'new-token')}>
|
|
167
|
-
{#if copiedKeyId === 'new-token'}
|
|
168
|
-
<Check class="h-4 w-4" />
|
|
169
|
-
{:else}
|
|
170
|
-
<Copy class="h-4 w-4" />
|
|
171
|
-
{/if}
|
|
172
|
-
</Button>
|
|
173
|
-
</div>
|
|
174
|
-
<div class="flex justify-end">
|
|
175
|
-
<Button variant="ghost" size="sm" onclick={() => { showCreatedToken = false; generatedToken = ''; }}>{i18n.t('common.close')}</Button>
|
|
176
|
-
</div>
|
|
60
|
+
<ContentPageShell pageId="api-settings" width="wide" title={i18n.t('settings.api')} description={i18n.t('settings.apiDescription')}>
|
|
61
|
+
<WorkspaceLayout secondaryWidth="21rem">
|
|
62
|
+
{#snippet summary()}
|
|
63
|
+
{#if showCreatedToken}
|
|
64
|
+
<section class="rounded-lg border border-primary/20 bg-primary/5 p-4" role="status" aria-live="polite">
|
|
65
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
66
|
+
<div class="flex min-w-0 items-start gap-3"><ShieldCheck class="mt-0.5 size-5 shrink-0 text-primary" /><div class="min-w-0"><h2 class="text-sm font-semibold text-foreground">{i18n.t('api.successTitle')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('api.successHint')}</p></div></div>
|
|
67
|
+
<Button variant="ghost" size="sm" onclick={() => { showCreatedToken = false; generatedToken = ''; }}>{i18n.t('common.close')}</Button>
|
|
177
68
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{#if copiedKeyId === key.id}
|
|
197
|
-
<Check class="h-4 w-4 text-green-500" />
|
|
198
|
-
{:else}
|
|
199
|
-
<Copy class="h-4 w-4" />
|
|
200
|
-
{/if}
|
|
201
|
-
</Button>
|
|
202
|
-
<Button variant="ghost" size="icon-sm" onclick={() => deleteKey(key.id)} title={i18n.t('common.delete')}>
|
|
203
|
-
<Trash2 class="h-4 w-4" />
|
|
204
|
-
</Button>
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
<div class="flex flex-wrap gap-1">
|
|
208
|
-
{#each key.permissions as permission (permission)}
|
|
209
|
-
<Badge variant="secondary" class="px-2 py-0 text-[10px]">{permission}</Badge>
|
|
210
|
-
{/each}
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
{:else}
|
|
214
|
-
<div class="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted-foreground">
|
|
215
|
-
<AlertCircle class="h-8 w-8 opacity-60" />
|
|
216
|
-
<span>{i18n.t('api.noKeys')}</span>
|
|
217
|
-
</div>
|
|
218
|
-
{/each}
|
|
219
|
-
</div>
|
|
220
|
-
</Card.CardContent>
|
|
221
|
-
</Card.Card>
|
|
222
|
-
|
|
223
|
-
<!-- Webhooks -->
|
|
224
|
-
<Card.Card>
|
|
225
|
-
<Card.CardHeader>
|
|
226
|
-
<Card.CardTitle class="flex items-center gap-2">
|
|
227
|
-
<Webhook class="h-4 w-4 text-muted-foreground" />
|
|
228
|
-
{i18n.t('api.webhooks')}
|
|
229
|
-
</Card.CardTitle>
|
|
230
|
-
<Card.CardDescription>{i18n.t('api.webhooksDesc')}</Card.CardDescription>
|
|
231
|
-
</Card.CardHeader>
|
|
232
|
-
<Card.CardContent class="space-y-4">
|
|
233
|
-
<form class="grid gap-3 sm:grid-cols-[1fr_1fr_180px_auto]" onsubmit={handleAddWebhook}>
|
|
234
|
-
<div class="space-y-1.5">
|
|
235
|
-
<Label for="webhook-name">{i18n.t('api.webhookName')}</Label>
|
|
236
|
-
<Input id="webhook-name" bind:value={newWebhookName} placeholder="Order Events" />
|
|
237
|
-
</div>
|
|
238
|
-
<div class="space-y-1.5">
|
|
239
|
-
<Label for="webhook-url">{i18n.t('api.webhookUrl')}</Label>
|
|
240
|
-
<Input id="webhook-url" type="url" bind:value={newWebhookUrl} placeholder="https://api.example.com/hooks" />
|
|
241
|
-
</div>
|
|
242
|
-
<div class="space-y-1.5">
|
|
243
|
-
<Label for="webhook-event">{i18n.t('api.eventType')}</Label>
|
|
244
|
-
<select id="webhook-event" bind:value={newWebhookEvent} class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring">
|
|
245
|
-
<option value="resource.created">resource.created</option>
|
|
246
|
-
<option value="resource.updated">resource.updated</option>
|
|
247
|
-
<option value="resource.deleted">resource.deleted</option>
|
|
248
|
-
<option value="auth.login">auth.login</option>
|
|
249
|
-
</select>
|
|
250
|
-
</div>
|
|
251
|
-
<div class="flex items-end">
|
|
252
|
-
<Button type="submit" size="sm" disabled={!newWebhookName.trim() || !newWebhookUrl.trim()}>
|
|
253
|
-
<Plus class="h-3.5 w-3.5 mr-1" />{i18n.t('api.addWebhook')}
|
|
254
|
-
</Button>
|
|
255
|
-
</div>
|
|
256
|
-
</form>
|
|
257
|
-
|
|
258
|
-
<div class="divide-y border-t">
|
|
259
|
-
{#each webhooks as hook (hook.id)}
|
|
260
|
-
<div class="flex items-center justify-between gap-4 py-3">
|
|
261
|
-
<div class="min-w-0">
|
|
262
|
-
<p class="truncate text-sm font-medium text-foreground">{hook.name}</p>
|
|
263
|
-
<p class="truncate font-mono text-xs text-muted-foreground">{hook.url}</p>
|
|
264
|
-
</div>
|
|
265
|
-
<div class="flex shrink-0 items-center gap-2">
|
|
266
|
-
<Badge variant="secondary" class="px-2 py-0 text-[10px]">{hook.eventType}</Badge>
|
|
267
|
-
<Button variant="ghost" size="icon-sm" onclick={() => deleteWebhook(hook.id)} title={i18n.t('common.delete')}>
|
|
268
|
-
<Trash2 class="h-4 w-4" />
|
|
269
|
-
</Button>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
69
|
+
<div class="mt-4 flex min-w-0 items-center gap-2"><Input readonly value={generatedToken} class="min-w-0 font-mono text-xs" aria-label={i18n.t('api.successTitle')} /><Button size="icon" onclick={() => copyToken(generatedToken, 'new-token')} aria-label={i18n.t('common.copy')}>{#if copiedKeyId === 'new-token'}<Check class="size-4" />{:else}<Copy class="size-4" />{/if}</Button></div>
|
|
70
|
+
</section>
|
|
71
|
+
{/if}
|
|
72
|
+
{/snippet}
|
|
73
|
+
{#snippet primary()}
|
|
74
|
+
<div class="space-y-6">
|
|
75
|
+
<SettingsGroup title={i18n.t('api.activeKeysTitle')} description={i18n.t('api.activeKeysDesc')} bodyClass="p-0">
|
|
76
|
+
<ApiKeyList class="rounded-none border-0" keys={keys.map((key) => ({ id: key.id, name: key.name, prefix: key.maskedToken, maskedToken: key.maskedToken, createdAt: key.created, permissions: key.permissions }))} stateTitle={i18n.t('api.noKeys')} stateDescription={i18n.t('api.generateDesc')} {copiedKeyId} oncopy={(key) => copyToken(key.maskedToken ?? key.prefix, key.id)} onrevoke={(key) => deleteKey(key.id)} />
|
|
77
|
+
</SettingsGroup>
|
|
78
|
+
<SettingsGroup title={i18n.t('api.webhooks')} description={i18n.t('api.webhooksDesc')} bodyClass="space-y-5">
|
|
79
|
+
<form class="grid min-w-0 gap-4 md:grid-cols-2" onsubmit={handleAddWebhook}>
|
|
80
|
+
<div class="space-y-2"><Label for="webhook-name">{i18n.t('api.webhookName')}</Label><Input id="webhook-name" bind:value={newWebhookName} placeholder="Order Events" /></div>
|
|
81
|
+
<div class="space-y-2"><Label for="webhook-url">{i18n.t('api.webhookUrl')}</Label><Input id="webhook-url" type="url" bind:value={newWebhookUrl} placeholder="https://api.example.com/hooks" /></div>
|
|
82
|
+
<div class="space-y-2"><Label for="webhook-event">{i18n.t('api.eventType')}</Label><select id="webhook-event" bind:value={newWebhookEvent} class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"><option value="resource.created">resource.created</option><option value="resource.updated">resource.updated</option><option value="resource.deleted">resource.deleted</option><option value="auth.login">auth.login</option></select></div>
|
|
83
|
+
<div class="flex items-end"><Button type="submit" class="w-full" disabled={!newWebhookName.trim() || !newWebhookUrl.trim()}><Plus class="size-4" />{i18n.t('api.addWebhook')}</Button></div>
|
|
84
|
+
</form>
|
|
85
|
+
{#if webhooks.length === 0}
|
|
86
|
+
<DataState state="empty" title={i18n.t('api.noWebhooks')} description={i18n.t('api.webhooksDesc')} />
|
|
272
87
|
{:else}
|
|
273
|
-
<
|
|
274
|
-
{/
|
|
275
|
-
</
|
|
276
|
-
</
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
88
|
+
<div class="divide-y divide-border border-t border-border">{#each webhooks as hook (hook.id)}<div class="flex flex-col gap-3 py-4 sm:flex-row sm:items-center sm:justify-between"><div class="min-w-0"><p class="text-sm font-medium text-foreground">{hook.name}</p><p class="truncate font-mono text-xs text-muted-foreground">{hook.url}</p></div><div class="flex items-center justify-between gap-3 sm:justify-end"><span class="rounded bg-muted px-2 py-1 font-mono text-xs text-muted-foreground">{hook.eventType}</span><Button variant="ghost" size="icon-sm" onclick={() => { webhooks = webhooks.filter((entry) => entry.id !== hook.id); }} aria-label={i18n.t('common.delete') + ' ' + hook.name}><Trash2 class="size-4" /></Button></div></div>{/each}</div>
|
|
89
|
+
{/if}
|
|
90
|
+
</SettingsGroup>
|
|
91
|
+
</div>
|
|
92
|
+
{/snippet}
|
|
93
|
+
{#snippet secondary()}
|
|
94
|
+
<SettingsGroup title={i18n.t('api.generateTitle')} description={i18n.t('api.generateDesc')}>
|
|
95
|
+
<form onsubmit={handleGenerateKey} class="space-y-5"><div class="space-y-2"><Label for="api-key-name">{i18n.t('api.keyName')}</Label><Input id="api-key-name" placeholder="CI deployment token" bind:value={newKeyName} required /></div><fieldset class="space-y-3"><legend class="text-sm font-medium text-foreground">{i18n.t('api.permissions')}</legend>{#each Object.keys(newKeyPermissions) as permission (permission)}<div class="flex items-center gap-2"><Checkbox id={`perm-${permission}`} bind:checked={newKeyPermissions[permission as keyof typeof newKeyPermissions]} /><Label for={`perm-${permission}`} class="cursor-pointer text-sm font-normal">{permission}</Label></div>{/each}</fieldset><Button type="submit" class="w-full">{i18n.t('api.generateButton')}</Button></form>
|
|
96
|
+
</SettingsGroup>
|
|
97
|
+
{/snippet}
|
|
98
|
+
</WorkspaceLayout>
|
|
99
|
+
</ContentPageShell>
|