@svadmin/ui 0.76.0 → 0.78.0
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/app.css +439 -72
- package/dist/app.theme.css +439 -72
- package/dist/component-registry.d.ts +300 -0
- package/dist/component-registry.js +2 -0
- package/dist/components/AboutSettings.svelte +4 -3
- package/dist/components/AboutSettings.svelte.d.ts +1 -0
- package/dist/components/AdminApp.svelte +30 -15
- package/dist/components/ApiSettings.svelte +39 -2
- package/dist/components/AppearanceSettings.svelte +35 -9
- package/dist/components/AppearanceSettings.svelte.d.ts +4 -1
- package/dist/components/AutoTable.svelte +10 -6
- package/dist/components/AutoTable.svelte.d.ts +2 -0
- package/dist/components/IntegrationsSettings.svelte +5 -3
- package/dist/components/IntegrationsSettings.svelte.d.ts +1 -0
- package/dist/components/Layout.svelte +38 -5
- package/dist/components/Layout.svelte.d.ts +4 -3
- package/dist/components/NotificationsSettings.svelte +18 -12
- package/dist/components/NotificationsSettings.svelte.d.ts +1 -0
- package/dist/components/ProfilePage.svelte +3 -3
- package/dist/components/ProfilePage.svelte.d.ts +4 -1
- package/dist/components/ResourceOverview.svelte +36 -0
- package/dist/components/ResourceOverview.svelte.d.ts +6 -0
- package/dist/components/ResourceOverviewCard.svelte +50 -0
- package/dist/components/ResourceOverviewCard.svelte.d.ts +7 -0
- package/dist/components/SecuritySettings.svelte +9 -6
- package/dist/components/SecuritySettings.svelte.d.ts +4 -1
- package/dist/components/Sidebar.svelte +1 -1
- package/dist/components/account/SettingsEnterprisePage.svelte +19 -19
- package/dist/components/account/SettingsPlainPage.svelte +7 -3
- package/dist/components/account/SettingsSidebarPage.svelte +3 -3
- package/dist/components/content/ContentPageHeader.svelte +20 -4
- package/dist/components/content/ContentPageHeader.svelte.d.ts +2 -0
- package/dist/components/content/ContentPageShell.svelte +12 -3
- package/dist/components/content/ContentPageShell.svelte.d.ts +1 -0
- package/dist/components/content/DashboardPage.svelte +35 -0
- package/dist/components/content/DashboardPage.svelte.d.ts +13 -0
- package/dist/components/content/FeedbackNotice.svelte +1 -1
- package/dist/components/content/MetricBlock.svelte +1 -1
- package/dist/components/content/PageSection.svelte +28 -0
- package/dist/components/content/PageSection.svelte.d.ts +10 -0
- package/dist/components/content/SettingsFieldRow.svelte +10 -5
- package/dist/components/content/SettingsFieldRow.svelte.d.ts +1 -0
- package/dist/components/content/SettingsGroup.svelte +3 -3
- package/dist/components/content/SettingsGroup.svelte.d.ts +1 -0
- package/dist/components/content/index.d.ts +2 -0
- package/dist/components/content/index.js +2 -0
- package/dist/components/ui/button/button.svelte +17 -1
- package/dist/components/ui/button/button.svelte.d.ts +1 -0
- package/dist/components/ui/sheet/sheet.svelte +3 -0
- package/dist/default-theme.d.ts +3 -0
- package/dist/default-theme.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/page-chrome.css +4 -4
- package/dist/recipes.d.ts +77 -38
- package/dist/recipes.js +33 -8
- package/package.json +3 -3
|
@@ -21,6 +21,7 @@ export interface NotificationPreferencesProvider {
|
|
|
21
21
|
}
|
|
22
22
|
type $$ComponentProps = {
|
|
23
23
|
preferencesProvider?: NotificationPreferencesProvider;
|
|
24
|
+
headingLevel?: 'h1' | 'h2';
|
|
24
25
|
};
|
|
25
26
|
declare const NotificationsSettings: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
26
27
|
type NotificationsSettings = ReturnType<typeof NotificationsSettings>;
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
import * as Card from './ui/card/index.js';
|
|
14
14
|
import * as Alert from './ui/alert/index.js';
|
|
15
15
|
import PasswordInput from './PasswordInput.svelte';
|
|
16
|
+
import ContentPageHeader from './content/ContentPageHeader.svelte';
|
|
16
17
|
import { User, Mail, Lock, Loader2, AlertCircle, Camera } from '@lucide/svelte';
|
|
17
18
|
|
|
19
|
+
let { headingLevel = 'h1' }: { headingLevel?: 'h1' | 'h2' } = $props();
|
|
18
20
|
const i18n = useTranslation();
|
|
19
21
|
const adminContext = captureAdminContext();
|
|
20
22
|
|
|
@@ -150,9 +152,7 @@
|
|
|
150
152
|
</script>
|
|
151
153
|
|
|
152
154
|
<div class="svadmin-u-b3542e058833">
|
|
153
|
-
<
|
|
154
|
-
<h2 class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{i18n.t('profile.title')}</h2>
|
|
155
|
-
</div>
|
|
155
|
+
<ContentPageHeader title={i18n.t('profile.title')} {headingLevel} />
|
|
156
156
|
|
|
157
157
|
<!-- Profile Info -->
|
|
158
158
|
<Card.Card>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
headingLevel?: 'h1' | 'h2';
|
|
3
|
+
};
|
|
4
|
+
declare const ProfilePage: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
5
|
type ProfilePage = ReturnType<typeof ProfilePage>;
|
|
3
6
|
export default ProfilePage;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { captureAdminContext } from '@svadmin/core';
|
|
3
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
|
+
import ContentPageShell from './content/ContentPageShell.svelte';
|
|
5
|
+
import DataState from './content/DataState.svelte';
|
|
6
|
+
import ResourceOverviewCard from './ResourceOverviewCard.svelte';
|
|
7
|
+
|
|
8
|
+
let { title }: { title?: string } = $props();
|
|
9
|
+
const context = captureAdminContext();
|
|
10
|
+
const i18n = useTranslation();
|
|
11
|
+
const resources = $derived(context.resources
|
|
12
|
+
.filter(resource => resource.showInMenu !== false && !resource.parentName)
|
|
13
|
+
.sort((left, right) => (left.menuOrder ?? 0) - (right.menuOrder ?? 0)));
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<ContentPageShell title={title ?? i18n.t('common.dashboard')} pageId="resource-overview" width="wide">
|
|
17
|
+
{#if resources.length}
|
|
18
|
+
<div class="svadmin-resource-overview-grid">
|
|
19
|
+
{#each resources as resource (resource.identifier ?? resource.name)}
|
|
20
|
+
<ResourceOverviewCard {resource} />
|
|
21
|
+
{/each}
|
|
22
|
+
</div>
|
|
23
|
+
{:else}
|
|
24
|
+
<DataState state="empty" />
|
|
25
|
+
{/if}
|
|
26
|
+
</ContentPageShell>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.svadmin-resource-overview-grid {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
|
32
|
+
align-items: start;
|
|
33
|
+
gap: 1rem;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useCan, useNavigation, type ResourceDefinition } from '@svadmin/core';
|
|
3
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
|
+
import { ArrowRight, Plus } from '@lucide/svelte';
|
|
5
|
+
import { Button } from './ui/button/index.js';
|
|
6
|
+
import DataState from './content/DataState.svelte';
|
|
7
|
+
|
|
8
|
+
let { resource }: { resource: ResourceDefinition } = $props();
|
|
9
|
+
const i18n = useTranslation();
|
|
10
|
+
const navigation = useNavigation();
|
|
11
|
+
const read = useCan(() => ({ resource: resource.name, action: 'list' }));
|
|
12
|
+
const create = useCan(() => ({ resource: resource.name, action: 'create' }));
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<section class="svadmin-resource-overview-card" aria-label={resource.label}>
|
|
16
|
+
<h2>{resource.label}</h2>
|
|
17
|
+
{#if read.isLoading}
|
|
18
|
+
<DataState state="loading" />
|
|
19
|
+
{:else if !read.allowed}
|
|
20
|
+
<DataState state="forbidden" />
|
|
21
|
+
{:else}
|
|
22
|
+
<div class="svadmin-resource-overview-actions">
|
|
23
|
+
<Button variant="outline" onclick={() => navigation.list(resource.name)}>
|
|
24
|
+
{i18n.t('profileSections.viewAll')}<ArrowRight aria-hidden="true" />
|
|
25
|
+
</Button>
|
|
26
|
+
{#if resource.canCreate !== false && !create.isLoading && create.allowed}
|
|
27
|
+
<Button onclick={() => navigation.create(resource.name)}>
|
|
28
|
+
<Plus aria-hidden="true" />{i18n.t('common.create')}
|
|
29
|
+
</Button>
|
|
30
|
+
{/if}
|
|
31
|
+
</div>
|
|
32
|
+
{/if}
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.svadmin-resource-overview-card {
|
|
37
|
+
display: grid;
|
|
38
|
+
align-content: start;
|
|
39
|
+
gap: 1rem;
|
|
40
|
+
min-width: 0;
|
|
41
|
+
padding: 1rem;
|
|
42
|
+
border: 1px solid var(--border);
|
|
43
|
+
border-radius: var(--radius);
|
|
44
|
+
background: var(--card);
|
|
45
|
+
color: var(--card-foreground);
|
|
46
|
+
box-shadow: var(--shadow-surface);
|
|
47
|
+
}
|
|
48
|
+
h2 { margin: 0; font-size: 1rem; font-weight: 600; overflow-wrap: anywhere; }
|
|
49
|
+
.svadmin-resource-overview-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ResourceDefinition } from '@svadmin/core';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
resource: ResourceDefinition;
|
|
4
|
+
};
|
|
5
|
+
declare const ResourceOverviewCard: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type ResourceOverviewCard = ReturnType<typeof ResourceOverviewCard>;
|
|
7
|
+
export default ResourceOverviewCard;
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
import DataState from './content/DataState.svelte';
|
|
12
12
|
import FeedbackNotice from './content/FeedbackNotice.svelte';
|
|
13
13
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
14
|
+
import ContentPageHeader from './content/ContentPageHeader.svelte';
|
|
14
15
|
|
|
16
|
+
let { headingLevel = 'h1' }: { headingLevel?: 'h1' | 'h2' } = $props();
|
|
17
|
+
const controlId = $props.id();
|
|
15
18
|
const i18n = useTranslation();
|
|
16
19
|
const adminContext = captureAdminContext();
|
|
17
20
|
const authProvider = $derived(adminContext.authProvider);
|
|
@@ -179,16 +182,16 @@
|
|
|
179
182
|
</script>
|
|
180
183
|
|
|
181
184
|
<div class="svadmin-u-b3542e058833">
|
|
182
|
-
<
|
|
185
|
+
<ContentPageHeader title={i18n.t('settings.security')} description={i18n.t('settings.securityDescription')} {headingLevel} />
|
|
183
186
|
{#if !sessionProvider}
|
|
184
187
|
<FeedbackNotice tone="warning" message={isZh ? '未配置 SessionProvider。会话和 MFA 设置不会使用模拟数据。' : 'SessionProvider is not configured. Session and MFA controls do not use simulated data.'} />
|
|
185
188
|
{/if}
|
|
186
|
-
<SettingsGroup title={i18n.t('security.twoFactorAuth')} description={i18n.t('security.twoFactorDescription')}>
|
|
187
|
-
<SettingsFieldRow label={i18n.t('security.enable2fa')} description={is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}>
|
|
188
|
-
{#snippet control()}<Switch bind:checked={() => is2faEnabled, (enabled) => { void handleMfaChange(enabled); }} disabled={!sessionProvider?.setMfaEnabled || updatingMfa} />{/snippet}
|
|
189
|
+
<SettingsGroup title={i18n.t('security.twoFactorAuth')} description={i18n.t('security.twoFactorDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
|
|
190
|
+
<SettingsFieldRow controlId={`${controlId}-mfa`} label={i18n.t('security.enable2fa')} description={is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}>
|
|
191
|
+
{#snippet control()}<Switch id={`${controlId}-mfa`} bind:checked={() => is2faEnabled, (enabled) => { void handleMfaChange(enabled); }} disabled={!sessionProvider?.setMfaEnabled || updatingMfa} />{/snippet}
|
|
189
192
|
</SettingsFieldRow>
|
|
190
193
|
</SettingsGroup>
|
|
191
|
-
<SettingsGroup title={i18n.t('profile.changePassword')} description={isZh ? '密码修改由 AuthProvider 持久化。' : 'Password changes are persisted by AuthProvider.'}>
|
|
194
|
+
<SettingsGroup title={i18n.t('profile.changePassword')} description={isZh ? '密码修改由 AuthProvider 持久化。' : 'Password changes are persisted by AuthProvider.'} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
|
|
192
195
|
{#if !authProvider?.updatePassword}<FeedbackNotice tone="warning" message={isZh ? '当前 AuthProvider 不支持修改密码。' : 'The current AuthProvider does not support password updates.'} />{/if}
|
|
193
196
|
<form onsubmit={handlePasswordChange} class="svadmin-u-6199866f612f svadmin-u-3e7ce58d64fa">
|
|
194
197
|
{#if passwordErrorMessage}<Alert.Root variant="destructive"><AlertTriangle class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /><Alert.Description>{passwordErrorMessage}</Alert.Description></Alert.Root>{/if}
|
|
@@ -198,7 +201,7 @@
|
|
|
198
201
|
<Button type="submit" disabled={!authProvider?.updatePassword || changingPassword}>{changingPassword ? (isZh ? '更新中...' : 'Updating...') : i18n.t('profile.updatePassword')}</Button>
|
|
199
202
|
</form>
|
|
200
203
|
</SettingsGroup>
|
|
201
|
-
<SettingsGroup title={i18n.t('security.activeSessions')} description={i18n.t('security.sessionsDescription')}>
|
|
204
|
+
<SettingsGroup title={i18n.t('security.activeSessions')} description={i18n.t('security.sessionsDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
|
|
202
205
|
{#snippet actions()}{#if sessions.some((session) => !session.current)}<Button variant="outline" size="sm" disabled={revoking !== null} onclick={() => revokeAllOthers()}>{i18n.t('security.revokeOthers')}</Button>{/if}{/snippet}
|
|
203
206
|
{#if loadingSessions}
|
|
204
207
|
<DataState state="loading" />
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
headingLevel?: 'h1' | 'h2';
|
|
3
|
+
};
|
|
4
|
+
declare const SecuritySettings: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
5
|
type SecuritySettings = ReturnType<typeof SecuritySettings>;
|
|
3
6
|
export default SecuritySettings;
|
|
@@ -427,7 +427,7 @@
|
|
|
427
427
|
<span class="svadmin-u-36e579c0b41c svadmin-u-2eba0d65d059">{i18n.t('common.toggleTheme')}</span>
|
|
428
428
|
<span
|
|
429
429
|
class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29 svadmin-u-ac204c108886 svadmin-u-3daca9af0861 svadmin-u-823f000e740a svadmin-u-6f8ab8abb126"
|
|
430
|
-
style="background-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#
|
|
430
|
+
style="background-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#635bff'}; --tw-ring-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#635bff'}"
|
|
431
431
|
></span>
|
|
432
432
|
<!-- The dot previews the user-selected runtime theme swatch; the hex
|
|
433
433
|
fallback only covers the case where no registered theme matches. -->
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import FeedbackNotice from '../content/FeedbackNotice.svelte';
|
|
26
26
|
|
|
27
27
|
const i18n = useTranslation();
|
|
28
|
+
const controlId = $props.id();
|
|
28
29
|
const adminContext = captureAdminContext();
|
|
29
30
|
const identityProvider = $derived(adminContext.identityGovernanceProvider);
|
|
30
31
|
const organizationProvider = $derived(adminContext.organizationProvider);
|
|
@@ -50,16 +51,12 @@
|
|
|
50
51
|
&& organizationName.trim()
|
|
51
52
|
&& organizationName.trim() !== organization.name,
|
|
52
53
|
));
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
&& Number.isSafeInteger(auditRetention)
|
|
60
|
-
&& sessionTimeout > 0
|
|
61
|
-
&& auditRetention > 0;
|
|
62
|
-
});
|
|
54
|
+
function isPositiveInteger(value: string): boolean {
|
|
55
|
+
return /^\d+$/.test(value) && Number.isSafeInteger(Number(value)) && Number(value) > 0;
|
|
56
|
+
}
|
|
57
|
+
const sessionTimeoutValid = $derived(isPositiveInteger(sessionTimeoutValue));
|
|
58
|
+
const auditRetentionValid = $derived(isPositiveInteger(auditRetentionValue));
|
|
59
|
+
const policyNumbersValid = $derived(sessionTimeoutValid && auditRetentionValid);
|
|
63
60
|
|
|
64
61
|
function message(error: unknown): string {
|
|
65
62
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -202,11 +199,11 @@
|
|
|
202
199
|
</script>
|
|
203
200
|
|
|
204
201
|
{#snippet auditControl()}
|
|
205
|
-
<Switch checked={policy?.auditLoggingEnabled ?? false} disabled={!policy || saving}
|
|
202
|
+
<Switch id={`${controlId}-audit`} checked={policy?.auditLoggingEnabled ?? false} disabled={!policy || saving} onCheckedChange={(checked) => { if (policy) policy = { ...policy, auditLoggingEnabled: checked }; }} />
|
|
206
203
|
{/snippet}
|
|
207
204
|
|
|
208
205
|
{#snippet ssoControl()}
|
|
209
|
-
<Switch checked={policy?.requireSso ?? false} disabled={!policy || saving}
|
|
206
|
+
<Switch id={`${controlId}-sso`} checked={policy?.requireSso ?? false} disabled={!policy || saving} onCheckedChange={(checked) => { if (policy) policy = { ...policy, requireSso: checked }; }} />
|
|
210
207
|
{/snippet}
|
|
211
208
|
|
|
212
209
|
<ContentPageShell pageId="account-settings-enterprise" width="wide">
|
|
@@ -222,19 +219,22 @@
|
|
|
222
219
|
<div class="svadmin-u-f3c543ad5fe9 svadmin-u-60541e1e26f8 svadmin-u-0c3bc98565dd svadmin-u-2f27a80ed92b">
|
|
223
220
|
<SettingsGroup title={i18n.t('account.securityPolicy')} description={isZh ? '配置组织级身份、会话与审计策略。' : 'Configure organization-wide identity, session, and audit controls.'} bodyClass="space-y-4">
|
|
224
221
|
{#if organizationProvider}
|
|
225
|
-
<div class="svadmin-u-6f7e013d6499"><Label for=
|
|
222
|
+
<div class="svadmin-u-6f7e013d6499"><Label for={`${controlId}-organization-name`}>{isZh ? '组织名称' : 'Organization name'}</Label><div class="svadmin-u-60fbb7713999 svadmin-u-77a2a20e90d4"><Input id={`${controlId}-organization-name`} bind:value={organizationName} disabled={!organizationProvider.updateCurrentOrganization || savingOrganization} /><Button variant="outline" size="sm" disabled={!organizationChanged || savingOrganization} onclick={saveOrganization}>{savingOrganization ? (isZh ? '保存中...' : 'Saving...') : i18n.t('common.save')}</Button></div></div>
|
|
226
223
|
{/if}
|
|
227
224
|
<div class="svadmin-u-f3c543ad5fe9 svadmin-u-0c3bc98565dd svadmin-u-e00ad81645a2">
|
|
228
|
-
<div class="svadmin-u-6f7e013d6499"><Label for=
|
|
229
|
-
<div class="svadmin-u-6f7e013d6499"><Label for=
|
|
225
|
+
<div class="svadmin-u-6f7e013d6499"><Label for={`${controlId}-session-timeout`}>{i18n.t('account.sessionTimeout')}</Label><Input id={`${controlId}-session-timeout`} type="number" min="1" step="1" bind:value={sessionTimeoutValue} disabled={!policy || saving} aria-invalid={policy ? !sessionTimeoutValid : undefined} aria-describedby={policy && !sessionTimeoutValid ? `${controlId}-policy-error` : undefined} /></div>
|
|
226
|
+
<div class="svadmin-u-6f7e013d6499"><Label for={`${controlId}-retention`}>{i18n.t('account.dataRetention')}</Label><Input id={`${controlId}-retention`} type="number" min="1" step="1" bind:value={auditRetentionValue} disabled={!policy || saving} aria-invalid={policy ? !auditRetentionValid : undefined} aria-describedby={policy && !auditRetentionValid ? `${controlId}-policy-error` : undefined} /></div>
|
|
230
227
|
</div>
|
|
231
|
-
{#if policy && !policyNumbersValid}<FeedbackNotice tone="warning" message={isZh ? '会话超时和审计保留天数必须为正整数。' : 'Session timeout and audit retention must be positive integers.'}
|
|
232
|
-
<SettingsFieldRow label={i18n.t('account.auditLogging')} description={isZh ? '由后端持久化组织级审计策略。' : 'Persist the organization audit policy through the backend provider.'} control={auditControl} separated />
|
|
228
|
+
{#if policy && !policyNumbersValid}<div id={`${controlId}-policy-error`}><FeedbackNotice tone="warning" message={isZh ? '会话超时和审计保留天数必须为正整数。' : 'Session timeout and audit retention must be positive integers.'} /></div>{/if}
|
|
229
|
+
<SettingsFieldRow label={i18n.t('account.auditLogging')} controlId={`${controlId}-audit`} description={isZh ? '由后端持久化组织级审计策略。' : 'Persist the organization audit policy through the backend provider.'} control={auditControl} separated />
|
|
233
230
|
<Button size="sm" disabled={!identityProvider || !policy || !policyNumbersValid || saving} onclick={saveSettings}>{saving ? (isZh ? '保存中...' : 'Saving...') : i18n.t('common.save')}</Button>
|
|
234
231
|
</SettingsGroup>
|
|
235
232
|
<SettingsGroup title={i18n.t('account.singleSignOn')} description={i18n.t('account.ssoDescription')} bodyClass="space-y-4">
|
|
236
|
-
<SettingsFieldRow label={i18n.t('account.enableSso')} description={isZh ? '启用后由已配置身份源执行组织登录策略。' : 'Require the configured identity provider for organization members.'} control={ssoControl} />
|
|
237
|
-
<
|
|
233
|
+
<SettingsFieldRow label={i18n.t('account.enableSso')} controlId={`${controlId}-sso`} description={isZh ? '启用后由已配置身份源执行组织登录策略。' : 'Require the configured identity provider for organization members.'} control={ssoControl} />
|
|
234
|
+
<div class="svadmin-u-6f7e013d6499">
|
|
235
|
+
<Label for={`${controlId}-metadata-url`}>{i18n.t('account.ssoProvider')}</Label>
|
|
236
|
+
<Input id={`${controlId}-metadata-url`} type="url" bind:value={metadataUrl} placeholder="https://idp.example.com/.well-known/openid-configuration" disabled={!identityProvider || testing} />
|
|
237
|
+
</div>
|
|
238
238
|
<Button variant="outline" size="sm" disabled={!identityProvider || testing} onclick={() => testConnection()}>{testing ? (isZh ? '测试中...' : 'Testing...') : i18n.t('common.test')}</Button>
|
|
239
239
|
</SettingsGroup>
|
|
240
240
|
</div>
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
import NotificationsSettings from '../NotificationsSettings.svelte';
|
|
4
4
|
import SecuritySettings from '../SecuritySettings.svelte';
|
|
5
5
|
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
6
|
+
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
7
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
8
|
+
const i18n = useTranslation();
|
|
6
9
|
let { preferencesProvider }: { preferencesProvider?: import('../NotificationsSettings.svelte').NotificationPreferencesProvider } = $props();
|
|
7
10
|
</script>
|
|
8
11
|
|
|
9
12
|
<ContentPageShell pageId="account-settings-plain" width="narrow">
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
+
<ContentPageHeader title={i18n.t('account.settingsPlain')} />
|
|
14
|
+
<ProfilePage headingLevel="h2" />
|
|
15
|
+
<NotificationsSettings headingLevel="h2" {...preferencesProvider ? { preferencesProvider } : {}} />
|
|
16
|
+
<SecuritySettings headingLevel="h2" />
|
|
13
17
|
</ContentPageShell>
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
</nav>
|
|
28
28
|
<div class="svadmin-u-7e0b7cdf1a94">
|
|
29
29
|
{#if active === 'profile'}
|
|
30
|
-
<ProfilePage />
|
|
30
|
+
<ProfilePage headingLevel="h2" />
|
|
31
31
|
{:else if active === 'signin'}
|
|
32
|
-
<IntegrationsSettings />
|
|
32
|
+
<IntegrationsSettings headingLevel="h2" />
|
|
33
33
|
{:else if active === 'preferences'}
|
|
34
|
-
<AppearanceSettings />
|
|
34
|
+
<AppearanceSettings headingLevel="h2" />
|
|
35
35
|
{:else}
|
|
36
36
|
<SettingsGroup title={i18n.t('account.manageApi')} description={isZh ? '使用有作用域的真实凭据,并在权限变更时轮换。' : 'Use scoped credentials and rotate them when access changes.'} bodyClass="space-y-3">
|
|
37
37
|
{#if credentialProvider}
|
|
@@ -8,13 +8,29 @@
|
|
|
8
8
|
description?: string;
|
|
9
9
|
breadcrumbs?: string[];
|
|
10
10
|
actions?: Snippet;
|
|
11
|
+
density?: 'compact' | 'comfortable';
|
|
12
|
+
headingLevel?: 'h1' | 'h2';
|
|
11
13
|
class?: string;
|
|
12
14
|
}
|
|
13
|
-
let {
|
|
14
|
-
|
|
15
|
+
let {
|
|
16
|
+
title,
|
|
17
|
+
eyebrow,
|
|
18
|
+
description,
|
|
19
|
+
breadcrumbs = [],
|
|
20
|
+
actions,
|
|
21
|
+
density = 'comfortable',
|
|
22
|
+
headingLevel = 'h1',
|
|
23
|
+
class: className = '',
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
const styles = $derived(contentHeader({ density }));
|
|
15
26
|
</script>
|
|
16
27
|
|
|
17
|
-
<header
|
|
28
|
+
<header
|
|
29
|
+
class={styles.root + ' ' + className}
|
|
30
|
+
data-svadmin-page-header
|
|
31
|
+
data-svadmin-content-header
|
|
32
|
+
data-density={density}
|
|
33
|
+
>
|
|
18
34
|
{#if breadcrumbs.length > 0}
|
|
19
35
|
<nav aria-label="Breadcrumb" class={styles.breadcrumbs}>
|
|
20
36
|
{#each breadcrumbs as crumb, index (crumb)}
|
|
@@ -26,7 +42,7 @@
|
|
|
26
42
|
<div class={styles.row}>
|
|
27
43
|
<div class={styles.heading}>
|
|
28
44
|
{#if eyebrow}<p class={styles.eyebrow}>{eyebrow}</p>{/if}
|
|
29
|
-
<
|
|
45
|
+
<svelte:element this={headingLevel} class={styles.title}>{title}</svelte:element>
|
|
30
46
|
{#if description}<p class={styles.description}>{description}</p>{/if}
|
|
31
47
|
</div>
|
|
32
48
|
{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
children: Snippet;
|
|
13
13
|
pageId?: string;
|
|
14
14
|
width?: 'narrow' | 'default' | 'wide';
|
|
15
|
+
density?: 'compact' | 'comfortable';
|
|
15
16
|
class?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -23,16 +24,24 @@
|
|
|
23
24
|
children,
|
|
24
25
|
pageId = 'content',
|
|
25
26
|
width = 'default',
|
|
27
|
+
density = 'comfortable',
|
|
26
28
|
class: className = '',
|
|
27
29
|
}: Props = $props();
|
|
28
30
|
|
|
29
31
|
// 保留旧组件对非窄屏/宽屏输入的默认宽度回退。
|
|
32
|
+
const resolvedWidth = $derived(width === 'narrow' || width === 'wide' ? width : 'default');
|
|
30
33
|
const styles = $derived(contentPage({
|
|
31
|
-
width:
|
|
34
|
+
width: resolvedWidth,
|
|
35
|
+
density,
|
|
32
36
|
}));
|
|
33
37
|
</script>
|
|
34
38
|
|
|
35
|
-
<div
|
|
36
|
-
{
|
|
39
|
+
<div
|
|
40
|
+
data-svadmin-content-page={pageId}
|
|
41
|
+
data-svadmin-content-page-width={resolvedWidth}
|
|
42
|
+
data-density={density}
|
|
43
|
+
class={styles.root + ' ' + className}
|
|
44
|
+
>
|
|
45
|
+
{#if title}<ContentPageHeader {title} {density} {...definedOptions({ "eyebrow": eyebrow })} {...definedOptions({ "description": description })} {...definedOptions({ "actions": actions })} />{/if}
|
|
37
46
|
{@render children()}
|
|
38
47
|
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
4
|
+
import ContentPageShell from './ContentPageShell.svelte';
|
|
5
|
+
import WorkspaceLayout from './WorkspaceLayout.svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
title, description, actions, metrics, secondary, children,
|
|
9
|
+
pageId = 'dashboard',
|
|
10
|
+
}: {
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
actions?: Snippet;
|
|
14
|
+
metrics?: Snippet;
|
|
15
|
+
secondary?: Snippet;
|
|
16
|
+
children: Snippet;
|
|
17
|
+
pageId?: string;
|
|
18
|
+
} = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<ContentPageShell {title} {pageId} width="wide" {...definedOptions({ description, actions })}>
|
|
22
|
+
{#if metrics}
|
|
23
|
+
<div class="svadmin-dashboard-metrics" data-svadmin-dashboard-metrics>{@render metrics()}</div>
|
|
24
|
+
{/if}
|
|
25
|
+
<WorkspaceLayout primary={children} {...definedOptions({ secondary })} />
|
|
26
|
+
</ContentPageShell>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.svadmin-dashboard-metrics {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
|
32
|
+
gap: 1rem;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
actions?: Snippet;
|
|
6
|
+
metrics?: Snippet;
|
|
7
|
+
secondary?: Snippet;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
pageId?: string;
|
|
10
|
+
};
|
|
11
|
+
declare const DashboardPage: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type DashboardPage = ReturnType<typeof DashboardPage>;
|
|
13
|
+
export default DashboardPage;
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
tone === 'danger'
|
|
28
28
|
? 'svadmin-u-26e4f7bdbd56 svadmin-u-7a0854fdbc30 svadmin-u-811148b13d1e'
|
|
29
29
|
: tone === 'warning'
|
|
30
|
-
? 'svadmin-u-d008dee27eaa svadmin-u-283481e780bb svadmin-u-
|
|
30
|
+
? 'svadmin-u-d008dee27eaa svadmin-u-283481e780bb svadmin-u-d4108abe6359'
|
|
31
31
|
: 'svadmin-u-18049387f0af svadmin-u-b00f43c30c2b svadmin-u-d4108abe6359',
|
|
32
32
|
);
|
|
33
33
|
</script>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
{#if loading}
|
|
36
36
|
<Skeleton class={styles.skeleton} />
|
|
37
37
|
{:else}
|
|
38
|
-
<p class={styles.value}>{value}</p>
|
|
38
|
+
<p data-svadmin-metric-value class={styles.value}>{value}</p>
|
|
39
39
|
{#if detail || trend}<div class={styles.meta}>{#if trend}<span class={styles.trend}>{trend}</span>{/if}{#if detail}<span class={styles.detail}>{detail}</span>{/if}</div>{/if}
|
|
40
40
|
{/if}
|
|
41
41
|
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
4
|
+
import SectionHeader from './SectionHeader.svelte';
|
|
5
|
+
|
|
6
|
+
let { title, description, actions, children }: {
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
actions?: Snippet;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
} = $props();
|
|
12
|
+
const id = $props.id();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<section class="svadmin-page-section" aria-labelledby={id} data-svadmin-page-section>
|
|
16
|
+
<SectionHeader {id} {title} {...definedOptions({ description, actions })} />
|
|
17
|
+
{@render children()}
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<style>
|
|
21
|
+
.svadmin-page-section {
|
|
22
|
+
display: grid;
|
|
23
|
+
grid-template-columns: minmax(0, 1fr);
|
|
24
|
+
align-content: start;
|
|
25
|
+
gap: 1rem;
|
|
26
|
+
min-width: 0;
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
actions?: Snippet;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const PageSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type PageSection = ReturnType<typeof PageSection>;
|
|
10
|
+
export default PageSection;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { productSettingsRowRecipe as productSettingsRow } from '../../recipes.js';
|
|
4
|
-
interface Props { label: string; description?: string; control: Snippet; separated?: boolean; class?: string; }
|
|
5
|
-
let { label, description, control, separated = false, class: className = '' }: Props = $props();
|
|
4
|
+
interface Props { label: string; description?: string; control: Snippet; controlId?: string; separated?: boolean; class?: string; }
|
|
5
|
+
let { label, description, control, controlId, separated = false, class: className = '' }: Props = $props();
|
|
6
|
+
const labelId = $props.id();
|
|
6
7
|
const styles = $derived(productSettingsRow({ separated }));
|
|
7
8
|
</script>
|
|
8
9
|
<div class={styles.root + ' ' + className} data-svadmin-settings-field-row>
|
|
9
10
|
<div class={styles.heading}>
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
{#if controlId}
|
|
12
|
+
<label id={labelId} for={controlId} class={styles.label}>{label}</label>
|
|
13
|
+
{:else}
|
|
14
|
+
<p id={labelId} class={styles.label}>{label}</p>
|
|
15
|
+
{/if}
|
|
16
|
+
{#if description}<p id={`${labelId}-description`} class={styles.description}>{description}</p>{/if}
|
|
12
17
|
</div>
|
|
13
|
-
<div class={styles.control}>{@render control()}</div>
|
|
18
|
+
<div class={styles.control} role="group" aria-labelledby={labelId} aria-describedby={description ? `${labelId}-description` : undefined}>{@render control()}</div>
|
|
14
19
|
</div>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { productSettingsRecipe as productSettings } from '../../recipes.js';
|
|
4
|
-
interface Props { title: string; description?: string; actions?: Snippet; children: Snippet; class?: string; bodyClass?: string; }
|
|
5
|
-
let { title, description, actions, children, class: className = '', bodyClass = '' }: Props = $props();
|
|
4
|
+
interface Props { title: string; description?: string; actions?: Snippet; children: Snippet; headingLevel?: 'h2' | 'h3'; class?: string; bodyClass?: string; }
|
|
5
|
+
let { title, description, actions, children, headingLevel = 'h2', class: className = '', bodyClass = '' }: Props = $props();
|
|
6
6
|
const styles = productSettings();
|
|
7
7
|
</script>
|
|
8
8
|
<section class={styles.root + ' ' + className} data-svadmin-settings-group>
|
|
9
9
|
<header class={styles.header}>
|
|
10
10
|
<div class={styles.heading}>
|
|
11
|
-
<
|
|
11
|
+
<svelte:element this={headingLevel} class={styles.title}>{title}</svelte:element>
|
|
12
12
|
{#if description}<p class={styles.description}>{description}</p>{/if}
|
|
13
13
|
</div>
|
|
14
14
|
{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as ContentPageShell } from './ContentPageShell.svelte';
|
|
2
|
+
export { default as DashboardPage } from './DashboardPage.svelte';
|
|
3
|
+
export { default as PageSection } from './PageSection.svelte';
|
|
2
4
|
export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
3
5
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
6
|
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as ContentPageShell } from './ContentPageShell.svelte';
|
|
2
|
+
export { default as DashboardPage } from './DashboardPage.svelte';
|
|
3
|
+
export { default as PageSection } from './PageSection.svelte';
|
|
2
4
|
export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
3
5
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
6
|
export { default as PageToolbar } from './PageToolbar.svelte';
|