Skip to content

Commit cadf21f

Browse files
committed
wip
1 parent de8df94 commit cadf21f

File tree

6 files changed

+392
-1
lines changed

6 files changed

+392
-1
lines changed
+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use client';
2+
3+
import { ActionIcon } from '@lobehub/ui';
4+
import { useRequest } from 'ahooks';
5+
import { Card, Progress, Statistic, Typography, theme } from 'antd';
6+
import { createStyles } from 'antd-style';
7+
import { AlertCircle, HardDrive, RotateCw } from 'lucide-react';
8+
import { Center, Flexbox } from 'react-layout-kit';
9+
10+
import { formatSize } from '@/utils/format';
11+
12+
const { Text } = Typography;
13+
14+
const useStyles = createStyles(({ css, token }) => ({
15+
card: css`
16+
width: 100%;
17+
max-width: 1024px;
18+
border: 1px solid ${token.colorBorder};
19+
border-radius: ${token.borderRadiusLG}px;
20+
21+
background: ${token.colorBgContainer};
22+
box-shadow: 0 2px 8px ${token.boxShadow};
23+
24+
.ant-card-body {
25+
padding: 24px;
26+
}
27+
`,
28+
detailItem: css`
29+
.ant-typography {
30+
margin-block-end: 4px;
31+
32+
&:last-child {
33+
margin-block-end: 0;
34+
}
35+
}
36+
`,
37+
icon: css`
38+
color: ${token.colorPrimary};
39+
`,
40+
percentage: css`
41+
font-size: 24px;
42+
font-weight: 600;
43+
line-height: 1;
44+
color: ${token.colorTextBase};
45+
`,
46+
progressInfo: css`
47+
position: absolute;
48+
inset-block-start: 50%;
49+
inset-inline-start: 50%;
50+
transform: translate(-50%, -50%);
51+
52+
text-align: center;
53+
`,
54+
progressWrapper: css`
55+
position: relative;
56+
width: 180px;
57+
height: 180px;
58+
`,
59+
title: css`
60+
margin-block-end: 0;
61+
font-size: 16px;
62+
font-weight: 500;
63+
color: ${token.colorTextBase};
64+
`,
65+
usageText: css`
66+
font-size: 13px;
67+
color: ${token.colorTextSecondary};
68+
`,
69+
warning: css`
70+
font-size: 13px;
71+
color: ${token.colorWarning};
72+
`,
73+
}));
74+
75+
// 字节转换函数
76+
77+
const StorageEstimate = () => {
78+
const { styles } = useStyles();
79+
const { token } = theme.useToken();
80+
81+
const { data, loading, refresh } = useRequest(async () => {
82+
const estimate = await navigator.storage.estimate();
83+
return {
84+
quota: estimate.quota || 0,
85+
usage: estimate.usage || 0,
86+
};
87+
});
88+
89+
if (!data) return null;
90+
91+
const usedPercentage = Math.round((data.usage / data.quota) * 100);
92+
const freeSpace = data.quota - data.usage;
93+
const isLowStorage = usedPercentage > 90;
94+
95+
return (
96+
<Center>
97+
<Card
98+
className={styles.card}
99+
extra={<ActionIcon icon={RotateCw} loading={loading} onClick={refresh} title="Refresh" />}
100+
title={
101+
<Flexbox align="center" gap={8} horizontal>
102+
<HardDrive className={styles.icon} size={18} />
103+
<span className={styles.title}>Storage Usage</span>
104+
</Flexbox>
105+
}
106+
>
107+
<Flexbox align="center" gap={80} horizontal justify={'center'}>
108+
{/* 左侧环形进度区 */}
109+
<div className={styles.progressWrapper}>
110+
<Progress
111+
percent={usedPercentage < 1 ? 1 : usedPercentage}
112+
size={180}
113+
strokeColor={isLowStorage ? token.colorWarning : token.colorPrimary}
114+
strokeWidth={8}
115+
type="circle"
116+
/>
117+
</div>
118+
119+
{/* 右侧详细信息区 */}
120+
<Flexbox gap={24}>
121+
<Statistic title={'Used Storage'} value={formatSize(data.usage)} />
122+
<Statistic title={'Available Storage'} value={formatSize(freeSpace)} />
123+
<Statistic title={'Total Storage'} value={formatSize(data.quota)} />
124+
{/* 警告信息 */}
125+
{isLowStorage && (
126+
<Flexbox align="center" gap={8} horizontal>
127+
<AlertCircle className={styles.warning} size={16} />
128+
<Text className={styles.warning}>
129+
Storage space is running low ({'<'}10% available)
130+
</Text>
131+
</Flexbox>
132+
)}
133+
</Flexbox>
134+
</Flexbox>
135+
</Card>
136+
</Center>
137+
);
138+
};
139+
140+
export default StorageEstimate;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { and, eq, inArray } from 'drizzle-orm/expressions';
2+
import pMap from 'p-map';
3+
4+
import * as EXPORT_TABLES from '@/database/schemas';
5+
import { LobeChatDatabase } from '@/database/type';
6+
7+
interface BaseTableConfig {
8+
table: keyof typeof EXPORT_TABLES;
9+
type: 'base';
10+
userField?: string;
11+
}
12+
13+
interface RelationTableConfig {
14+
relations: {
15+
field: string;
16+
sourceField?: string;
17+
sourceTable: string;
18+
}[];
19+
table: keyof typeof EXPORT_TABLES;
20+
type: 'relation';
21+
}
22+
23+
// 配置拆分为基础表和关联表
24+
25+
export const DATA_EXPORT_CONFIG = {
26+
// 1. 基础表
27+
baseTables: [
28+
{ table: 'users', userField: 'id' },
29+
{ table: 'userSettings', userField: 'id' },
30+
{ table: 'userInstalledPlugins' },
31+
{ table: 'agents' },
32+
{ table: 'sessionGroups' },
33+
{ table: 'sessions' },
34+
{ table: 'topics' },
35+
{ table: 'threads' },
36+
{ table: 'messages' },
37+
{ table: 'files' },
38+
{ table: 'knowledgeBases' },
39+
{ table: 'agentsKnowledgeBases' },
40+
{ table: 'aiProviders' },
41+
{ table: 'aiModels' },
42+
{ table: 'asyncTasks' },
43+
{ table: 'chunks' },
44+
{ table: 'embeddings' },
45+
] as BaseTableConfig[],
46+
// 2. 关联表
47+
relationTables: [
48+
{
49+
relations: [
50+
{ sourceField: 'id', sourceTable: 'agents', field: 'agentId' },
51+
{ sourceField: 'sessionId', sourceTable: 'sessions' },
52+
],
53+
table: 'agentsToSessions',
54+
},
55+
// {
56+
// relations: [
57+
// { sourceField: 'agentId', sourceTable: 'agents' },
58+
// { sourceField: 'fileId', sourceTable: 'files' },
59+
// ],
60+
// table: 'agentsFiles',
61+
// },
62+
// {
63+
// relations: [{ field: 'sessionId', sourceTable: 'sessions' }],
64+
// table: 'filesToSessions',
65+
// },
66+
// {
67+
// relations: [{ field: 'id', sourceTable: 'chunks' }],
68+
// table: 'fileChunks',
69+
// },
70+
{
71+
relations: [{ field: 'id', sourceTable: 'messages' }],
72+
table: 'messagePlugins',
73+
},
74+
{
75+
relations: [{ field: 'id', sourceTable: 'messages' }],
76+
table: 'messageTTS',
77+
},
78+
{
79+
relations: [{ field: 'id', sourceTable: 'messages' }],
80+
table: 'messageTranslates',
81+
},
82+
{
83+
relations: [
84+
{ field: 'messageId', sourceTable: 'messages' },
85+
{ field: 'fileId', sourceTable: 'files' },
86+
],
87+
table: 'messagesFiles',
88+
},
89+
{
90+
relations: [{ field: 'messageId', sourceTable: 'messages' }],
91+
table: 'messageQueries',
92+
},
93+
{
94+
relations: [
95+
{ field: 'messageId', sourceTable: 'messages' },
96+
{ field: 'chunkId', sourceTable: 'chunks' },
97+
],
98+
table: 'messageQueryChunks',
99+
},
100+
{
101+
relations: [
102+
{ field: 'messageId', sourceTable: 'messages' },
103+
{ field: 'chunkId', sourceTable: 'chunks' },
104+
],
105+
table: 'messageChunks',
106+
},
107+
{
108+
relations: [
109+
{ field: 'knowledgeBaseId', sourceTable: 'knowledgeBases' },
110+
{ field: 'fileId', sourceTable: 'files' },
111+
],
112+
table: 'knowledgeBaseFiles',
113+
},
114+
] as RelationTableConfig[],
115+
};
116+
117+
export class DataExporterRepos {
118+
constructor(
119+
private db: LobeChatDatabase,
120+
private userId: string,
121+
) {}
122+
123+
private removeUserId(data: any[]) {
124+
return data.map((item) => {
125+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
126+
const { userId: _, ...rest } = item;
127+
return rest;
128+
});
129+
}
130+
131+
private async queryTable(config: RelationTableConfig, existingData: Record<string, any[]>) {
132+
const { table } = config;
133+
const tableObj = EXPORT_TABLES[table];
134+
if (!tableObj) throw new Error(`Table ${table} not found`);
135+
136+
try {
137+
let where;
138+
139+
const conditions = config.relations.map((relation) => {
140+
const sourceData = existingData[relation.sourceTable] || [];
141+
142+
const sourceIds = sourceData.map((item) => item[relation.sourceField || 'id']);
143+
console.log(sourceIds);
144+
return inArray(tableObj[relation.field], sourceIds);
145+
});
146+
147+
where = conditions.length === 1 ? conditions[0] : and(...conditions);
148+
149+
const result = await this.db.query[table].findMany({ where });
150+
151+
// 只对使用 userId 查询的表移除 userId 字段
152+
console.log(`Successfully exported table: ${table}, count: ${result.length}`);
153+
return config.relations ? result : this.removeUserId(result);
154+
} catch (error) {
155+
console.error(`Error querying table ${table}:`, error);
156+
return [];
157+
}
158+
}
159+
160+
private async queryBaseTables(config: BaseTableConfig) {
161+
const { table } = config;
162+
const tableObj = EXPORT_TABLES[table];
163+
if (!tableObj) throw new Error(`Table ${table} not found`);
164+
165+
try {
166+
// 如果有关联配置,使用关联查询
167+
168+
// 默认使用 userId 查询,特殊情况使用 userField
169+
const userField = config.userField || 'userId';
170+
const where = eq(tableObj[userField], this.userId);
171+
172+
const result = await this.db.query[table].findMany({ where });
173+
174+
// 只对使用 userId 查询的表移除 userId 字段
175+
console.log(`Successfully exported table: ${table}, count: ${result.length}`);
176+
return this.removeUserId(result);
177+
} catch (error) {
178+
console.error(`Error querying table ${table}:`, error);
179+
return [];
180+
}
181+
}
182+
183+
async export(concurrency = 10) {
184+
const result: Record<string, any[]> = {};
185+
186+
// 1. 首先并发查询所有基础表
187+
console.log('Querying base tables...');
188+
const baseResults = await pMap(
189+
DATA_EXPORT_CONFIG.baseTables,
190+
async (config) => ({ data: await this.queryBaseTables(config), table: config.table }),
191+
{ concurrency },
192+
);
193+
194+
// 更新结果集
195+
baseResults.forEach(({ table, data }) => {
196+
result[table] = data;
197+
});
198+
199+
console.log('baseResults:', baseResults);
200+
// 2. 然后并发查询所有关联表
201+
202+
console.log('Querying relation tables...');
203+
const relationResults = await pMap(
204+
DATA_EXPORT_CONFIG.relationTables,
205+
async (config) => ({
206+
data: await this.queryTable(config, result),
207+
table: config.table,
208+
}),
209+
{ concurrency },
210+
);
211+
212+
// 更新结果集
213+
relationResults.forEach(({ table, data }) => {
214+
result[table] = data;
215+
});
216+
217+
return result;
218+
}
219+
}

