@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/changelog/v1.json +14 -0
  3. package/package.json +2 -1
  4. package/src/app/(main)/discover/(detail)/provider/[slug]/features/ProviderConfig.tsx +2 -2
  5. package/src/app/(main)/settings/hooks/useCategory.tsx +3 -3
  6. package/src/app/(main)/settings/provider/(detail)/[id]/ClientMode.tsx +25 -0
  7. package/src/app/(main)/settings/provider/(detail)/[id]/page.tsx +2 -1
  8. package/src/app/(main)/settings/provider/ProviderMenu/SortProviderModal/index.tsx +0 -1
  9. package/src/database/client/migrations.json +11 -0
  10. package/src/database/repositories/tableViewer/index.test.ts +256 -0
  11. package/src/database/repositories/tableViewer/index.ts +251 -0
  12. package/src/database/server/models/aiProvider.ts +2 -2
  13. package/src/features/DevPanel/FloatPanel.tsx +136 -0
  14. package/src/features/DevPanel/PostgresViewer/DataTable/Table.tsx +157 -0
  15. package/src/features/DevPanel/PostgresViewer/DataTable/TableCell.tsx +34 -0
  16. package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +67 -0
  17. package/src/features/DevPanel/PostgresViewer/Schema.tsx +196 -0
  18. package/src/features/DevPanel/PostgresViewer/TableColumns.tsx +67 -0
  19. package/src/features/DevPanel/PostgresViewer/index.tsx +19 -0
  20. package/src/features/DevPanel/PostgresViewer/useTableColumns.ts +13 -0
  21. package/src/features/DevPanel/index.tsx +12 -0
  22. package/src/features/ModelSwitchPanel/index.tsx +4 -2
  23. package/src/hooks/useEnabledChatModels.ts +2 -2
  24. package/src/hooks/useModelContextWindowTokens.ts +2 -2
  25. package/src/hooks/useModelHasContextWindowToken.ts +2 -2
  26. package/src/hooks/useModelSupportToolUse.ts +2 -2
  27. package/src/hooks/useModelSupportVision.ts +2 -2
  28. package/src/layout/GlobalProvider/index.tsx +2 -2
  29. package/src/services/_auth.ts +2 -2
  30. package/src/services/aiModel/client.ts +60 -0
  31. package/src/services/aiModel/index.test.ts +10 -0
  32. package/src/services/aiModel/index.ts +5 -0
  33. package/src/services/aiModel/server.ts +47 -0
  34. package/src/services/aiModel/type.ts +30 -0
  35. package/src/services/aiProvider/client.ts +64 -0
  36. package/src/services/aiProvider/index.test.ts +10 -0
  37. package/src/services/aiProvider/index.ts +5 -0
  38. package/src/services/aiProvider/server.ts +43 -0
  39. package/src/services/aiProvider/type.ts +26 -0
  40. package/src/services/chat.ts +5 -5
  41. package/src/services/tableViewer/client.ts +16 -0
  42. package/src/services/tableViewer/index.ts +3 -0
  43. package/src/store/aiInfra/slices/aiProvider/action.ts +2 -2
  44. package/src/types/serverConfig.ts +6 -0
  45. package/src/types/tableViewer.ts +30 -0
  46. package/tests/utils.tsx +46 -0
  47. package/vercel.json +1 -1
  48. package/src/features/DebugUI/Content.tsx +0 -34
  49. package/src/features/DebugUI/index.tsx +0 -20
  50. package/src/services/aiModel.ts +0 -52
  51. 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: DecryptUserKeyVaults,
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: DecryptUserKeyVaults) => {
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;