@svadmin/ui 0.42.2 → 0.43.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 +482 -840
- package/dist/app.theme.css +482 -840
- package/dist/components/ErrorPage.svelte +4 -56
- package/dist/components/Layout.svelte +1 -1
- package/dist/components/Sidebar.svelte +1 -0
- package/dist/components/TwoFactorAuthPage.svelte +44 -232
- package/dist/components/account/CompanyProfilePage.svelte +21 -142
- package/dist/components/account/GetStartedPage.svelte +33 -115
- package/dist/components/account/GetStartedPage.svelte.d.ts +1 -2
- package/dist/components/account/SecurityLogPage.svelte +16 -131
- package/dist/components/account/SettingsEnterprisePage.svelte +15 -275
- package/dist/components/account/SettingsPlainPage.svelte +10 -175
- package/dist/components/account/SettingsSidebarPage.svelte +11 -304
- package/dist/components/account/TeamMembersPage.svelte +18 -187
- package/dist/components/account/UserProfilePage.svelte +19 -196
- package/dist/components/account/UserProfilePage.svelte.d.ts +1 -2
- package/dist/components/content/ApiKeyList.svelte +13 -0
- package/dist/components/content/ApiKeyList.svelte.d.ts +16 -0
- package/dist/components/content/ContentPageHeader.svelte +31 -0
- package/dist/components/content/ContentPageHeader.svelte.d.ts +12 -0
- package/dist/components/content/ContentPageShell.svelte +39 -0
- package/dist/components/content/ContentPageShell.svelte.d.ts +14 -0
- package/dist/components/content/DataState.svelte +44 -0
- package/dist/components/content/DataState.svelte.d.ts +13 -0
- package/dist/components/content/DescriptionList.svelte +27 -0
- package/dist/components/content/DescriptionList.svelte.d.ts +14 -0
- package/dist/components/content/FileList.svelte +13 -0
- package/dist/components/content/FileList.svelte.d.ts +16 -0
- package/dist/components/content/FilterToolbar.svelte +24 -0
- package/dist/components/content/FilterToolbar.svelte.d.ts +11 -0
- package/dist/components/content/ImportDropzone.svelte +19 -0
- package/dist/components/content/ImportDropzone.svelte.d.ts +10 -0
- package/dist/components/content/IntegrationCard.svelte +16 -0
- package/dist/components/content/IntegrationCard.svelte.d.ts +15 -0
- package/dist/components/content/MemberList.svelte +16 -0
- package/dist/components/content/MemberList.svelte.d.ts +17 -0
- package/dist/components/content/MetricBlock.svelte +27 -0
- package/dist/components/content/MetricBlock.svelte.d.ts +13 -0
- package/dist/components/content/NetworkTable.svelte +18 -0
- package/dist/components/content/NetworkTable.svelte.d.ts +31 -0
- package/dist/components/content/NetworkTable.types.d.ts +4 -0
- package/dist/components/content/NetworkTable.types.js +1 -0
- package/dist/components/content/NetworkUserCard.svelte +17 -0
- package/dist/components/content/NetworkUserCard.svelte.d.ts +17 -0
- package/dist/components/content/OtpInput.svelte +22 -0
- package/dist/components/content/OtpInput.svelte.d.ts +10 -0
- package/dist/components/content/PageToolbar.svelte +18 -0
- package/dist/components/content/PageToolbar.svelte.d.ts +10 -0
- package/dist/components/content/ProjectCard.svelte +39 -0
- package/dist/components/content/ProjectCard.svelte.d.ts +20 -0
- package/dist/components/content/SectionHeader.svelte +18 -0
- package/dist/components/content/SectionHeader.svelte.d.ts +10 -0
- package/dist/components/content/SecurityEventTable.svelte +18 -0
- package/dist/components/content/SecurityEventTable.svelte.d.ts +15 -0
- package/dist/components/content/StatusBadge.svelte +19 -0
- package/dist/components/content/StatusBadge.svelte.d.ts +9 -0
- package/dist/components/content/SystemErrorState.svelte +18 -0
- package/dist/components/content/SystemErrorState.svelte.d.ts +10 -0
- package/dist/components/content/TeamCard.svelte +26 -0
- package/dist/components/content/TeamCard.svelte.d.ts +23 -0
- package/dist/components/content/TwoFactorStepper.svelte +11 -0
- package/dist/components/content/TwoFactorStepper.svelte.d.ts +8 -0
- package/dist/components/content/index.d.ts +32 -0
- package/dist/components/content/index.js +22 -0
- package/dist/components/network/TeamCrewTablePage.svelte +29 -181
- package/dist/components/network/UserCardsNFTPage.svelte +24 -137
- package/dist/components/profile/ActivityTimeline.svelte +17 -91
- package/dist/components/profile/ProfileCard.svelte +4 -4
- package/dist/components/profile/ProjectsGrid.svelte +28 -106
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +2 -12
- package/dist/components/profile/PublicProfilePage.svelte +31 -181
- package/dist/components/profile/TeamsShowcase.svelte +12 -114
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +2 -12
- package/dist/index.d.ts +36 -0
- package/dist/index.js +24 -0
- package/dist/reference-data.d.ts +171 -0
- package/dist/reference-data.js +39 -0
- package/package.json +1 -1
|
@@ -1,125 +1,43 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
+
import { ArrowRight, Camera, Check, Puzzle, UserPlus, Users } from '@lucide/svelte';
|
|
3
4
|
import * as Card from '../ui/card/index.js';
|
|
4
|
-
import { Button } from '../ui/button/index.js';
|
|
5
5
|
import { Badge } from '../ui/badge/index.js';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { Button } from '../ui/button/index.js';
|
|
7
|
+
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
8
|
+
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
9
|
+
import TwoFactorStepper from '../content/TwoFactorStepper.svelte';
|
|
8
10
|
|
|
9
11
|
const i18n = useTranslation();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
title:
|
|
14
|
-
description:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ key: 'profile', title: i18n.t('account.setupProfile'), description: i18n.t('account.getStartedDescription'), icon: UserPlus, completed: false },
|
|
24
|
-
{ key: 'connect', title: i18n.t('account.connectProvider'), description: i18n.t('account.getStartedDescription'), icon: Puzzle, completed: false },
|
|
25
|
-
{ key: 'team', title: i18n.t('account.setupTeam'), description: i18n.t('account.getStartedDescription'), icon: Users, completed: false },
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
const completedCount = $derived(steps.filter(s => s.completed).length);
|
|
29
|
-
const progressPercent = $derived(Math.round((completedCount / steps.length) * 100));
|
|
30
|
-
const allCompleted = $derived(completedCount === steps.length);
|
|
31
|
-
|
|
32
|
-
function markComplete(index: number) {
|
|
33
|
-
steps[index] = { ...steps[index], completed: true };
|
|
34
|
-
if (currentStep < steps.length - 1) {
|
|
35
|
-
currentStep = index + 1;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function goToStep(index: number) {
|
|
40
|
-
if (index <= completedCount) {
|
|
41
|
-
currentStep = index;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function skipStep() {
|
|
46
|
-
if (currentStep < steps.length - 1) {
|
|
47
|
-
currentStep++;
|
|
48
|
-
}
|
|
12
|
+
const steps = [
|
|
13
|
+
{ title: i18n.t('account.uploadAvatar'), description: 'Add a recognizable profile image for your workspace.', icon: Camera },
|
|
14
|
+
{ title: i18n.t('account.setupProfile'), description: 'Complete your contact details and working preferences.', icon: UserPlus },
|
|
15
|
+
{ title: i18n.t('account.connectProvider'), description: 'Connect the services your team uses every day.', icon: Puzzle },
|
|
16
|
+
{ title: i18n.t('account.setupTeam'), description: 'Invite teammates and set the right access level.', icon: Users },
|
|
17
|
+
];
|
|
18
|
+
let completed = $state<boolean[]>(steps.map(() => false));
|
|
19
|
+
let current = $state(0);
|
|
20
|
+
const completedCount = $derived(completed.filter(Boolean).length);
|
|
21
|
+
const progress = $derived(Math.round(completedCount / steps.length * 100));
|
|
22
|
+
function complete(index: number) {
|
|
23
|
+
completed[index] = true;
|
|
24
|
+
current = Math.min(index + 1, steps.length - 1);
|
|
49
25
|
}
|
|
50
26
|
</script>
|
|
51
27
|
|
|
52
|
-
<
|
|
53
|
-
<div class="flex items-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
28
|
+
<ContentPageShell pageId="account-get-started" width="narrow">
|
|
29
|
+
<div class="flex items-center justify-between gap-3"><ContentPageHeader title={i18n.t('account.getStarted')} description={i18n.t('account.getStartedDescription')} /><Badge variant="secondary">{progress}%</Badge></div>
|
|
30
|
+
<TwoFactorStepper current={completedCount} steps={steps.map((step) => step.title)} />
|
|
31
|
+
<div class="space-y-3">
|
|
32
|
+
{#each steps as step, index (step.title)}
|
|
33
|
+
<Card.Card class={current === index && !completed[index] ? 'border-primary/40' : ''}>
|
|
34
|
+
<Card.CardContent class="flex items-start gap-4 p-4">
|
|
35
|
+
<span class={'flex size-10 shrink-0 items-center justify-center rounded-md ' + (completed[index] ? 'bg-success/10 text-success' : current === index ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground')}>
|
|
36
|
+
{#if completed[index]}<Check class="size-5" />{:else}<step.icon class="size-5" />{/if}
|
|
37
|
+
</span>
|
|
38
|
+
<div class="min-w-0 flex-1"><h2 class="text-sm font-semibold text-foreground">{step.title}</h2><p class="mt-1 text-sm text-muted-foreground">{step.description}</p>{#if current === index && !completed[index]}<Button class="mt-3" size="sm" onclick={() => complete(index)}>{i18n.t('account.completeSetup')}<ArrowRight class="size-3.5" /></Button>{/if}</div>
|
|
39
|
+
</Card.CardContent>
|
|
40
|
+
</Card.Card>
|
|
41
|
+
{/each}
|
|
59
42
|
</div>
|
|
60
|
-
|
|
61
|
-
<!-- Progress bar -->
|
|
62
|
-
<div class="space-y-1.5">
|
|
63
|
-
<div class="h-2 rounded-full bg-muted overflow-hidden">
|
|
64
|
-
<div class="h-full rounded-full bg-primary transition-all duration-500" style="width: {progressPercent}%"></div>
|
|
65
|
-
</div>
|
|
66
|
-
<p class="text-xs text-muted-foreground">{i18n.t('account.step', { current: completedCount, total: steps.length })}</p>
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
{#if allCompleted}
|
|
70
|
-
<Card.Card class="border-success/30 bg-success/5">
|
|
71
|
-
<Card.CardContent class="flex items-center gap-4 p-6">
|
|
72
|
-
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-success/10 text-success">
|
|
73
|
-
<Sparkles class="h-6 w-6" />
|
|
74
|
-
</div>
|
|
75
|
-
<div>
|
|
76
|
-
<h3 class="font-semibold text-success">{i18n.t('account.completeSetup')}</h3>
|
|
77
|
-
<p class="text-sm text-success/70">{i18n.t('account.getStartedDescription')}</p>
|
|
78
|
-
</div>
|
|
79
|
-
</Card.CardContent>
|
|
80
|
-
</Card.Card>
|
|
81
|
-
{:else}
|
|
82
|
-
<!-- Step cards -->
|
|
83
|
-
<div class="space-y-3">
|
|
84
|
-
{#each steps as step, i (i)}
|
|
85
|
-
<Card.Card class="border-border/60 transition-all {currentStep === i ? 'border-primary/40 shadow-sm' : ''} {step.completed ? 'opacity-60' : ''}">
|
|
86
|
-
<Card.CardContent class="p-4">
|
|
87
|
-
<div class="flex items-start gap-4">
|
|
88
|
-
<button
|
|
89
|
-
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-colors
|
|
90
|
-
{step.completed ? 'bg-success/10 text-success' : currentStep === i ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}"
|
|
91
|
-
onclick={() => goToStep(i)}
|
|
92
|
-
>
|
|
93
|
-
{#if step.completed}
|
|
94
|
-
<CheckCircle2 class="h-5 w-5" />
|
|
95
|
-
{:else}
|
|
96
|
-
<step.icon class="h-5 w-5" />
|
|
97
|
-
{/if}
|
|
98
|
-
</button>
|
|
99
|
-
<div class="flex-1 min-w-0">
|
|
100
|
-
<div class="flex items-center gap-2">
|
|
101
|
-
<h4 class="font-medium text-foreground">{step.title}</h4>
|
|
102
|
-
{#if currentStep === i && !step.completed}
|
|
103
|
-
<Badge variant="outline" class="text-[10px]">{i18n.t('account.step', { current: i + 1, total: steps.length })}</Badge>
|
|
104
|
-
{/if}
|
|
105
|
-
</div>
|
|
106
|
-
<p class="text-sm text-muted-foreground mt-0.5">{step.description}</p>
|
|
107
|
-
|
|
108
|
-
{#if currentStep === i && !step.completed}
|
|
109
|
-
<div class="mt-3 flex gap-2">
|
|
110
|
-
<Button size="sm" onclick={() => markComplete(i)}>
|
|
111
|
-
{i18n.t('account.completeSetup')} <ArrowRight class="h-3.5 w-3.5 ml-1" />
|
|
112
|
-
</Button>
|
|
113
|
-
<Button size="sm" variant="ghost" onclick={skipStep}>
|
|
114
|
-
{i18n.t('account.skipSetup')}
|
|
115
|
-
</Button>
|
|
116
|
-
</div>
|
|
117
|
-
{/if}
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
</Card.CardContent>
|
|
121
|
-
</Card.Card>
|
|
122
|
-
{/each}
|
|
123
|
-
</div>
|
|
124
|
-
{/if}
|
|
125
|
-
</div>
|
|
43
|
+
</ContentPageShell>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
declare const GetStartedPage: Component<Record<string, never>, {}, "">;
|
|
1
|
+
declare const GetStartedPage: import("svelte").Component<Record<string, never>, {}, "">;
|
|
3
2
|
type GetStartedPage = ReturnType<typeof GetStartedPage>;
|
|
4
3
|
export default GetStartedPage;
|
|
@@ -1,137 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import
|
|
3
|
+
import { Download } from '@lucide/svelte';
|
|
4
4
|
import { Button } from '../ui/button/index.js';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
5
|
+
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
6
|
+
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
7
|
+
import FilterToolbar from '../content/FilterToolbar.svelte';
|
|
8
|
+
import SecurityEventTable from '../content/SecurityEventTable.svelte';
|
|
9
|
+
import type { SecurityEvent } from '../content/SecurityEventTable.svelte';
|
|
10
|
+
import { referenceDemoData } from '../../reference-data.js';
|
|
8
11
|
const i18n = useTranslation();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
event: string;
|
|
13
|
-
eventLabel: string;
|
|
14
|
-
ip: string;
|
|
15
|
-
device: string;
|
|
16
|
-
location: string;
|
|
17
|
-
time: string;
|
|
18
|
-
severity: 'info' | 'warning' | 'critical';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let searchQuery = $state('');
|
|
22
|
-
let filterSeverity = $state<'all' | 'info' | 'warning' | 'critical'>('all');
|
|
23
|
-
|
|
24
|
-
const logs: LogEntry[] = [
|
|
25
|
-
{ id: '1', event: 'loginSuccess', eventLabel: i18n.t('securityLog.loginSuccess'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '2 minutes ago', severity: 'info' },
|
|
26
|
-
{ id: '2', event: 'apiKeyCreated', eventLabel: i18n.t('securityLog.apiKeyCreated'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '1 hour ago', severity: 'info' },
|
|
27
|
-
{ id: '3', event: 'loginFailed', eventLabel: i18n.t('securityLog.loginFailed'), ip: '203.0.113.42', device: 'Firefox / Windows', location: 'Unknown', time: '3 hours ago', severity: 'warning' },
|
|
28
|
-
{ id: '4', event: 'twoFactorEnabled', eventLabel: i18n.t('securityLog.twoFactorEnabled'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '1 day ago', severity: 'info' },
|
|
29
|
-
{ id: '5', event: 'passwordChanged', eventLabel: i18n.t('securityLog.passwordChanged'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '2 days ago', severity: 'info' },
|
|
30
|
-
{ id: '6', event: 'loginFailed', eventLabel: i18n.t('securityLog.loginFailed'), ip: '198.51.100.77', device: 'Safari / iOS', location: 'Beijing, CN', time: '3 days ago', severity: 'critical' },
|
|
31
|
-
{ id: '7', event: 'sessionRevoked', eventLabel: i18n.t('securityLog.sessionRevoked'), ip: '192.168.1.105', device: 'Safari / iOS', location: 'Shanghai, CN', time: '5 days ago', severity: 'info' },
|
|
32
|
-
{ id: '8', event: 'apiKeyRevoked', eventLabel: i18n.t('securityLog.apiKeyRevoked'), ip: '192.168.1.102', device: 'Chrome / macOS', location: 'Shanghai, CN', time: '1 week ago', severity: 'warning' },
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const filteredLogs = $derived(
|
|
36
|
-
logs.filter(l => {
|
|
37
|
-
if (filterSeverity !== 'all' && l.severity !== filterSeverity) return false;
|
|
38
|
-
if (searchQuery) {
|
|
39
|
-
const q = searchQuery.toLowerCase();
|
|
40
|
-
return l.eventLabel.toLowerCase().includes(q) || l.ip.includes(q) || l.device.toLowerCase().includes(q);
|
|
41
|
-
}
|
|
42
|
-
return true;
|
|
43
|
-
})
|
|
44
|
-
);
|
|
45
|
-
|
|
12
|
+
let query = $state('');
|
|
13
|
+
const events: SecurityEvent[] = referenceDemoData.securityEvents;
|
|
14
|
+
const filtered = $derived(query ? events.filter((event) => `${event.event} ${event.actor} ${event.location}`.toLowerCase().includes(query.toLowerCase())) : events);
|
|
46
15
|
</script>
|
|
47
16
|
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<Button variant="outline" size="sm">
|
|
55
|
-
<Download class="h-3.5 w-3.5 mr-1" />{i18n.t('common.export')}
|
|
56
|
-
</Button>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<!-- Filters -->
|
|
60
|
-
<div class="flex flex-wrap gap-3">
|
|
61
|
-
<div class="relative flex-1 min-w-[200px]">
|
|
62
|
-
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
63
|
-
<Input placeholder={i18n.t('common.search')} bind:value={searchQuery} class="pl-9" />
|
|
64
|
-
</div>
|
|
65
|
-
<div class="flex rounded-lg border border-input overflow-hidden">
|
|
66
|
-
{#each ['all', 'info', 'warning', 'critical'] as sev (sev)}
|
|
67
|
-
<button
|
|
68
|
-
class="px-3 py-1.5 text-xs font-medium transition-colors capitalize
|
|
69
|
-
{filterSeverity === sev ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
|
|
70
|
-
onclick={() => filterSeverity = sev as typeof filterSeverity}
|
|
71
|
-
>{sev === 'all' ? i18n.t('common.filter') : sev}</button>
|
|
72
|
-
{/each}
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
<!-- Log table -->
|
|
77
|
-
<Card.Card class="border-border/60">
|
|
78
|
-
<Card.CardContent class="p-0">
|
|
79
|
-
<div class="hidden overflow-x-auto md:block">
|
|
80
|
-
<table class="w-full">
|
|
81
|
-
<thead>
|
|
82
|
-
<tr class="border-b bg-muted/30">
|
|
83
|
-
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('securityLog.event')}</th>
|
|
84
|
-
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('securityLog.ip')}</th>
|
|
85
|
-
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('securityLog.device')}</th>
|
|
86
|
-
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('securityLog.location')}</th>
|
|
87
|
-
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('securityLog.time')}</th>
|
|
88
|
-
</tr>
|
|
89
|
-
</thead>
|
|
90
|
-
<tbody class="divide-y">
|
|
91
|
-
{#each filteredLogs as log (log.id)}
|
|
92
|
-
<tr class="hover:bg-accent/30 transition-colors">
|
|
93
|
-
<td class="px-4 py-3">
|
|
94
|
-
<div class="flex items-center gap-2">
|
|
95
|
-
<span class="inline-flex h-2 w-2 rounded-full {log.severity === 'critical' ? 'bg-red-500' : log.severity === 'warning' ? 'bg-amber-500' : 'bg-blue-500'}"></span>
|
|
96
|
-
<span class="text-sm text-foreground">{log.eventLabel}</span>
|
|
97
|
-
</div>
|
|
98
|
-
</td>
|
|
99
|
-
<td class="px-4 py-3 text-sm text-muted-foreground font-mono">{log.ip}</td>
|
|
100
|
-
<td class="px-4 py-3 text-sm text-muted-foreground">{log.device}</td>
|
|
101
|
-
<td class="px-4 py-3 text-sm text-muted-foreground">{log.location}</td>
|
|
102
|
-
<td class="px-4 py-3 text-sm text-muted-foreground whitespace-nowrap">{log.time}</td>
|
|
103
|
-
</tr>
|
|
104
|
-
{/each}
|
|
105
|
-
</tbody>
|
|
106
|
-
</table>
|
|
107
|
-
</div>
|
|
108
|
-
<div class="divide-y md:hidden">
|
|
109
|
-
{#each filteredLogs as log (log.id)}
|
|
110
|
-
<div class="space-y-3 p-4">
|
|
111
|
-
<div class="flex items-start justify-between gap-3">
|
|
112
|
-
<div class="flex min-w-0 items-center gap-2">
|
|
113
|
-
<span class="inline-flex h-2 w-2 shrink-0 rounded-full {log.severity === 'critical' ? 'bg-red-500' : log.severity === 'warning' ? 'bg-amber-500' : 'bg-blue-500'}"></span>
|
|
114
|
-
<span class="truncate text-sm font-medium text-foreground">{log.eventLabel}</span>
|
|
115
|
-
</div>
|
|
116
|
-
<span class="shrink-0 text-xs text-muted-foreground">{log.time}</span>
|
|
117
|
-
</div>
|
|
118
|
-
<div class="grid gap-2 text-xs text-muted-foreground">
|
|
119
|
-
<div class="flex items-center justify-between gap-3">
|
|
120
|
-
<span>{i18n.t('securityLog.ip')}</span>
|
|
121
|
-
<span class="font-mono text-foreground">{log.ip}</span>
|
|
122
|
-
</div>
|
|
123
|
-
<div class="flex items-center justify-between gap-3">
|
|
124
|
-
<span>{i18n.t('securityLog.device')}</span>
|
|
125
|
-
<span class="truncate text-foreground">{log.device}</span>
|
|
126
|
-
</div>
|
|
127
|
-
<div class="flex items-center justify-between gap-3">
|
|
128
|
-
<span>{i18n.t('securityLog.location')}</span>
|
|
129
|
-
<span class="truncate text-foreground">{log.location}</span>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
{/each}
|
|
134
|
-
</div>
|
|
135
|
-
</Card.CardContent>
|
|
136
|
-
</Card.Card>
|
|
137
|
-
</div>
|
|
17
|
+
<ContentPageShell pageId="account-security-log" width="wide">
|
|
18
|
+
<ContentPageHeader title={i18n.t('account.securityLog')} description={i18n.t('account.securityLogDescription')} actions={undefined} />
|
|
19
|
+
<div class="flex justify-end"><Button variant="outline" size="sm"><Download class="size-3.5" />{i18n.t('common.export')}</Button></div>
|
|
20
|
+
<FilterToolbar bind:query placeholder={i18n.t('common.search')} />
|
|
21
|
+
<SecurityEventTable events={filtered} />
|
|
22
|
+
</ContentPageShell>
|
|
@@ -1,286 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import * as Card from '../ui/card/index.js';
|
|
4
3
|
import { Button } from '../ui/button/index.js';
|
|
5
4
|
import { Input } from '../ui/input/index.js';
|
|
6
5
|
import { Label } from '../ui/label/index.js';
|
|
7
6
|
import { Switch } from '../ui/switch/index.js';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
7
|
+
import * as Card from '../ui/card/index.js';
|
|
8
|
+
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
9
|
+
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
10
|
+
import IntegrationCard from '../content/IntegrationCard.svelte';
|
|
11
|
+
import type { IntegrationSummary } from '../content/IntegrationCard.svelte';
|
|
12
|
+
import SecurityEventTable from '../content/SecurityEventTable.svelte';
|
|
13
|
+
import type { SecurityEvent } from '../content/SecurityEventTable.svelte';
|
|
11
14
|
const i18n = useTranslation();
|
|
12
|
-
|
|
13
|
-
const trustedDevices = [
|
|
14
|
-
{ name: 'MacBook Pro 16"', detail: 'macOS · Shanghai, CN', lastSeen: '2026-08-04 09:12', current: true },
|
|
15
|
-
{ name: 'iPhone 17', detail: 'iOS · Shanghai, CN', lastSeen: '2026-08-03 22:41', current: false },
|
|
16
|
-
{ name: 'Windows Workstation', detail: 'Windows · Beijing, CN', lastSeen: '2026-07-28 18:05', current: false },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const paymentHistory = [
|
|
20
|
-
{ date: '2026-08-01', type: 'Enterprise · Monthly', amount: '$299.00' },
|
|
21
|
-
{ date: '2026-07-01', type: 'Enterprise · Monthly', amount: '$299.00' },
|
|
22
|
-
{ date: '2026-06-01', type: 'Enterprise · Monthly', amount: '$299.00' },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const calendarAccounts = [
|
|
26
|
-
{ name: 'Work Calendar', account: 'alex@acme.com', active: true },
|
|
27
|
-
{ name: 'Personal Calendar', account: 'alex@example.com', active: false },
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const connections = [
|
|
31
|
-
{ name: 'Source Control', account: 'alexchen', linked: true },
|
|
32
|
-
{ name: 'ChatOps', account: '@alex.chen', linked: true },
|
|
33
|
-
{ name: 'Cloud Drive', account: 'alex@acme.com', linked: false },
|
|
34
|
-
];
|
|
35
|
-
|
|
36
15
|
let ssoEnabled = $state(false);
|
|
37
|
-
let ssoProvider = $state('');
|
|
38
|
-
let domainRestriction = $state('');
|
|
39
|
-
let passwordPolicy = $state('strong');
|
|
40
|
-
let sessionTimeout = $state('30');
|
|
41
|
-
let ipWhitelist = $state('');
|
|
42
16
|
let auditLogging = $state(true);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
async function handleSave() {
|
|
47
|
-
saving = true;
|
|
48
|
-
await new Promise(r => setTimeout(r, 800));
|
|
49
|
-
saving = false;
|
|
50
|
-
}
|
|
17
|
+
const identity: IntegrationSummary[] = [{ id: 'sso', name: 'Enterprise identity provider', description: i18n.t('account.ssoDescription'), account: 'SAML / OIDC', connected: false }];
|
|
18
|
+
const events: SecurityEvent[] = [{ id: '1', event: 'SSO policy updated', actor: 'Alex Chen', location: 'Shanghai, CN', createdAt: 'Today, 09:12', severity: 'info' }, { id: '2', event: 'Audit log retention changed', actor: 'Alex Chen', location: 'Shanghai, CN', createdAt: 'Yesterday, 16:40', severity: 'warning' }];
|
|
51
19
|
</script>
|
|
52
20
|
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<Badge variant="secondary"><Building2 class="h-3 w-3 mr-1" />Enterprise</Badge>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<!-- SSO -->
|
|
63
|
-
<Card.Card class="border-border/60">
|
|
64
|
-
<Card.CardHeader>
|
|
65
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
66
|
-
<Shield class="h-4 w-4 text-muted-foreground" />
|
|
67
|
-
{i18n.t('account.singleSignOn')}
|
|
68
|
-
</Card.CardTitle>
|
|
69
|
-
<Card.CardDescription>{i18n.t('account.ssoDescription')}</Card.CardDescription>
|
|
70
|
-
</Card.CardHeader>
|
|
71
|
-
<Card.CardContent class="space-y-4">
|
|
72
|
-
<div class="flex items-center justify-between gap-4">
|
|
73
|
-
<div class="space-y-1">
|
|
74
|
-
<Label for="sso-enabled">{i18n.t('account.enableSso')}</Label>
|
|
75
|
-
<p class="text-xs text-muted-foreground">{i18n.t('account.enableSsoDesc')}</p>
|
|
76
|
-
</div>
|
|
77
|
-
<Switch id="sso-enabled" bind:checked={ssoEnabled} />
|
|
78
|
-
</div>
|
|
79
|
-
{#if ssoEnabled}
|
|
80
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
81
|
-
<div class="space-y-2">
|
|
82
|
-
<Label for="sso-provider">{i18n.t('account.ssoProvider')}</Label>
|
|
83
|
-
<Input id="sso-provider" bind:value={ssoProvider} placeholder="Okta, Azure AD, etc." />
|
|
84
|
-
</div>
|
|
85
|
-
<div class="space-y-2">
|
|
86
|
-
<Label for="domain-restriction">{i18n.t('account.domainRestriction')}</Label>
|
|
87
|
-
<Input id="domain-restriction" bind:value={domainRestriction} placeholder="@company.com" />
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
{/if}
|
|
91
|
-
</Card.CardContent>
|
|
92
|
-
</Card.Card>
|
|
93
|
-
|
|
94
|
-
<!-- Security Policy -->
|
|
95
|
-
<Card.Card class="border-border/60">
|
|
96
|
-
<Card.CardHeader>
|
|
97
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
98
|
-
<Lock class="h-4 w-4 text-muted-foreground" />
|
|
99
|
-
{i18n.t('account.securityPolicy')}
|
|
100
|
-
</Card.CardTitle>
|
|
101
|
-
</Card.CardHeader>
|
|
102
|
-
<Card.CardContent class="space-y-4">
|
|
103
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
104
|
-
<div class="space-y-2">
|
|
105
|
-
<Label for="password-policy">{i18n.t('account.passwordPolicy')}</Label>
|
|
106
|
-
<select id="password-policy" bind:value={passwordPolicy} 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">
|
|
107
|
-
<option value="basic">{i18n.t('account.policyBasic')}</option>
|
|
108
|
-
<option value="strong">{i18n.t('account.policyStrong')}</option>
|
|
109
|
-
<option value="very-strong">{i18n.t('account.policyVeryStrong')}</option>
|
|
110
|
-
</select>
|
|
111
|
-
</div>
|
|
112
|
-
<div class="space-y-2">
|
|
113
|
-
<Label for="session-timeout">{i18n.t('account.sessionTimeout')}</Label>
|
|
114
|
-
<Input id="session-timeout" bind:value={sessionTimeout} type="number" />
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div class="space-y-2">
|
|
119
|
-
<Label for="ip-whitelist">{i18n.t('account.ipWhitelist')}</Label>
|
|
120
|
-
<Input id="ip-whitelist" bind:value={ipWhitelist} placeholder="10.0.0.0/8, 192.168.1.0/24" />
|
|
121
|
-
<p class="text-xs text-muted-foreground">{i18n.t('account.ipWhitelistHint')}</p>
|
|
122
|
-
</div>
|
|
123
|
-
</Card.CardContent>
|
|
124
|
-
</Card.Card>
|
|
125
|
-
|
|
126
|
-
<!-- Data & Compliance -->
|
|
127
|
-
<Card.Card class="border-border/60">
|
|
128
|
-
<Card.CardHeader>
|
|
129
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
130
|
-
<Globe class="h-4 w-4 text-muted-foreground" />
|
|
131
|
-
{i18n.t('account.dataCompliance')}
|
|
132
|
-
</Card.CardTitle>
|
|
133
|
-
</Card.CardHeader>
|
|
134
|
-
<Card.CardContent class="space-y-4">
|
|
135
|
-
<div class="flex items-center justify-between gap-4">
|
|
136
|
-
<div class="space-y-1">
|
|
137
|
-
<Label for="audit-logging">{i18n.t('account.auditLogging')}</Label>
|
|
138
|
-
<p class="text-xs text-muted-foreground">{i18n.t('account.auditLoggingDesc')}</p>
|
|
139
|
-
</div>
|
|
140
|
-
<Switch id="audit-logging" bind:checked={auditLogging} />
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<div class="space-y-2">
|
|
144
|
-
<Label for="data-retention">{i18n.t('account.dataRetention')}</Label>
|
|
145
|
-
<Input id="data-retention" bind:value={dataRetention} type="number" />
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<div class="flex justify-end">
|
|
149
|
-
<Button onclick={handleSave} disabled={saving}>
|
|
150
|
-
{#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
|
|
151
|
-
{i18n.t('common.save')}
|
|
152
|
-
</Button>
|
|
153
|
-
</div>
|
|
154
|
-
</Card.CardContent>
|
|
155
|
-
</Card.Card>
|
|
156
|
-
|
|
157
|
-
<div class="grid gap-4 lg:grid-cols-2">
|
|
158
|
-
<!-- Trusted Devices -->
|
|
159
|
-
<Card.Card class="border-border/60">
|
|
160
|
-
<Card.CardHeader>
|
|
161
|
-
<Card.CardTitle class="text-base">{i18n.t('account.trustedDevices')}</Card.CardTitle>
|
|
162
|
-
</Card.CardHeader>
|
|
163
|
-
<Card.CardContent class="p-0">
|
|
164
|
-
<div class="divide-y">
|
|
165
|
-
{#each trustedDevices as device (device.name)}
|
|
166
|
-
<div class="flex items-center justify-between gap-3 px-6 py-3">
|
|
167
|
-
<div class="min-w-0">
|
|
168
|
-
<p class="truncate text-sm font-medium text-foreground">{device.name}</p>
|
|
169
|
-
<p class="truncate text-xs text-muted-foreground">{device.detail} · {device.lastSeen}</p>
|
|
170
|
-
</div>
|
|
171
|
-
{#if device.current}
|
|
172
|
-
<Badge variant="secondary" class="shrink-0 text-[10px]">{i18n.t('security.currentSession')}</Badge>
|
|
173
|
-
{/if}
|
|
174
|
-
</div>
|
|
175
|
-
{/each}
|
|
176
|
-
</div>
|
|
177
|
-
</Card.CardContent>
|
|
178
|
-
</Card.Card>
|
|
179
|
-
|
|
180
|
-
<!-- Current Plan + Payment History -->
|
|
181
|
-
<div class="space-y-4">
|
|
182
|
-
<Card.Card class="border-primary/30 bg-primary/5">
|
|
183
|
-
<Card.CardHeader class="pb-3">
|
|
184
|
-
<div class="flex items-center justify-between">
|
|
185
|
-
<Card.CardTitle class="text-base">{i18n.t('account.currentPlan')}</Card.CardTitle>
|
|
186
|
-
<Badge variant="secondary">Enterprise</Badge>
|
|
187
|
-
</div>
|
|
188
|
-
</Card.CardHeader>
|
|
189
|
-
<Card.CardContent>
|
|
190
|
-
<div class="flex items-baseline gap-1">
|
|
191
|
-
<span class="text-2xl font-bold text-foreground">$299</span>
|
|
192
|
-
<span class="text-sm text-muted-foreground">{i18n.t('account.perMonth')}</span>
|
|
193
|
-
</div>
|
|
194
|
-
<p class="mt-1 text-xs text-muted-foreground">Unlimited seats · SSO · Audit retention 365d</p>
|
|
195
|
-
</Card.CardContent>
|
|
196
|
-
</Card.Card>
|
|
197
|
-
|
|
198
|
-
<Card.Card class="border-border/60">
|
|
199
|
-
<Card.CardHeader class="pb-3">
|
|
200
|
-
<Card.CardTitle class="text-base">{i18n.t('account.paymentHistory')}</Card.CardTitle>
|
|
201
|
-
</Card.CardHeader>
|
|
202
|
-
<Card.CardContent class="p-0">
|
|
203
|
-
<div class="divide-y">
|
|
204
|
-
<div class="grid grid-cols-3 px-6 py-2 text-xs font-medium text-muted-foreground">
|
|
205
|
-
<span>{i18n.t('account.date')}</span>
|
|
206
|
-
<span>{i18n.t('account.type')}</span>
|
|
207
|
-
<span class="text-right">{i18n.t('account.amount')}</span>
|
|
208
|
-
</div>
|
|
209
|
-
{#each paymentHistory as row (row.date)}
|
|
210
|
-
<div class="grid grid-cols-3 px-6 py-2.5 text-sm">
|
|
211
|
-
<span class="text-muted-foreground">{row.date}</span>
|
|
212
|
-
<span class="text-foreground">{row.type}</span>
|
|
213
|
-
<span class="text-right font-medium text-foreground">{row.amount}</span>
|
|
214
|
-
</div>
|
|
215
|
-
{/each}
|
|
216
|
-
</div>
|
|
217
|
-
</Card.CardContent>
|
|
218
|
-
</Card.Card>
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
<div class="grid gap-4 lg:grid-cols-3">
|
|
223
|
-
<!-- Calendar Accounts -->
|
|
224
|
-
<Card.Card class="border-border/60">
|
|
225
|
-
<Card.CardHeader>
|
|
226
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
227
|
-
<Calendar class="h-4 w-4 text-muted-foreground" />{i18n.t('account.calendarAccounts')}
|
|
228
|
-
</Card.CardTitle>
|
|
229
|
-
</Card.CardHeader>
|
|
230
|
-
<Card.CardContent class="space-y-3">
|
|
231
|
-
{#each calendarAccounts as cal (cal.account)}
|
|
232
|
-
<div class="flex items-center justify-between gap-3">
|
|
233
|
-
<div class="min-w-0">
|
|
234
|
-
<p class="truncate text-sm font-medium text-foreground">{cal.name}</p>
|
|
235
|
-
<p class="truncate text-xs text-muted-foreground">{cal.account}</p>
|
|
236
|
-
</div>
|
|
237
|
-
<Badge variant={cal.active ? 'secondary' : 'outline'} class="shrink-0 text-[10px]">
|
|
238
|
-
{cal.active ? i18n.t('account.active') : i18n.t('account.inactive')}
|
|
239
|
-
</Badge>
|
|
240
|
-
</div>
|
|
241
|
-
{/each}
|
|
242
|
-
</Card.CardContent>
|
|
243
|
-
</Card.Card>
|
|
244
|
-
|
|
245
|
-
<!-- Connections -->
|
|
246
|
-
<Card.Card class="border-border/60">
|
|
247
|
-
<Card.CardHeader>
|
|
248
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
249
|
-
<Link2 class="h-4 w-4 text-muted-foreground" />{i18n.t('account.connections')}
|
|
250
|
-
</Card.CardTitle>
|
|
251
|
-
</Card.CardHeader>
|
|
252
|
-
<Card.CardContent class="space-y-3">
|
|
253
|
-
{#each connections as conn (conn.name)}
|
|
254
|
-
<div class="flex items-center justify-between gap-3">
|
|
255
|
-
<div class="min-w-0">
|
|
256
|
-
<p class="truncate text-sm font-medium text-foreground">{conn.name}</p>
|
|
257
|
-
<p class="truncate text-xs text-muted-foreground">{conn.account}</p>
|
|
258
|
-
</div>
|
|
259
|
-
<Badge variant={conn.linked ? 'secondary' : 'outline'} class="shrink-0 text-[10px]">
|
|
260
|
-
{conn.linked ? i18n.t('integrations.statusConnected') : i18n.t('integrations.statusDisconnected')}
|
|
261
|
-
</Badge>
|
|
262
|
-
</div>
|
|
263
|
-
{/each}
|
|
264
|
-
</Card.CardContent>
|
|
265
|
-
</Card.Card>
|
|
266
|
-
|
|
267
|
-
<!-- Data import -->
|
|
268
|
-
<Card.Card class="border-border/60">
|
|
269
|
-
<Card.CardHeader>
|
|
270
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
271
|
-
<Database class="h-4 w-4 text-muted-foreground" />{i18n.t('account.dataImport')}
|
|
272
|
-
</Card.CardTitle>
|
|
273
|
-
<Card.CardDescription>{i18n.t('account.dataImportDescription')}</Card.CardDescription>
|
|
274
|
-
</Card.CardHeader>
|
|
275
|
-
<Card.CardContent>
|
|
276
|
-
<div class="flex items-center justify-between gap-3 rounded-lg border border-border/60 p-3">
|
|
277
|
-
<div class="min-w-0">
|
|
278
|
-
<p class="truncate text-sm font-medium text-foreground">Google Analytics</p>
|
|
279
|
-
<p class="truncate text-xs text-muted-foreground">analytics.google.com</p>
|
|
280
|
-
</div>
|
|
281
|
-
<Button size="sm" variant="outline">{i18n.t('network.connect')}</Button>
|
|
282
|
-
</div>
|
|
283
|
-
</Card.CardContent>
|
|
284
|
-
</Card.Card>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
21
|
+
<ContentPageShell pageId="account-settings-enterprise" width="wide">
|
|
22
|
+
<ContentPageHeader title={i18n.t('account.settingsEnterprise')} description={i18n.t('account.settingsEnterpriseDescription')} />
|
|
23
|
+
<div class="grid gap-4 lg:grid-cols-2"><Card.Card><Card.CardContent class="space-y-4 p-5"><div><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.securityPolicy')}</h2><p class="mt-1 text-sm text-muted-foreground">Set organization-wide controls for authentication and sessions.</p></div><div class="space-y-2"><Label for="session-timeout">{i18n.t('account.sessionTimeout')}</Label><Input id="session-timeout" value="30" /></div><div class="space-y-2"><Label for="retention">{i18n.t('account.dataRetention')}</Label><Input id="retention" value="365" /></div><div class="flex items-center justify-between border-t border-border pt-4"><span class="text-sm text-foreground">{i18n.t('account.auditLogging')}</span><Switch bind:checked={auditLogging} /></div><Button size="sm">{i18n.t('common.save')}</Button></Card.CardContent></Card.Card><Card.Card><Card.CardContent class="space-y-4 p-5"><div><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.singleSignOn')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.ssoDescription')}</p></div><div class="flex items-center justify-between border-t border-border pt-4"><span class="text-sm text-foreground">{i18n.t('account.enableSso')}</span><Switch bind:checked={ssoEnabled} /></div><Input value="https://idp.example.com/metadata" aria-label={i18n.t('account.ssoProvider')} /><Button variant="outline" size="sm">{i18n.t('common.test')}</Button></Card.CardContent></Card.Card></div>
|
|
24
|
+
<section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('account.connections')}</h2>{#each identity as integration (integration.id)}<IntegrationCard {integration} />{/each}</section>
|
|
25
|
+
<section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('account.securityLog')}</h2><SecurityEventTable events={events} /></section>
|
|
26
|
+
</ContentPageShell>
|