This commit is contained in:
rustmailer
2026-05-10 03:43:50 +08:00
parent 2f8ba5ad40
commit 41322c8357
24 changed files with 217 additions and 116 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ export function MailFilterPopover() {
size="sm"
variant="outline"
className={cn(
'h-6 rounded-none px-3 gap-1.5 transition-colors',
'h-6 rounded-none px-3 gap-1.5 transition-colors border-l-0',
activeCount > 0 && 'bg-primary/10 text-primary hover:bg-primary/20'
)}
>
+1 -1
View File
@@ -271,7 +271,7 @@ export function MailboxPopover() {
variant="outline"
disabled={disabled}
className={cn(
'h-6 rounded-none px-3 gap-1.5 transition-colors',
'h-6 rounded-none px-3 gap-1.5 transition-colors border-l-0',
selectedMailboxIds.length > 0 && 'bg-primary/10 text-primary border-primary/20'
)}
>
@@ -98,7 +98,7 @@ export function TagFilterPopover() {
size="sm"
variant="outline"
className={cn(
'h-6 gap-1.5 px-3 rounded-none',
'h-6 gap-1.5 px-3 rounded-none border-l-0',
selectedTags.length > 0 &&
'bg-primary/10 border-primary text-primary'
)}
+6 -3
View File
@@ -25,6 +25,7 @@ import { useCurrentUser } from '@/hooks/use-current-user'
import { PermissionsDialog } from './permissions-dialog'
import Logo from '@/assets/logo.svg'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import LongText from '@/components/long-text'
export function AccountAccessList() {
@@ -70,13 +71,15 @@ export function AccountAccessList() {
if (!email) return null
return (
<Card key={accountId} className="group hover:bg-accent/40 transition-all h-fit">
<Card key={accountId} className="group hover:bg-accent/40 transition-all h-fit min-w-0">
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary text-sm font-bold shrink-0">
{email.charAt(0).toUpperCase()}
</div>
<div className="flex flex-col min-w-0">
<CardTitle className="text-sm font-semibold truncate">{email}</CardTitle>
<div className="flex flex-col min-w-0 flex-1">
<CardTitle className="text-sm font-semibold">
<LongText className="max-w-[248px]">{email}</LongText>
</CardTitle>
<CardDescription className="text-[10px] font-mono">
{t('settings.profile.account.id', { id: accountId })}
</CardDescription>
+126 -110
View File
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
@@ -35,9 +36,9 @@ import { useQuery } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
const formatMB = (bytes?: number) => {
if (!bytes) return "—";
return `${(bytes / 1024 / 1024).toFixed(0)} MB`;
};
if (!bytes) return "—"
return `${(bytes / 1024 / 1024).toFixed(0)} MB`
}
function BooleanBadge({ value }: { value: boolean }) {
const { t } = useTranslation()
@@ -58,13 +59,13 @@ function SettingRow({
description?: string
}) {
return (
<div className="py-1.5 border-b border-border/40 last:border-0">
<div className="grid grid-cols-[1fr_auto] items-center gap-3">
<div className="text-sm font-medium text-foreground/80 leading-tight font-mono">{label}</div>
<div className="text-sm text-right break-all leading-tight font-medium">{value}</div>
<div className="py-2.5 border-b border-border/40 last:border-0">
<div className="grid grid-cols-[1fr_auto] items-center gap-4">
<div className="text-xs font-medium text-muted-foreground font-mono">{label}</div>
<div className="text-sm text-right font-medium">{value}</div>
</div>
{description && (
<div className="mt-0.5 text-[11px] leading-tight text-muted-foreground">{description}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{description}</div>
)}
</div>
)
@@ -82,31 +83,36 @@ function SettingsCard({
children: React.ReactNode
}) {
return (
<Card className="h-full shadow-sm">
<CardHeader className="flex flex-row items-center gap-2 py-3 bg-muted/20">
<Icon className="h-4 w-4 text-primary/70" />
<Card className="h-full">
<CardHeader className="flex flex-row items-center gap-3 py-4 px-5 border-b bg-muted/20">
<div className="p-1.5 rounded-md bg-primary/10">
<Icon className="h-3.5 w-3.5 text-primary" />
</div>
<div className="space-y-0.5">
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
{description && <CardDescription className="text-[11px] leading-none">{description}</CardDescription>}
{description && (
<CardDescription className="text-[11px]">{description}</CardDescription>
)}
</div>
</CardHeader>
<CardContent className="pt-2 pb-1 px-4">{children}</CardContent>
<CardContent className="px-5 py-1">{children}</CardContent>
</Card>
)
}
function PageSkeleton() {
return (
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i}>
<CardHeader className="py-3">
<CardHeader className="py-4 px-5 border-b">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-56" />
</CardHeader>
<CardContent className="space-y-2">
<CardContent className="px-5 py-3 space-y-3">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-5/6" />
<Skeleton className="h-3 w-4/6" />
</CardContent>
</Card>
))}
@@ -121,109 +127,119 @@ export default function ServerConfigurationsPage() {
queryFn: get_system_configurations,
})
if (isLoading) return <ScrollArea className="h-full"><PageSkeleton /></ScrollArea>
if (isError || !data) {
return <div className="p-8 text-center text-sm text-destructive">{t("systemConfig.fields.loadError")}</div>
if (isError || (!isLoading && !data)) {
return (
<div className="p-8 text-center text-sm text-destructive">
{t("systemConfig.fields.loadError")}
</div>
)
}
return (
<div className="w-full max-w-6xl mx-auto">
<div className="w-full">
<ScrollArea className="h-full w-full">
<div className="px-6 pt-6 pb-2">
<div className="flex items-start gap-4 p-5 rounded-xl border bg-gradient-to-br from-secondary/50 to-background shadow-inner">
<div className="p-2 bg-primary/10 rounded-lg">
<InfoIcon className="h-5 w-5 text-primary" />
</div>
<div className="max-w-5xl mx-auto px-6 py-8 space-y-6">
<div className="flex items-start gap-3 p-4 rounded-xl border bg-muted/30">
<InfoIcon className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
<div>
<h4 className="text-base font-bold text-foreground">
{t("systemConfig.pageTitle")}
</h4>
<p className="text-xs text-muted-foreground mt-1 max-w-2xl leading-relaxed">
<h4 className="text-sm font-semibold">{t("systemConfig.pageTitle")}</h4>
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
{t("systemConfig.pageDescription")}
</p>
</div>
</div>
</div>
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
<SettingsCard
icon={Server}
title={t("systemConfig.sections.network.title")}
description={t("systemConfig.sections.network.desc")}
>
<SettingRow label="BICHON_BIND_IP" value={data.bichon_bind_ip ?? "0.0.0.0"} />
<SettingRow label="BICHON_HTTP_PORT" value={data.bichon_http_port} />
<SettingRow label="BICHON_BASE_URL" value={data.bichon_base_url} />
<SettingRow label="BICHON_PUBLIC_URL" value={data.bichon_public_url} />
<SettingRow
label="BICHON_ENABLE_REST_HTTPS"
value={<BooleanBadge value={data.bichon_enable_rest_https} />}
/>
</SettingsCard>
<SettingsCard
icon={Mail}
title={t("systemConfig.sections.smtp.title", "SMTP Server")}
description={t("systemConfig.sections.smtp.desc", "Incoming mail reception settings")}
>
<SettingRow label="BICHON_ENABLE_SMTP" value={<BooleanBadge value={data.bichon_enable_smtp} />} />
<SettingRow label="BICHON_SMTP_PORT" value={data.bichon_smtp_port} />
<SettingRow label="BICHON_SMTP_ENCRYPTION" value={data.bichon_smtp_encryption} />
<SettingRow label="BICHON_SMTP_AUTH_REQUIRED" value={<BooleanBadge value={data.bichon_smtp_auth_required} />} />
</SettingsCard>
<SettingsCard
icon={Zap}
title={t("systemConfig.sections.performance.title")}
description={t("systemConfig.sections.performance.desc")}
>
<SettingRow label="BICHON_SYNC_CONCURRENCY" value={data.bichon_sync_concurrency ?? t("systemConfig.status.auto")} />
<SettingRow
label="BICHON_HTTP_COMPRESSION_ENABLED"
value={<BooleanBadge value={data.bichon_http_compression_enabled} />}
/>
</SettingsCard>
<SettingsCard
icon={Database}
title={t("systemConfig.sections.storage.title")}
description={t("systemConfig.sections.storage.desc")}
>
<SettingRow label="BICHON_ROOT_DIR" value={<span className="font-mono">{data.bichon_root_dir}</span>} />
<SettingRow label="BICHON_DATA_DIR" value={data.bichon_data_dir ? <span className="font-mono">{data.bichon_data_dir}</span> : "—"} />
<SettingRow label="BICHON_INDEX_DIR" value={data.bichon_index_dir ? <span className="font-mono">{data.bichon_index_dir}</span> : "—"} />
<SettingRow label="BICHON_METADATA_CACHE_SIZE" value={formatMB(data.bichon_metadata_cache_size)} />
<SettingRow label="BICHON_ENVELOPE_CACHE_SIZE" value={formatMB(data.bichon_envelope_cache_size)} />
</SettingsCard>
<SettingsCard
icon={ShieldCheck}
title={t("systemConfig.sections.security.title")}
description={t("systemConfig.sections.security.desc")}
>
<SettingRow
label="BICHON_ENCRYPT_PASSWORD_SET"
value={
data.bichon_encrypt_password_set ? (
<Badge variant="secondary" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20">
{t("systemConfig.status.configured")}
</Badge>
) : (
<Badge variant="destructive">{t("systemConfig.status.missing")}</Badge>
)
}
/>
<SettingRow
label="BICHON_WEBUI_TOKEN_EXPIRATION_HOURS"
value={`${data.bichon_webui_token_expiration_hours}h`}
/>
</SettingsCard>
<SettingsCard
icon={Activity}
title={t("systemConfig.sections.logging.title")}
description={t("systemConfig.sections.logging.desc")}
>
<SettingRow label="BICHON_LOG_LEVEL" value={<Badge variant="outline" className="uppercase">{data.bichon_log_level}</Badge>} />
<SettingRow label="BICHON_ANSI_LOGS" value={<BooleanBadge value={data.bichon_ansi_logs} />} />
<SettingRow label="BICHON_JSON_LOGS" value={<BooleanBadge value={data.bichon_json_logs} />} />
<SettingRow label="BICHON_LOG_TO_FILE" value={<BooleanBadge value={data.bichon_log_to_file} />} />
<SettingRow label="BICHON_MAX_SERVER_LOG_FILES" value={data.bichon_max_server_log_files} />
</SettingsCard>
{isLoading ? (
<PageSkeleton />
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<SettingsCard
icon={Server}
title={t("systemConfig.sections.network.title")}
description={t("systemConfig.sections.network.desc")}
>
<SettingRow label="BICHON_BIND_IP" value={data!.bichon_bind_ip ?? "0.0.0.0"} />
<SettingRow label="BICHON_HTTP_PORT" value={data!.bichon_http_port} />
<SettingRow label="BICHON_BASE_URL" value={data!.bichon_base_url} />
<SettingRow label="BICHON_PUBLIC_URL" value={data!.bichon_public_url} />
<SettingRow
label="BICHON_ENABLE_REST_HTTPS"
value={<BooleanBadge value={data!.bichon_enable_rest_https} />}
/>
</SettingsCard>
<SettingsCard
icon={Mail}
title={t("systemConfig.sections.smtp.title")}
description={t("systemConfig.sections.smtp.desc")}
>
<SettingRow label="BICHON_ENABLE_SMTP" value={<BooleanBadge value={data!.bichon_enable_smtp} />} />
<SettingRow label="BICHON_SMTP_PORT" value={data!.bichon_smtp_port} />
<SettingRow label="BICHON_SMTP_ENCRYPTION" value={data!.bichon_smtp_encryption} />
<SettingRow label="BICHON_SMTP_AUTH_REQUIRED" value={<BooleanBadge value={data!.bichon_smtp_auth_required} />} />
</SettingsCard>
<SettingsCard
icon={Zap}
title={t("systemConfig.sections.performance.title")}
description={t("systemConfig.sections.performance.desc")}
>
<SettingRow label="BICHON_SYNC_CONCURRENCY" value={data!.bichon_sync_concurrency ?? t("systemConfig.status.auto")} />
<SettingRow
label="BICHON_HTTP_COMPRESSION_ENABLED"
value={<BooleanBadge value={data!.bichon_http_compression_enabled} />}
/>
</SettingsCard>
<SettingsCard
icon={Database}
title={t("systemConfig.sections.storage.title")}
description={t("systemConfig.sections.storage.desc")}
>
<SettingRow label="BICHON_ROOT_DIR" value={<span className="font-mono">{data!.bichon_root_dir}</span>} />
<SettingRow label="BICHON_DATA_DIR" value={data!.bichon_data_dir ? <span className="font-mono">{data!.bichon_data_dir}</span> : "—"} />
<SettingRow label="BICHON_INDEX_DIR" value={data!.bichon_index_dir ? <span className="font-mono">{data!.bichon_index_dir}</span> : "—"} />
<SettingRow label="BICHON_METADATA_CACHE_SIZE" value={formatMB(data!.bichon_metadata_cache_size)} />
<SettingRow label="BICHON_ENVELOPE_CACHE_SIZE" value={formatMB(data!.bichon_envelope_cache_size)} />
</SettingsCard>
<SettingsCard
icon={ShieldCheck}
title={t("systemConfig.sections.security.title")}
description={t("systemConfig.sections.security.desc")}
>
<SettingRow
label="BICHON_ENCRYPT_PASSWORD_SET"
value={
data!.bichon_encrypt_password_set ? (
<Badge variant="secondary" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20">
{t("systemConfig.status.configured")}
</Badge>
) : (
<Badge variant="destructive">{t("systemConfig.status.missing")}</Badge>
)
}
/>
<SettingRow
label="BICHON_WEBUI_TOKEN_EXPIRATION_HOURS"
value={`${data!.bichon_webui_token_expiration_hours}h`}
/>
</SettingsCard>
<SettingsCard
icon={Activity}
title={t("systemConfig.sections.logging.title")}
description={t("systemConfig.sections.logging.desc")}
>
<SettingRow label="BICHON_LOG_LEVEL" value={<Badge variant="outline" className="uppercase">{data!.bichon_log_level}</Badge>} />
<SettingRow label="BICHON_ANSI_LOGS" value={<BooleanBadge value={data!.bichon_ansi_logs} />} />
<SettingRow label="BICHON_JSON_LOGS" value={<BooleanBadge value={data!.bichon_json_logs} />} />
<SettingRow label="BICHON_LOG_TO_FILE" value={<BooleanBadge value={data!.bichon_log_to_file} />} />
<SettingRow label="BICHON_MAX_SERVER_LOG_FILES" value={data!.bichon_max_server_log_files} />
</SettingsCard>
</div>
)}
</div>
</ScrollArea>
+10
View File
@@ -24,6 +24,7 @@ import { KeyRound, Palette, SettingsIcon, ShieldCheck, UserCog, Waypoints } from
import { FixedHeader } from '@/components/layout/fixed-header'
import { useCurrentUser } from '@/hooks/use-current-user'
import { useTranslation } from 'react-i18next'
import { Separator } from '@/components/ui/separator'
export default function Settings() {
const { t } = useTranslation()
@@ -68,6 +69,15 @@ export default function Settings() {
<>
<FixedHeader />
<Main>
<div className='space-y-0.5'>
<h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
{t('settings.header.title')}
</h1>
<p className='text-muted-foreground'>
{t('settings.header.description')}
</p>
</div>
<Separator className='my-4 lg:my-6' />
<div className='flex flex-1 flex-col space-y-2 md:space-y-2 overflow-hidden lg:flex-row lg:space-x-12 lg:space-y-0'>
<aside className='top-0 lg:sticky lg:w-1/5'>
<SidebarNav items={sidebarNavItems} />