Files
BackupX/web/src/components/common/DatabasePicker.tsx
Awuqing 09698cc767 feat: add community enhancements — password reset, audit logs, multi-source backup
Three community-requested features:

1. CLI password reset: `backupx reset-password --username admin --password xxx`
   Docker users can run via `docker exec`. No full app init needed.

2. Audit logging: async fire-and-forget audit trail for all key operations
   (login, CRUD on tasks/targets/records, settings changes).
   New UI page at /audit with category filter and pagination.

3. Multi-source path backup: file backup tasks now support multiple source
   directories packed into a single tar archive. Backward compatible with
   existing single sourcePath field.
2026-03-30 23:04:37 +08:00

121 lines
3.4 KiB
TypeScript

import { Button, Checkbox, Input, Message, Space, Spin, Typography } from '@arco-design/web-react'
import { useState } from 'react'
import { discoverDatabases } from '../../services/database'
interface DatabasePickerProps {
dbType: 'mysql' | 'postgresql'
dbHost: string
dbPort: number
dbUser: string
dbPassword: string
value: string
onChange: (value: string) => void
}
export function DatabasePicker({ dbType, dbHost, dbPort, dbUser, dbPassword, value, onChange }: DatabasePickerProps) {
const [databases, setDatabases] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [discovered, setDiscovered] = useState(false)
const [error, setError] = useState('')
const selectedDbs = value
.split(',')
.map((s) => s.trim())
.filter(Boolean)
const canDiscover = dbHost.trim() && dbPort > 0 && dbUser.trim() && dbPassword.trim()
async function handleDiscover() {
setLoading(true)
setError('')
try {
const result = await discoverDatabases({
type: dbType,
host: dbHost.trim(),
port: dbPort,
user: dbUser.trim(),
password: dbPassword.trim(),
})
setDatabases(result)
setDiscovered(true)
if (result.length === 0) {
setError('未发现用户数据库')
}
} catch (discoverError: any) {
const msg = discoverError?.response?.data?.message ?? discoverError?.message ?? '发现数据库失败'
setError(msg)
Message.error(msg)
} finally {
setLoading(false)
}
}
function handleToggle(db: string, checked: boolean) {
let next: string[]
if (checked) {
next = [...selectedDbs, db]
} else {
next = selectedDbs.filter((d) => d !== db)
}
onChange(next.join(','))
}
function handleSelectAll() {
onChange(databases.join(','))
}
function handleDeselectAll() {
onChange('')
}
return (
<Space direction="vertical" size="medium" style={{ width: '100%' }}>
<Space style={{ width: '100%' }}>
<Input
style={{ flex: 1 }}
value={value}
placeholder="数据库名称(多个以逗号分隔)"
onChange={onChange}
/>
<Button
type="outline"
size="small"
loading={loading}
disabled={!canDiscover}
onClick={handleDiscover}
>
</Button>
</Space>
{error && <Typography.Text type="error">{error}</Typography.Text>}
{loading && <Spin size={16} />}
{discovered && databases.length > 0 && (
<div style={{ border: '1px solid var(--color-border-2)', borderRadius: 4, padding: '8px 12px', maxHeight: 200, overflow: 'auto' }}>
<Space size="mini" style={{ marginBottom: 8 }}>
<Button type="text" size="mini" onClick={handleSelectAll}>
</Button>
<Button type="text" size="mini" onClick={handleDeselectAll}>
</Button>
</Space>
<Space direction="vertical" size={4}>
{databases.map((db) => (
<Checkbox
key={db}
checked={selectedDbs.includes(db)}
onChange={(checked) => handleToggle(db, checked)}
>
{db}
</Checkbox>
))}
</Space>
</div>
)}
</Space>
)
}