@lobehub/chat 1.46.7 → 1.47.1
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/CHANGELOG.md +42 -0
- package/changelog/v1.json +14 -0
- package/package.json +2 -1
- package/src/app/(main)/discover/(detail)/provider/[slug]/features/ProviderConfig.tsx +2 -2
- package/src/app/(main)/settings/hooks/useCategory.tsx +3 -3
- package/src/app/(main)/settings/provider/(detail)/[id]/ClientMode.tsx +25 -0
- package/src/app/(main)/settings/provider/(detail)/[id]/page.tsx +2 -1
- package/src/app/(main)/settings/provider/ProviderMenu/SortProviderModal/index.tsx +0 -1
- package/src/database/client/migrations.json +11 -0
- package/src/database/repositories/tableViewer/index.test.ts +256 -0
- package/src/database/repositories/tableViewer/index.ts +251 -0
- package/src/database/server/models/aiProvider.ts +2 -2
- package/src/features/DevPanel/FloatPanel.tsx +136 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/Table.tsx +157 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/TableCell.tsx +34 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +67 -0
- package/src/features/DevPanel/PostgresViewer/Schema.tsx +196 -0
- package/src/features/DevPanel/PostgresViewer/TableColumns.tsx +67 -0
- package/src/features/DevPanel/PostgresViewer/index.tsx +19 -0
- package/src/features/DevPanel/PostgresViewer/useTableColumns.ts +13 -0
- package/src/features/DevPanel/index.tsx +12 -0
- package/src/features/ModelSwitchPanel/index.tsx +4 -2
- package/src/hooks/useEnabledChatModels.ts +2 -2
- package/src/hooks/useModelContextWindowTokens.ts +2 -2
- package/src/hooks/useModelHasContextWindowToken.ts +2 -2
- package/src/hooks/useModelSupportToolUse.ts +2 -2
- package/src/hooks/useModelSupportVision.ts +2 -2
- package/src/layout/GlobalProvider/index.tsx +2 -2
- package/src/services/_auth.ts +2 -2
- package/src/services/aiModel/client.ts +60 -0
- package/src/services/aiModel/index.test.ts +10 -0
- package/src/services/aiModel/index.ts +5 -0
- package/src/services/aiModel/server.ts +47 -0
- package/src/services/aiModel/type.ts +30 -0
- package/src/services/aiProvider/client.ts +64 -0
- package/src/services/aiProvider/index.test.ts +10 -0
- package/src/services/aiProvider/index.ts +5 -0
- package/src/services/aiProvider/server.ts +43 -0
- package/src/services/aiProvider/type.ts +26 -0
- package/src/services/chat.ts +5 -5
- package/src/services/tableViewer/client.ts +16 -0
- package/src/services/tableViewer/index.ts +3 -0
- package/src/store/aiInfra/slices/aiProvider/action.ts +2 -2
- package/src/types/serverConfig.ts +6 -0
- package/src/types/tableViewer.ts +30 -0
- package/tests/utils.tsx +46 -0
- package/vercel.json +1 -1
- package/src/features/DebugUI/Content.tsx +0 -34
- package/src/features/DebugUI/index.tsx +0 -20
- package/src/services/aiModel.ts +0 -52
- package/src/services/aiProvider.ts +0 -47
@@ -0,0 +1,251 @@
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
2
|
+
import pMap from 'p-map';
|
3
|
+
|
4
|
+
import { LobeChatDatabase } from '@/database/type';
|
5
|
+
import {
|
6
|
+
FilterCondition,
|
7
|
+
PaginationParams,
|
8
|
+
TableBasicInfo,
|
9
|
+
TableColumnInfo,
|
10
|
+
} from '@/types/tableViewer';
|
11
|
+
|
12
|
+
export class TableViewerRepo {
|
13
|
+
private userId: string;
|
14
|
+
private db: LobeChatDatabase;
|
15
|
+
|
16
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
17
|
+
this.userId = userId;
|
18
|
+
this.db = db;
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* 获取数据库中所有的表
|
23
|
+
*/
|
24
|
+
async getAllTables(schema = 'public'): Promise<TableBasicInfo[]> {
|
25
|
+
const query = sql`
|
26
|
+
SELECT
|
27
|
+
table_name as name,
|
28
|
+
table_type as type
|
29
|
+
FROM information_schema.tables
|
30
|
+
WHERE table_schema = ${schema}
|
31
|
+
ORDER BY table_name;
|
32
|
+
`;
|
33
|
+
|
34
|
+
const tables = await this.db.execute(query);
|
35
|
+
|
36
|
+
const tableNames = tables.rows.map((row) => row.name) as string[];
|
37
|
+
|
38
|
+
const counts = await pMap(tableNames, async (name) => this.getTableCount(name), {
|
39
|
+
concurrency: 10,
|
40
|
+
});
|
41
|
+
|
42
|
+
return tables.rows.map((row, index) => ({
|
43
|
+
count: counts[index],
|
44
|
+
name: row.name,
|
45
|
+
type: row.type,
|
46
|
+
})) as TableBasicInfo[];
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* 获取指定表的详细结构信息
|
51
|
+
*/
|
52
|
+
async getTableDetails(tableName: string): Promise<TableColumnInfo[]> {
|
53
|
+
const query = sql`
|
54
|
+
SELECT
|
55
|
+
c.column_name,
|
56
|
+
c.data_type,
|
57
|
+
c.is_nullable,
|
58
|
+
c.column_default,
|
59
|
+
-- 主键信息
|
60
|
+
(
|
61
|
+
SELECT true
|
62
|
+
FROM information_schema.table_constraints tc
|
63
|
+
JOIN information_schema.key_column_usage kcu
|
64
|
+
ON tc.constraint_name = kcu.constraint_name
|
65
|
+
WHERE tc.table_name = c.table_name
|
66
|
+
AND kcu.column_name = c.column_name
|
67
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
68
|
+
) is_primary_key,
|
69
|
+
-- 外键信息
|
70
|
+
(
|
71
|
+
SELECT json_build_object(
|
72
|
+
'table', ccu.table_name,
|
73
|
+
'column', ccu.column_name
|
74
|
+
)
|
75
|
+
FROM information_schema.table_constraints tc
|
76
|
+
JOIN information_schema.key_column_usage kcu
|
77
|
+
ON tc.constraint_name = kcu.constraint_name
|
78
|
+
JOIN information_schema.constraint_column_usage ccu
|
79
|
+
ON ccu.constraint_name = tc.constraint_name
|
80
|
+
WHERE tc.table_name = c.table_name
|
81
|
+
AND kcu.column_name = c.column_name
|
82
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
83
|
+
) foreign_key
|
84
|
+
FROM information_schema.columns c
|
85
|
+
WHERE c.table_name = ${tableName}
|
86
|
+
AND c.table_schema = 'public'
|
87
|
+
ORDER BY c.ordinal_position;
|
88
|
+
`;
|
89
|
+
|
90
|
+
const columns = await this.db.execute(query);
|
91
|
+
|
92
|
+
return columns.rows.map((col: any) => ({
|
93
|
+
defaultValue: col.column_default,
|
94
|
+
foreignKey: col.foreign_key,
|
95
|
+
isPrimaryKey: !!col.is_primary_key,
|
96
|
+
name: col.column_name,
|
97
|
+
nullable: col.is_nullable === 'YES',
|
98
|
+
type: col.data_type,
|
99
|
+
}));
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* 获取表数据,支持分页、排序和筛选
|
104
|
+
*/
|
105
|
+
async getTableData(tableName: string, pagination: PaginationParams, filters?: FilterCondition[]) {
|
106
|
+
const offset = (pagination.page - 1) * pagination.pageSize;
|
107
|
+
|
108
|
+
// 构建基础查询
|
109
|
+
let baseQuery = sql`SELECT * FROM ${sql.identifier(tableName)}`;
|
110
|
+
|
111
|
+
// 添加筛选条件
|
112
|
+
if (filters && filters.length > 0) {
|
113
|
+
const whereConditions = filters.map((filter) => {
|
114
|
+
const column = sql.identifier(filter.column);
|
115
|
+
|
116
|
+
switch (filter.operator) {
|
117
|
+
case 'equals': {
|
118
|
+
return sql`${column} = ${filter.value}`;
|
119
|
+
}
|
120
|
+
case 'contains': {
|
121
|
+
return sql`${column} ILIKE ${`%${filter.value}%`}`;
|
122
|
+
}
|
123
|
+
case 'startsWith': {
|
124
|
+
return sql`${column} ILIKE ${`${filter.value}%`}`;
|
125
|
+
}
|
126
|
+
case 'endsWith': {
|
127
|
+
return sql`${column} ILIKE ${`%${filter.value}`}`;
|
128
|
+
}
|
129
|
+
default: {
|
130
|
+
return sql`1=1`;
|
131
|
+
}
|
132
|
+
}
|
133
|
+
});
|
134
|
+
|
135
|
+
baseQuery = sql`${baseQuery} WHERE ${sql.join(whereConditions, sql` AND `)}`;
|
136
|
+
}
|
137
|
+
|
138
|
+
// 添加排序
|
139
|
+
if (pagination.sortBy) {
|
140
|
+
const direction = pagination.sortOrder === 'desc' ? sql`DESC` : sql`ASC`;
|
141
|
+
baseQuery = sql`${baseQuery} ORDER BY ${sql.identifier(pagination.sortBy)} ${direction}`;
|
142
|
+
}
|
143
|
+
|
144
|
+
// 添加分页
|
145
|
+
const query = sql`${baseQuery} LIMIT ${pagination.pageSize} OFFSET ${offset}`;
|
146
|
+
|
147
|
+
// 获取总数
|
148
|
+
const countQuery = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
|
149
|
+
|
150
|
+
// 并行执行查询
|
151
|
+
const [data, count] = await Promise.all([this.db.execute(query), this.db.execute(countQuery)]);
|
152
|
+
|
153
|
+
return {
|
154
|
+
data: data.rows,
|
155
|
+
pagination: {
|
156
|
+
page: pagination.page,
|
157
|
+
pageSize: pagination.pageSize,
|
158
|
+
total: Number(count.rows[0].total),
|
159
|
+
},
|
160
|
+
};
|
161
|
+
}
|
162
|
+
|
163
|
+
/**
|
164
|
+
* 更新表中的一行数据
|
165
|
+
*/
|
166
|
+
async updateRow(
|
167
|
+
tableName: string,
|
168
|
+
id: string,
|
169
|
+
primaryKeyColumn: string,
|
170
|
+
data: Record<string, any>,
|
171
|
+
) {
|
172
|
+
const setColumns = Object.entries(data).map(([key, value]) => {
|
173
|
+
return sql`${sql.identifier(key)} = ${value}`;
|
174
|
+
});
|
175
|
+
|
176
|
+
const query = sql`
|
177
|
+
UPDATE ${sql.identifier(tableName)}
|
178
|
+
SET ${sql.join(setColumns, sql`, `)}
|
179
|
+
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
|
180
|
+
RETURNING *
|
181
|
+
`;
|
182
|
+
|
183
|
+
const result = await this.db.execute(query);
|
184
|
+
return result.rows[0];
|
185
|
+
}
|
186
|
+
|
187
|
+
/**
|
188
|
+
* 删除表中的一行数据
|
189
|
+
*/
|
190
|
+
async deleteRow(tableName: string, id: string, primaryKeyColumn: string) {
|
191
|
+
const query = sql`
|
192
|
+
DELETE FROM ${sql.identifier(tableName)}
|
193
|
+
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
|
194
|
+
`;
|
195
|
+
|
196
|
+
await this.db.execute(query);
|
197
|
+
}
|
198
|
+
|
199
|
+
/**
|
200
|
+
* 插入新行数据
|
201
|
+
*/
|
202
|
+
async insertRow(tableName: string, data: Record<string, any>) {
|
203
|
+
const columns = Object.keys(data).map((key) => sql.identifier(key));
|
204
|
+
const values = Object.values(data);
|
205
|
+
|
206
|
+
const query = sql`
|
207
|
+
INSERT INTO ${sql.identifier(tableName)}
|
208
|
+
(${sql.join(columns, sql`, `)})
|
209
|
+
VALUES (${sql.join(
|
210
|
+
values.map((v) => sql`${v}`),
|
211
|
+
sql`, `,
|
212
|
+
)})
|
213
|
+
RETURNING *
|
214
|
+
`;
|
215
|
+
|
216
|
+
const result = await this.db.execute(query);
|
217
|
+
return result.rows[0];
|
218
|
+
}
|
219
|
+
|
220
|
+
/**
|
221
|
+
* 获取表的总记录数
|
222
|
+
*/
|
223
|
+
async getTableCount(tableName: string): Promise<number> {
|
224
|
+
const query = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
|
225
|
+
const result = await this.db.execute(query);
|
226
|
+
return Number(result.rows[0].total);
|
227
|
+
}
|
228
|
+
|
229
|
+
/**
|
230
|
+
* 批量删除数据
|
231
|
+
*/
|
232
|
+
async batchDelete(tableName: string, ids: string[], primaryKeyColumn: string) {
|
233
|
+
const query = sql`
|
234
|
+
DELETE FROM ${sql.identifier(tableName)}
|
235
|
+
WHERE ${sql.identifier(primaryKeyColumn)} = ANY(${ids})
|
236
|
+
`;
|
237
|
+
|
238
|
+
await this.db.execute(query);
|
239
|
+
}
|
240
|
+
|
241
|
+
/**
|
242
|
+
* 导出表数据(支持分页导出)
|
243
|
+
*/
|
244
|
+
async exportTableData(
|
245
|
+
tableName: string,
|
246
|
+
pagination?: PaginationParams,
|
247
|
+
filters?: FilterCondition[],
|
248
|
+
) {
|
249
|
+
return this.getTableData(tableName, pagination || { page: 1, pageSize: 1000 }, filters);
|
250
|
+
}
|
251
|
+
}
|
@@ -165,7 +165,7 @@ export class AiProviderModel {
|
|
165
165
|
|
166
166
|
getAiProviderById = async (
|
167
167
|
id: string,
|
168
|
-
decryptor
|
168
|
+
decryptor?: DecryptUserKeyVaults,
|
169
169
|
): Promise<AiProviderDetailItem | undefined> => {
|
170
170
|
const query = this.db
|
171
171
|
.select({
|
@@ -205,7 +205,7 @@ export class AiProviderModel {
|
|
205
205
|
return { ...result, keyVaults } as AiProviderDetailItem;
|
206
206
|
};
|
207
207
|
|
208
|
-
getAiProviderRuntimeConfig = async (decryptor
|
208
|
+
getAiProviderRuntimeConfig = async (decryptor?: DecryptUserKeyVaults) => {
|
209
209
|
const result = await this.db
|
210
210
|
.select({
|
211
211
|
fetchOnClient: aiProviders.fetchOnClient,
|
@@ -0,0 +1,136 @@
|
|
1
|
+
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
|
+
import { FloatButton } from 'antd';
|
3
|
+
import { createStyles } from 'antd-style';
|
4
|
+
import { BugIcon, BugOff, XIcon } from 'lucide-react';
|
5
|
+
import React, { PropsWithChildren, useEffect, useState } from 'react';
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
7
|
+
import { Rnd } from 'react-rnd';
|
8
|
+
|
9
|
+
// 定义样式
|
10
|
+
const useStyles = createStyles(({ token, css }) => {
|
11
|
+
return {
|
12
|
+
collapsed: css`
|
13
|
+
pointer-events: none;
|
14
|
+
transform: scale(0.8);
|
15
|
+
opacity: 0;
|
16
|
+
`,
|
17
|
+
content: css`
|
18
|
+
overflow: auto;
|
19
|
+
flex: 1;
|
20
|
+
height: 100%;
|
21
|
+
color: ${token.colorText};
|
22
|
+
`,
|
23
|
+
|
24
|
+
expanded: css`
|
25
|
+
pointer-events: auto;
|
26
|
+
transform: scale(1);
|
27
|
+
opacity: 1;
|
28
|
+
`,
|
29
|
+
|
30
|
+
header: css`
|
31
|
+
cursor: move;
|
32
|
+
user-select: none;
|
33
|
+
|
34
|
+
padding-block: 8px;
|
35
|
+
padding-inline: 16px;
|
36
|
+
border-block-end: 1px solid ${token.colorBorderSecondary};
|
37
|
+
border-start-start-radius: 12px;
|
38
|
+
border-start-end-radius: 12px;
|
39
|
+
|
40
|
+
font-weight: ${token.fontWeightStrong};
|
41
|
+
color: ${token.colorText};
|
42
|
+
|
43
|
+
background: ${token.colorFillAlter};
|
44
|
+
`,
|
45
|
+
panel: css`
|
46
|
+
position: fixed;
|
47
|
+
z-index: 1000;
|
48
|
+
|
49
|
+
overflow: hidden;
|
50
|
+
display: flex;
|
51
|
+
|
52
|
+
border-radius: 12px;
|
53
|
+
|
54
|
+
background: ${token.colorBgContainer};
|
55
|
+
box-shadow: ${token.boxShadow};
|
56
|
+
|
57
|
+
transition: opacity ${token.motionDurationMid} ${token.motionEaseInOut};
|
58
|
+
`,
|
59
|
+
};
|
60
|
+
});
|
61
|
+
|
62
|
+
const minWidth = 800;
|
63
|
+
const minHeight = 600;
|
64
|
+
|
65
|
+
const CollapsibleFloatPanel = ({ children }: PropsWithChildren) => {
|
66
|
+
const { styles } = useStyles();
|
67
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
68
|
+
const [position, setPosition] = useState({ x: 100, y: 100 });
|
69
|
+
const [size, setSize] = useState({ height: minHeight, width: minWidth });
|
70
|
+
|
71
|
+
useEffect(() => {
|
72
|
+
try {
|
73
|
+
const localStoragePosition = localStorage.getItem('debug-panel-position');
|
74
|
+
if (localStoragePosition && JSON.parse(localStoragePosition)) {
|
75
|
+
setPosition(JSON.parse(localStoragePosition));
|
76
|
+
}
|
77
|
+
} catch {
|
78
|
+
/* empty */
|
79
|
+
}
|
80
|
+
|
81
|
+
try {
|
82
|
+
const localStorageSize = localStorage.getItem('debug-panel-size');
|
83
|
+
if (localStorageSize && JSON.parse(localStorageSize)) {
|
84
|
+
setSize(JSON.parse(localStorageSize));
|
85
|
+
}
|
86
|
+
} catch {
|
87
|
+
/* empty */
|
88
|
+
}
|
89
|
+
}, []);
|
90
|
+
|
91
|
+
return (
|
92
|
+
<>
|
93
|
+
<FloatButton
|
94
|
+
icon={<Icon icon={isExpanded ? BugOff : BugIcon} />}
|
95
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
96
|
+
style={{ bottom: 24, right: 24 }}
|
97
|
+
/>
|
98
|
+
{isExpanded && (
|
99
|
+
<Rnd
|
100
|
+
bounds="window"
|
101
|
+
className={`${styles.panel} ${isExpanded ? styles.expanded : styles.collapsed}`}
|
102
|
+
dragHandleClassName="panel-drag-handle"
|
103
|
+
minHeight={minHeight}
|
104
|
+
minWidth={minWidth}
|
105
|
+
onDragStop={(e, d) => {
|
106
|
+
setPosition({ x: d.x, y: d.y });
|
107
|
+
}}
|
108
|
+
onResizeStop={(e, direction, ref, delta, position) => {
|
109
|
+
setSize({
|
110
|
+
height: Number(ref.style.height),
|
111
|
+
width: Number(ref.style.width),
|
112
|
+
});
|
113
|
+
setPosition(position);
|
114
|
+
}}
|
115
|
+
position={position}
|
116
|
+
size={size}
|
117
|
+
>
|
118
|
+
<Flexbox height={'100%'}>
|
119
|
+
<Flexbox
|
120
|
+
align={'center'}
|
121
|
+
className={`panel-drag-handle ${styles.header}`}
|
122
|
+
horizontal
|
123
|
+
justify={'space-between'}
|
124
|
+
>
|
125
|
+
开发者面板
|
126
|
+
<ActionIcon icon={XIcon} onClick={() => setIsExpanded(false)} />
|
127
|
+
</Flexbox>
|
128
|
+
<Flexbox className={styles.content}>{children}</Flexbox>
|
129
|
+
</Flexbox>
|
130
|
+
</Rnd>
|
131
|
+
)}
|
132
|
+
</>
|
133
|
+
);
|
134
|
+
};
|
135
|
+
|
136
|
+
export default CollapsibleFloatPanel;
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import { createStyles } from 'antd-style';
|
2
|
+
import React from 'react';
|
3
|
+
import { Center } from 'react-layout-kit';
|
4
|
+
import { TableVirtuoso } from 'react-virtuoso';
|
5
|
+
import useSWR from 'swr';
|
6
|
+
|
7
|
+
import { tableViewerService } from '@/services/tableViewer';
|
8
|
+
import { useGlobalStore } from '@/store/global';
|
9
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
10
|
+
|
11
|
+
import { useTableColumns } from '../useTableColumns';
|
12
|
+
import TableCell from './TableCell';
|
13
|
+
|
14
|
+
const useStyles = createStyles(({ token, css }) => ({
|
15
|
+
columnList: css`
|
16
|
+
margin-inline-start: 32px;
|
17
|
+
font-size: ${token.fontSizeSM}px;
|
18
|
+
color: ${token.colorTextSecondary};
|
19
|
+
|
20
|
+
> div {
|
21
|
+
padding-block: ${token.paddingXS}px;
|
22
|
+
padding-inline: 0;
|
23
|
+
}
|
24
|
+
`,
|
25
|
+
table: css`
|
26
|
+
overflow: scroll hidden;
|
27
|
+
flex: 1;
|
28
|
+
|
29
|
+
table {
|
30
|
+
border-collapse: collapse;
|
31
|
+
width: 100%;
|
32
|
+
margin-inline-end: 12px;
|
33
|
+
font-family: ${token.fontFamilyCode};
|
34
|
+
}
|
35
|
+
|
36
|
+
thead {
|
37
|
+
tr {
|
38
|
+
outline: 1px solid ${token.colorBorderSecondary};
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
th,
|
43
|
+
td {
|
44
|
+
overflow: hidden;
|
45
|
+
|
46
|
+
max-width: 200px;
|
47
|
+
padding-block: 8px;
|
48
|
+
padding-inline: 12px;
|
49
|
+
border-inline-end: 1px solid ${token.colorBorderSecondary};
|
50
|
+
|
51
|
+
font-size: 12px;
|
52
|
+
text-overflow: ellipsis;
|
53
|
+
white-space: nowrap;
|
54
|
+
}
|
55
|
+
|
56
|
+
th {
|
57
|
+
position: sticky;
|
58
|
+
z-index: 1;
|
59
|
+
inset-block-start: 0;
|
60
|
+
|
61
|
+
border-block-end: 1px solid ${token.colorBorderSecondary};
|
62
|
+
|
63
|
+
font-weight: ${token.fontWeightStrong};
|
64
|
+
text-align: start;
|
65
|
+
text-wrap: nowrap;
|
66
|
+
|
67
|
+
background: ${token.colorBgElevated};
|
68
|
+
}
|
69
|
+
|
70
|
+
td {
|
71
|
+
border-block-end: 1px solid ${token.colorBorderSecondary};
|
72
|
+
text-wrap: nowrap;
|
73
|
+
}
|
74
|
+
|
75
|
+
tbody {
|
76
|
+
tr:hover {
|
77
|
+
background: ${token.colorFillTertiary};
|
78
|
+
}
|
79
|
+
}
|
80
|
+
`,
|
81
|
+
tableItem: css`
|
82
|
+
cursor: pointer;
|
83
|
+
|
84
|
+
display: flex;
|
85
|
+
gap: ${token.padding}px;
|
86
|
+
align-items: center;
|
87
|
+
|
88
|
+
padding: 12px;
|
89
|
+
border-radius: ${token.borderRadius}px;
|
90
|
+
|
91
|
+
color: ${token.colorText};
|
92
|
+
`,
|
93
|
+
}));
|
94
|
+
|
95
|
+
interface TableProps {
|
96
|
+
tableName?: string;
|
97
|
+
}
|
98
|
+
|
99
|
+
const Table = ({ tableName }: TableProps) => {
|
100
|
+
const { styles } = useStyles();
|
101
|
+
|
102
|
+
const tableColumns = useTableColumns(tableName);
|
103
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
104
|
+
|
105
|
+
const tableData = useSWR(
|
106
|
+
isDBInited && tableName ? ['fetch-table-data', tableName] : null,
|
107
|
+
([, table]) => tableViewerService.getTableData(table),
|
108
|
+
);
|
109
|
+
|
110
|
+
const columns = tableColumns.data?.map((t) => t.name) || [];
|
111
|
+
const isLoading = tableColumns.isLoading || tableData.isLoading;
|
112
|
+
|
113
|
+
if (!tableName) return <Center height={'80%'}>Select a table to view data</Center>;
|
114
|
+
|
115
|
+
if (isLoading) return <Center height={'80%'}>Loading...</Center>;
|
116
|
+
|
117
|
+
const dataSource = tableData.data?.data || [];
|
118
|
+
const header = (
|
119
|
+
<tr>
|
120
|
+
{columns.map((column) => (
|
121
|
+
<th key={column}>{column}</th>
|
122
|
+
))}
|
123
|
+
</tr>
|
124
|
+
);
|
125
|
+
|
126
|
+
return (
|
127
|
+
<div className={styles.table}>
|
128
|
+
{dataSource.length === 0 ? (
|
129
|
+
<>
|
130
|
+
<table>
|
131
|
+
<thead>{header}</thead>
|
132
|
+
</table>
|
133
|
+
<Center height={400}>no rows</Center>
|
134
|
+
</>
|
135
|
+
) : (
|
136
|
+
<TableVirtuoso
|
137
|
+
data={dataSource}
|
138
|
+
fixedHeaderContent={() => header}
|
139
|
+
itemContent={(index, row) => (
|
140
|
+
<>
|
141
|
+
{columns.map((column) => (
|
142
|
+
<TableCell
|
143
|
+
column={column}
|
144
|
+
dataItem={row}
|
145
|
+
key={`${column}_${index}`}
|
146
|
+
rowIndex={index}
|
147
|
+
/>
|
148
|
+
))}
|
149
|
+
</>
|
150
|
+
)}
|
151
|
+
/>
|
152
|
+
)}
|
153
|
+
</div>
|
154
|
+
);
|
155
|
+
};
|
156
|
+
|
157
|
+
export default Table;
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import React, { useMemo } from 'react';
|
2
|
+
|
3
|
+
interface TableCellProps {
|
4
|
+
column: string;
|
5
|
+
dataItem: any;
|
6
|
+
rowIndex: number;
|
7
|
+
}
|
8
|
+
|
9
|
+
const TableCell = ({ dataItem, column, rowIndex }: TableCellProps) => {
|
10
|
+
const data = dataItem[column];
|
11
|
+
const content = useMemo(() => {
|
12
|
+
switch (typeof data) {
|
13
|
+
case 'object': {
|
14
|
+
return JSON.stringify(data);
|
15
|
+
}
|
16
|
+
|
17
|
+
case 'boolean': {
|
18
|
+
return data ? 'True' : 'False';
|
19
|
+
}
|
20
|
+
|
21
|
+
default: {
|
22
|
+
return data;
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}, [data]);
|
26
|
+
|
27
|
+
return (
|
28
|
+
<td key={column} onDoubleClick={() => console.log('Edit cell:', rowIndex, column)}>
|
29
|
+
{content}
|
30
|
+
</td>
|
31
|
+
);
|
32
|
+
};
|
33
|
+
|
34
|
+
export default TableCell;
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
|
+
import { Button } from 'antd';
|
3
|
+
import { createStyles } from 'antd-style';
|
4
|
+
import { Download, Filter, RefreshCw } from 'lucide-react';
|
5
|
+
import React from 'react';
|
6
|
+
|
7
|
+
import Table from './Table';
|
8
|
+
|
9
|
+
const useStyles = createStyles(({ token, css }) => ({
|
10
|
+
dataPanel: css`
|
11
|
+
overflow: hidden;
|
12
|
+
display: flex;
|
13
|
+
flex: 1;
|
14
|
+
flex-direction: column;
|
15
|
+
|
16
|
+
height: 100%;
|
17
|
+
|
18
|
+
background: ${token.colorBgContainer};
|
19
|
+
`,
|
20
|
+
toolbar: css`
|
21
|
+
display: flex;
|
22
|
+
align-items: center;
|
23
|
+
justify-content: space-between;
|
24
|
+
|
25
|
+
padding-block: 12px;
|
26
|
+
padding-inline: 16px;
|
27
|
+
border-block-end: 1px solid ${token.colorBorderSecondary};
|
28
|
+
`,
|
29
|
+
toolbarButtons: css`
|
30
|
+
display: flex;
|
31
|
+
gap: 4px;
|
32
|
+
`,
|
33
|
+
toolbarTitle: css`
|
34
|
+
font-size: ${token.fontSizeLG}px;
|
35
|
+
font-weight: ${token.fontWeightStrong};
|
36
|
+
color: ${token.colorText};
|
37
|
+
`,
|
38
|
+
}));
|
39
|
+
|
40
|
+
interface DataTableProps {
|
41
|
+
tableName: string;
|
42
|
+
}
|
43
|
+
|
44
|
+
const DataTable = ({ tableName }: DataTableProps) => {
|
45
|
+
const { styles } = useStyles();
|
46
|
+
|
47
|
+
return (
|
48
|
+
<div className={styles.dataPanel}>
|
49
|
+
{/* Toolbar */}
|
50
|
+
<div className={styles.toolbar}>
|
51
|
+
<div className={styles.toolbarTitle}>{tableName || 'Select a table'}</div>
|
52
|
+
<div className={styles.toolbarButtons}>
|
53
|
+
<Button color={'default'} icon={<Icon icon={Filter} />} variant={'filled'}>
|
54
|
+
Filter
|
55
|
+
</Button>
|
56
|
+
<ActionIcon icon={Download} title={'Export'} />
|
57
|
+
<ActionIcon icon={RefreshCw} title={'Refresh'} />
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
|
61
|
+
{/* Table */}
|
62
|
+
<Table tableName={tableName} />
|
63
|
+
</div>
|
64
|
+
);
|
65
|
+
};
|
66
|
+
|
67
|
+
export default DataTable;
|