src/features/DevPanel/PostgresViewer/DataTable/index.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import React from 'react';
55
import { Center, Flexbox } from 'react-layout-kit';
66
import { mutate } from 'swr';
77

8+
import { exportService } from '@/services/export';
9+
810
import Header from '../../features/Header';
911
import Table from '../../features/Table';
1012
import { FETCH_TABLE_DATA_KEY, usePgTable, useTableColumns } from '../usePgTable';
@@ -40,6 +42,10 @@ const DataTable = ({ tableName }: DataTableProps) => {
4042
{
4143
icon: Download,
4244
title: 'Export',
45+
onClick:async () => {
46+
const data = await exportService.exportData();
47+
console.log(data);
48+
}
4349
},
4450
{
4551
icon: RefreshCw,

src/services/export/client.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { clientDB } from '@/database/client/db';
2+
import { DataExporterRepos } from '@/database/repositories/dataExporter';
3+
import { BaseClientService } from '@/services/baseClientService';
4+
5+
export class ClientService extends BaseClientService {
6+
private get dataExporterRepos(): DataExporterRepos {
7+
return new DataExporterRepos(clientDB as any, this.userId);
8+
}
9+
10+
exportData = async () => {
11+
return this.dataExporterRepos.export();
12+
};
13+
}

src/services/export/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ClientService } from './client';
2+
3+
export const exportService = new ClientService();

0 commit comments

Comments
 (0)