@octo-cyber/log-analysis 0.5.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/controllers/log-analysis.controller.d.ts +21 -0
- package/dist/controllers/log-analysis.controller.d.ts.map +1 -0
- package/dist/controllers/log-analysis.controller.js +75 -0
- package/dist/controllers/log-analysis.controller.js.map +1 -0
- package/dist/controllers/retention-rule.controller.d.ts +17 -0
- package/dist/controllers/retention-rule.controller.d.ts.map +1 -0
- package/dist/controllers/retention-rule.controller.js +97 -0
- package/dist/controllers/retention-rule.controller.js.map +1 -0
- package/dist/entities/index.d.ts +6 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +13 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/operation-log.entity.d.ts +45 -0
- package/dist/entities/operation-log.entity.d.ts.map +1 -0
- package/dist/entities/operation-log.entity.js +125 -0
- package/dist/entities/operation-log.entity.js.map +1 -0
- package/dist/entities/retention-rule.entity.d.ts +17 -0
- package/dist/entities/retention-rule.entity.d.ts.map +1 -0
- package/dist/entities/retention-rule.entity.js +66 -0
- package/dist/entities/retention-rule.entity.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/log-analysis.module.d.ts +18 -0
- package/dist/log-analysis.module.d.ts.map +1 -0
- package/dist/log-analysis.module.js +41 -0
- package/dist/log-analysis.module.js.map +1 -0
- package/dist/schemas/log-analysis.schema.d.ts +116 -0
- package/dist/schemas/log-analysis.schema.d.ts.map +1 -0
- package/dist/schemas/log-analysis.schema.js +42 -0
- package/dist/schemas/log-analysis.schema.js.map +1 -0
- package/dist/services/log-query.service.d.ts +37 -0
- package/dist/services/log-query.service.d.ts.map +1 -0
- package/dist/services/log-query.service.js +103 -0
- package/dist/services/log-query.service.js.map +1 -0
- package/dist/services/log-writer.service.d.ts +20 -0
- package/dist/services/log-writer.service.d.ts.map +1 -0
- package/dist/services/log-writer.service.js +66 -0
- package/dist/services/log-writer.service.js.map +1 -0
- package/dist/services/retention-rule.service.d.ts +19 -0
- package/dist/services/retention-rule.service.d.ts.map +1 -0
- package/dist/services/retention-rule.service.js +110 -0
- package/dist/services/retention-rule.service.js.map +1 -0
- package/package.json +88 -0
- package/web/components/LogsTable.tsx +69 -0
- package/web/components/RetentionRulesTab.tsx +187 -0
- package/web/components/StatsOverview.tsx +91 -0
- package/web/index.ts +27 -0
- package/web/manifest.ts +17 -0
- package/web/messages/en-US.json +70 -0
- package/web/messages/zh-CN.json +70 -0
- package/web/pages/LogAnalysisPage.tsx +202 -0
- package/web/services/log-analysis-service.ts +68 -0
- package/web/types/log-analysis.ts +83 -0
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octo-cyber/log-analysis",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Octo log-analysis module — operation log recording, querying, and statistical analysis",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./web": {
|
|
14
|
+
"types": "./web/index.ts",
|
|
15
|
+
"import": "./web/index.ts",
|
|
16
|
+
"default": "./web/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"./web/pages/*": {
|
|
19
|
+
"types": "./web/pages/*.tsx",
|
|
20
|
+
"import": "./web/pages/*.tsx",
|
|
21
|
+
"default": "./web/pages/*.tsx"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@octo-cyber/core": "^0.5.4",
|
|
26
|
+
"@octo-cyber/auth": "^0.5.6"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "^19.0.0",
|
|
30
|
+
"react-dom": "^19.0.0",
|
|
31
|
+
"next": "^16.0.0",
|
|
32
|
+
"next-intl": "^4.0.0",
|
|
33
|
+
"lucide-react": ">=0.400.0",
|
|
34
|
+
"@tanstack/react-table": "^8.0.0",
|
|
35
|
+
"zod": "^3.0.0",
|
|
36
|
+
"sonner": "^2.0.0",
|
|
37
|
+
"@octo-cyber/ui": "^0.5.3"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"@octo-cyber/ui": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"react": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"react-dom": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"next": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"next-intl": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"lucide-react": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@tanstack/react-table": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"zod": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"sonner": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/react": "^19",
|
|
70
|
+
"@types/react-dom": "^19",
|
|
71
|
+
"typescript": "^5.8.0",
|
|
72
|
+
"vitest": "^3.0.0"
|
|
73
|
+
},
|
|
74
|
+
"files": [
|
|
75
|
+
"dist",
|
|
76
|
+
"web"
|
|
77
|
+
],
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"registry": "https://registry.npmjs.org/"
|
|
80
|
+
},
|
|
81
|
+
"license": "MIT",
|
|
82
|
+
"scripts": {
|
|
83
|
+
"build": "tsc -p tsconfig.build.json",
|
|
84
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
85
|
+
"test": "vitest run",
|
|
86
|
+
"typecheck": "tsc --noEmit"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from 'next-intl'
|
|
4
|
+
import { Badge } from '@octo-cyber/ui/components/ui/badge'
|
|
5
|
+
import {
|
|
6
|
+
Table,
|
|
7
|
+
TableBody,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
} from '@octo-cyber/ui/components/ui/table'
|
|
13
|
+
import type { OperationLog, LogLevel } from '../types/log-analysis'
|
|
14
|
+
|
|
15
|
+
interface LogsTableProps {
|
|
16
|
+
logs: OperationLog[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const levelVariant: Record<LogLevel, 'default' | 'secondary' | 'destructive'> = {
|
|
20
|
+
INFO: 'default',
|
|
21
|
+
WARN: 'secondary',
|
|
22
|
+
ERROR: 'destructive',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function LogsTable({ logs }: LogsTableProps) {
|
|
26
|
+
const t = useTranslations('logAnalysis')
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="rounded-md border dark:border-zinc-700">
|
|
30
|
+
<Table>
|
|
31
|
+
<TableHeader>
|
|
32
|
+
<TableRow>
|
|
33
|
+
<TableHead>{t('columns.createdAt')}</TableHead>
|
|
34
|
+
<TableHead>{t('columns.level')}</TableHead>
|
|
35
|
+
<TableHead>{t('columns.module')}</TableHead>
|
|
36
|
+
<TableHead>{t('columns.action')}</TableHead>
|
|
37
|
+
<TableHead>{t('columns.username')}</TableHead>
|
|
38
|
+
<TableHead>{t('columns.description')}</TableHead>
|
|
39
|
+
<TableHead>{t('columns.ip')}</TableHead>
|
|
40
|
+
</TableRow>
|
|
41
|
+
</TableHeader>
|
|
42
|
+
<TableBody>
|
|
43
|
+
{logs.length === 0 && (
|
|
44
|
+
<TableRow>
|
|
45
|
+
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
|
46
|
+
{t('empty')}
|
|
47
|
+
</TableCell>
|
|
48
|
+
</TableRow>
|
|
49
|
+
)}
|
|
50
|
+
{logs.map(log => (
|
|
51
|
+
<TableRow key={log.id}>
|
|
52
|
+
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
|
53
|
+
{new Date(log.createdAt).toLocaleString()}
|
|
54
|
+
</TableCell>
|
|
55
|
+
<TableCell>
|
|
56
|
+
<Badge variant={levelVariant[log.level]}>{log.level}</Badge>
|
|
57
|
+
</TableCell>
|
|
58
|
+
<TableCell className="font-mono text-xs">{log.module}</TableCell>
|
|
59
|
+
<TableCell className="text-xs">{log.action}</TableCell>
|
|
60
|
+
<TableCell className="text-sm">{log.username ?? '-'}</TableCell>
|
|
61
|
+
<TableCell className="max-w-xs truncate text-sm">{log.description}</TableCell>
|
|
62
|
+
<TableCell className="text-xs text-muted-foreground">{log.ip ?? '-'}</TableCell>
|
|
63
|
+
</TableRow>
|
|
64
|
+
))}
|
|
65
|
+
</TableBody>
|
|
66
|
+
</Table>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useTranslations } from 'next-intl'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { Button } from '@octo-cyber/ui/components/ui/button'
|
|
7
|
+
import { Badge } from '@octo-cyber/ui/components/ui/badge'
|
|
8
|
+
import {
|
|
9
|
+
Table,
|
|
10
|
+
TableBody,
|
|
11
|
+
TableCell,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableHeader,
|
|
14
|
+
TableRow,
|
|
15
|
+
} from '@octo-cyber/ui/components/ui/table'
|
|
16
|
+
import {
|
|
17
|
+
Dialog,
|
|
18
|
+
DialogContent,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
DialogTrigger,
|
|
22
|
+
} from '@octo-cyber/ui/components/ui/dialog'
|
|
23
|
+
import { Input } from '@octo-cyber/ui/components/ui/input'
|
|
24
|
+
import { Label } from '@octo-cyber/ui/components/ui/label'
|
|
25
|
+
import { logAnalysisService } from '../services/log-analysis-service'
|
|
26
|
+
import type { RetentionRule, CreateRetentionRuleDto } from '../types/log-analysis'
|
|
27
|
+
|
|
28
|
+
interface RetentionRulesTabProps {
|
|
29
|
+
rules: RetentionRule[]
|
|
30
|
+
onRefresh: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const emptyForm: CreateRetentionRuleDto = {
|
|
34
|
+
name: '',
|
|
35
|
+
module: null,
|
|
36
|
+
action: null,
|
|
37
|
+
retentionDays: 90,
|
|
38
|
+
enabled: true,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function RetentionRulesTab({ rules, onRefresh }: RetentionRulesTabProps) {
|
|
42
|
+
const t = useTranslations('logAnalysis')
|
|
43
|
+
const [open, setOpen] = useState(false)
|
|
44
|
+
const [form, setForm] = useState<CreateRetentionRuleDto>(emptyForm)
|
|
45
|
+
const [saving, setSaving] = useState(false)
|
|
46
|
+
const [applying, setApplying] = useState(false)
|
|
47
|
+
|
|
48
|
+
const handleCreate = async () => {
|
|
49
|
+
if (!form.name.trim()) return
|
|
50
|
+
setSaving(true)
|
|
51
|
+
try {
|
|
52
|
+
await logAnalysisService.createRetentionRule(form)
|
|
53
|
+
toast.success(t('retention.created'))
|
|
54
|
+
setOpen(false)
|
|
55
|
+
setForm(emptyForm)
|
|
56
|
+
onRefresh()
|
|
57
|
+
} catch {
|
|
58
|
+
toast.error(t('retention.createError'))
|
|
59
|
+
} finally {
|
|
60
|
+
setSaving(false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleDelete = async (id: number) => {
|
|
65
|
+
try {
|
|
66
|
+
await logAnalysisService.deleteRetentionRule(id)
|
|
67
|
+
toast.success(t('retention.deleted'))
|
|
68
|
+
onRefresh()
|
|
69
|
+
} catch {
|
|
70
|
+
toast.error(t('retention.deleteError'))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handleApply = async () => {
|
|
75
|
+
setApplying(true)
|
|
76
|
+
try {
|
|
77
|
+
const result = await logAnalysisService.applyRetention()
|
|
78
|
+
toast.success(t('retention.applied', { count: result.deleted }))
|
|
79
|
+
onRefresh()
|
|
80
|
+
} catch {
|
|
81
|
+
toast.error(t('retention.applyError'))
|
|
82
|
+
} finally {
|
|
83
|
+
setApplying(false)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="space-y-4">
|
|
89
|
+
<div className="flex justify-end gap-2">
|
|
90
|
+
<Button variant="outline" onClick={handleApply} disabled={applying}>
|
|
91
|
+
{applying ? t('retention.applying') : t('retention.applyNow')}
|
|
92
|
+
</Button>
|
|
93
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
94
|
+
<DialogTrigger asChild>
|
|
95
|
+
<Button>{t('retention.addRule')}</Button>
|
|
96
|
+
</DialogTrigger>
|
|
97
|
+
<DialogContent>
|
|
98
|
+
<DialogHeader>
|
|
99
|
+
<DialogTitle>{t('retention.addRule')}</DialogTitle>
|
|
100
|
+
</DialogHeader>
|
|
101
|
+
<div className="space-y-4 py-2">
|
|
102
|
+
<div className="space-y-1">
|
|
103
|
+
<Label>{t('retention.name')}</Label>
|
|
104
|
+
<Input
|
|
105
|
+
value={form.name}
|
|
106
|
+
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
|
107
|
+
placeholder={t('retention.namePlaceholder')}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="space-y-1">
|
|
111
|
+
<Label>{t('retention.module')}</Label>
|
|
112
|
+
<Input
|
|
113
|
+
value={form.module ?? ''}
|
|
114
|
+
onChange={e => setForm(f => ({ ...f, module: e.target.value || null }))}
|
|
115
|
+
placeholder={t('retention.modulePlaceholder')}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="space-y-1">
|
|
119
|
+
<Label>{t('retention.retentionDays')}</Label>
|
|
120
|
+
<Input
|
|
121
|
+
type="number"
|
|
122
|
+
min={0}
|
|
123
|
+
value={form.retentionDays}
|
|
124
|
+
onChange={e => setForm(f => ({ ...f, retentionDays: Number(e.target.value) }))}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<Button className="w-full" onClick={handleCreate} disabled={saving}>
|
|
128
|
+
{saving ? t('saving') : t('save')}
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</DialogContent>
|
|
132
|
+
</Dialog>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="rounded-md border dark:border-zinc-700">
|
|
136
|
+
<Table>
|
|
137
|
+
<TableHeader>
|
|
138
|
+
<TableRow>
|
|
139
|
+
<TableHead>{t('retention.name')}</TableHead>
|
|
140
|
+
<TableHead>{t('retention.module')}</TableHead>
|
|
141
|
+
<TableHead>{t('retention.action')}</TableHead>
|
|
142
|
+
<TableHead>{t('retention.retentionDays')}</TableHead>
|
|
143
|
+
<TableHead>{t('retention.status')}</TableHead>
|
|
144
|
+
<TableHead className="w-24">{t('actions')}</TableHead>
|
|
145
|
+
</TableRow>
|
|
146
|
+
</TableHeader>
|
|
147
|
+
<TableBody>
|
|
148
|
+
{rules.length === 0 && (
|
|
149
|
+
<TableRow>
|
|
150
|
+
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
|
151
|
+
{t('empty')}
|
|
152
|
+
</TableCell>
|
|
153
|
+
</TableRow>
|
|
154
|
+
)}
|
|
155
|
+
{rules.map(rule => (
|
|
156
|
+
<TableRow key={rule.id}>
|
|
157
|
+
<TableCell className="font-medium">{rule.name}</TableCell>
|
|
158
|
+
<TableCell className="font-mono text-xs">{rule.module ?? t('retention.all')}</TableCell>
|
|
159
|
+
<TableCell className="text-sm">{rule.action ?? t('retention.all')}</TableCell>
|
|
160
|
+
<TableCell>
|
|
161
|
+
{rule.retentionDays === 0
|
|
162
|
+
? t('retention.forever')
|
|
163
|
+
: t('retention.days', { count: rule.retentionDays })}
|
|
164
|
+
</TableCell>
|
|
165
|
+
<TableCell>
|
|
166
|
+
<Badge variant={rule.enabled ? 'default' : 'secondary'}>
|
|
167
|
+
{rule.enabled ? t('enabled') : t('disabled')}
|
|
168
|
+
</Badge>
|
|
169
|
+
</TableCell>
|
|
170
|
+
<TableCell>
|
|
171
|
+
<Button
|
|
172
|
+
variant="ghost"
|
|
173
|
+
size="sm"
|
|
174
|
+
className="text-destructive hover:text-destructive"
|
|
175
|
+
onClick={() => handleDelete(rule.id)}
|
|
176
|
+
>
|
|
177
|
+
{t('delete')}
|
|
178
|
+
</Button>
|
|
179
|
+
</TableCell>
|
|
180
|
+
</TableRow>
|
|
181
|
+
))}
|
|
182
|
+
</TableBody>
|
|
183
|
+
</Table>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from 'next-intl'
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@octo-cyber/ui/components/ui/card'
|
|
5
|
+
import { Badge } from '@octo-cyber/ui/components/ui/badge'
|
|
6
|
+
import type { LogStats } from '../types/log-analysis'
|
|
7
|
+
|
|
8
|
+
interface StatsOverviewProps {
|
|
9
|
+
stats: LogStats
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StatsOverview({ stats }: StatsOverviewProps) {
|
|
13
|
+
const t = useTranslations('logAnalysis')
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
17
|
+
{/* 总计 */}
|
|
18
|
+
<Card className="dark:bg-zinc-900 dark:border-zinc-700">
|
|
19
|
+
<CardHeader className="pb-2">
|
|
20
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
21
|
+
{t('stats.total')}
|
|
22
|
+
</CardTitle>
|
|
23
|
+
</CardHeader>
|
|
24
|
+
<CardContent>
|
|
25
|
+
<div className="text-2xl font-bold">{stats.total.toLocaleString()}</div>
|
|
26
|
+
</CardContent>
|
|
27
|
+
</Card>
|
|
28
|
+
|
|
29
|
+
{/* 按级别 */}
|
|
30
|
+
<Card className="dark:bg-zinc-900 dark:border-zinc-700">
|
|
31
|
+
<CardHeader className="pb-2">
|
|
32
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
33
|
+
{t('stats.byLevel')}
|
|
34
|
+
</CardTitle>
|
|
35
|
+
</CardHeader>
|
|
36
|
+
<CardContent className="flex flex-wrap gap-2">
|
|
37
|
+
{stats.byLevel.map(item => (
|
|
38
|
+
<div key={item.level} className="flex items-center gap-1">
|
|
39
|
+
<Badge
|
|
40
|
+
variant={
|
|
41
|
+
item.level === 'ERROR'
|
|
42
|
+
? 'destructive'
|
|
43
|
+
: item.level === 'WARN'
|
|
44
|
+
? 'secondary'
|
|
45
|
+
: 'default'
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
{item.level}
|
|
49
|
+
</Badge>
|
|
50
|
+
<span className="text-sm font-semibold">{item.count}</span>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</CardContent>
|
|
54
|
+
</Card>
|
|
55
|
+
|
|
56
|
+
{/* Top 5 模块 */}
|
|
57
|
+
<Card className="dark:bg-zinc-900 dark:border-zinc-700">
|
|
58
|
+
<CardHeader className="pb-2">
|
|
59
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
60
|
+
{t('stats.topModules')}
|
|
61
|
+
</CardTitle>
|
|
62
|
+
</CardHeader>
|
|
63
|
+
<CardContent className="space-y-1">
|
|
64
|
+
{stats.byModule.slice(0, 5).map(item => (
|
|
65
|
+
<div key={item.module} className="flex justify-between text-sm">
|
|
66
|
+
<span className="font-mono text-xs">{item.module}</span>
|
|
67
|
+
<span className="font-semibold">{item.count}</span>
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</CardContent>
|
|
71
|
+
</Card>
|
|
72
|
+
|
|
73
|
+
{/* Top 5 动作 */}
|
|
74
|
+
<Card className="dark:bg-zinc-900 dark:border-zinc-700">
|
|
75
|
+
<CardHeader className="pb-2">
|
|
76
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
77
|
+
{t('stats.topActions')}
|
|
78
|
+
</CardTitle>
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent className="space-y-1">
|
|
81
|
+
{stats.byAction.slice(0, 5).map(item => (
|
|
82
|
+
<div key={item.action} className="flex justify-between text-sm">
|
|
83
|
+
<span>{item.action}</span>
|
|
84
|
+
<span className="font-semibold">{item.count}</span>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</CardContent>
|
|
88
|
+
</Card>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
package/web/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octo-cyber/log-analysis — Frontend entry point.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Manifest
|
|
6
|
+
export { logAnalysisFrontendManifest } from './manifest'
|
|
7
|
+
|
|
8
|
+
// i18n Messages
|
|
9
|
+
export { default as logAnalysisMessagesZhCN } from './messages/zh-CN.json'
|
|
10
|
+
export { default as logAnalysisMessagesEnUS } from './messages/en-US.json'
|
|
11
|
+
|
|
12
|
+
// Pages
|
|
13
|
+
export { default as LogAnalysisPage } from './pages/LogAnalysisPage'
|
|
14
|
+
|
|
15
|
+
// Services
|
|
16
|
+
export { logAnalysisService } from './services/log-analysis-service'
|
|
17
|
+
|
|
18
|
+
// Types
|
|
19
|
+
export type {
|
|
20
|
+
OperationLog,
|
|
21
|
+
RetentionRule,
|
|
22
|
+
LogStats,
|
|
23
|
+
LogLevel,
|
|
24
|
+
LogAction,
|
|
25
|
+
QueryLogsParams,
|
|
26
|
+
CreateRetentionRuleDto,
|
|
27
|
+
} from './types/log-analysis'
|
package/web/manifest.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IFrontendManifest } from '@octo-cyber/core'
|
|
2
|
+
|
|
3
|
+
export const logAnalysisFrontendManifest: IFrontendManifest = {
|
|
4
|
+
moduleId: 'log-analysis',
|
|
5
|
+
icon: 'ScrollText',
|
|
6
|
+
color: 'slate',
|
|
7
|
+
position: 'main',
|
|
8
|
+
titleKey: 'logAnalysis.title',
|
|
9
|
+
descriptionKey: 'logAnalysis.description',
|
|
10
|
+
pages: [
|
|
11
|
+
{
|
|
12
|
+
path: '/log-analysis',
|
|
13
|
+
titleKey: 'logAnalysis.pages.home',
|
|
14
|
+
component: '@octo-cyber/log-analysis/web/pages/LogAnalysisPage',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"logAnalysis": {
|
|
3
|
+
"title": "Operation Logs",
|
|
4
|
+
"description": "View system operation records, analyze user behavior, and manage log retention rules",
|
|
5
|
+
"tabs": {
|
|
6
|
+
"logs": "Logs",
|
|
7
|
+
"stats": "Statistics",
|
|
8
|
+
"retention": "Retention Rules"
|
|
9
|
+
},
|
|
10
|
+
"columns": {
|
|
11
|
+
"createdAt": "Time",
|
|
12
|
+
"level": "Level",
|
|
13
|
+
"module": "Module",
|
|
14
|
+
"action": "Action",
|
|
15
|
+
"username": "User",
|
|
16
|
+
"description": "Description",
|
|
17
|
+
"ip": "IP"
|
|
18
|
+
},
|
|
19
|
+
"filter": {
|
|
20
|
+
"keyword": "Search description...",
|
|
21
|
+
"module": "Module",
|
|
22
|
+
"level": "Level",
|
|
23
|
+
"action": "Action",
|
|
24
|
+
"all": "All"
|
|
25
|
+
},
|
|
26
|
+
"stats": {
|
|
27
|
+
"total": "Total Logs",
|
|
28
|
+
"byLevel": "By Level",
|
|
29
|
+
"topModules": "Top Modules",
|
|
30
|
+
"topActions": "Top Actions"
|
|
31
|
+
},
|
|
32
|
+
"retention": {
|
|
33
|
+
"addRule": "Add Rule",
|
|
34
|
+
"name": "Rule Name",
|
|
35
|
+
"namePlaceholder": "e.g. Clean login logs",
|
|
36
|
+
"module": "Match Module",
|
|
37
|
+
"modulePlaceholder": "Empty = all modules",
|
|
38
|
+
"action": "Match Action",
|
|
39
|
+
"retentionDays": "Retention Days",
|
|
40
|
+
"status": "Status",
|
|
41
|
+
"all": "All",
|
|
42
|
+
"forever": "Keep Forever",
|
|
43
|
+
"days": "{count} days",
|
|
44
|
+
"applyNow": "Apply Retention Now",
|
|
45
|
+
"applying": "Applying...",
|
|
46
|
+
"applied": "Cleaned {count} logs",
|
|
47
|
+
"created": "Rule created",
|
|
48
|
+
"deleted": "Rule deleted",
|
|
49
|
+
"createError": "Failed to create rule",
|
|
50
|
+
"deleteError": "Failed to delete rule",
|
|
51
|
+
"applyError": "Failed to apply retention"
|
|
52
|
+
},
|
|
53
|
+
"empty": "No data",
|
|
54
|
+
"loading": "Loading...",
|
|
55
|
+
"fetchError": "Failed to load data",
|
|
56
|
+
"search": "Search",
|
|
57
|
+
"save": "Save",
|
|
58
|
+
"saving": "Saving...",
|
|
59
|
+
"delete": "Delete",
|
|
60
|
+
"actions": "Actions",
|
|
61
|
+
"enabled": "Enabled",
|
|
62
|
+
"disabled": "Disabled",
|
|
63
|
+
"prev": "Previous",
|
|
64
|
+
"next": "Next",
|
|
65
|
+
"pageInfo": "Page {page} of {totalPages}",
|
|
66
|
+
"pages": {
|
|
67
|
+
"home": "Operation Logs"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"logAnalysis": {
|
|
3
|
+
"title": "操作日志",
|
|
4
|
+
"description": "查看系统操作记录,分析用户行为,管理日志保留规则",
|
|
5
|
+
"tabs": {
|
|
6
|
+
"logs": "日志列表",
|
|
7
|
+
"stats": "统计分析",
|
|
8
|
+
"retention": "保留规则"
|
|
9
|
+
},
|
|
10
|
+
"columns": {
|
|
11
|
+
"createdAt": "时间",
|
|
12
|
+
"level": "级别",
|
|
13
|
+
"module": "模块",
|
|
14
|
+
"action": "操作",
|
|
15
|
+
"username": "用户",
|
|
16
|
+
"description": "描述",
|
|
17
|
+
"ip": "IP"
|
|
18
|
+
},
|
|
19
|
+
"filter": {
|
|
20
|
+
"keyword": "搜索描述...",
|
|
21
|
+
"module": "模块",
|
|
22
|
+
"level": "级别",
|
|
23
|
+
"action": "操作",
|
|
24
|
+
"all": "全部"
|
|
25
|
+
},
|
|
26
|
+
"stats": {
|
|
27
|
+
"total": "总条数",
|
|
28
|
+
"byLevel": "按级别",
|
|
29
|
+
"topModules": "Top 模块",
|
|
30
|
+
"topActions": "Top 操作"
|
|
31
|
+
},
|
|
32
|
+
"retention": {
|
|
33
|
+
"addRule": "添加规则",
|
|
34
|
+
"name": "规则名称",
|
|
35
|
+
"namePlaceholder": "如:清理登录日志",
|
|
36
|
+
"module": "匹配模块",
|
|
37
|
+
"modulePlaceholder": "空表示全部模块",
|
|
38
|
+
"action": "匹配操作",
|
|
39
|
+
"retentionDays": "保留天数",
|
|
40
|
+
"status": "状态",
|
|
41
|
+
"all": "全部",
|
|
42
|
+
"forever": "永久保留",
|
|
43
|
+
"days": "{count} 天",
|
|
44
|
+
"applyNow": "立即执行清理",
|
|
45
|
+
"applying": "执行中...",
|
|
46
|
+
"applied": "已清理 {count} 条日志",
|
|
47
|
+
"created": "规则已创建",
|
|
48
|
+
"deleted": "规则已删除",
|
|
49
|
+
"createError": "创建失败",
|
|
50
|
+
"deleteError": "删除失败",
|
|
51
|
+
"applyError": "清理执行失败"
|
|
52
|
+
},
|
|
53
|
+
"empty": "暂无数据",
|
|
54
|
+
"loading": "加载中...",
|
|
55
|
+
"fetchError": "数据加载失败",
|
|
56
|
+
"search": "搜索",
|
|
57
|
+
"save": "保存",
|
|
58
|
+
"saving": "保存中...",
|
|
59
|
+
"delete": "删除",
|
|
60
|
+
"actions": "操作",
|
|
61
|
+
"enabled": "启用",
|
|
62
|
+
"disabled": "禁用",
|
|
63
|
+
"prev": "上一页",
|
|
64
|
+
"next": "下一页",
|
|
65
|
+
"pageInfo": "第 {page} / {totalPages} 页",
|
|
66
|
+
"pages": {
|
|
67
|
+
"home": "操作日志"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|