sqlseed-web 0.2.4__py3-none-any.whl
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.
- sqlseed_web/AGENTS.md +106 -0
- sqlseed_web/__init__.py +24 -0
- sqlseed_web/__main__.py +8 -0
- sqlseed_web/_application.py +205 -0
- sqlseed_web/ai_settings.py +290 -0
- sqlseed_web/api.py +1102 -0
- sqlseed_web/app.py +26 -0
- sqlseed_web/managed_worker.py +184 -0
- sqlseed_web/operation_errors.py +42 -0
- sqlseed_web/plugin_environment.py +231 -0
- sqlseed_web/plugin_management.py +322 -0
- sqlseed_web/plugin_process.py +131 -0
- sqlseed_web/runtime_lifecycle.py +130 -0
- sqlseed_web/runtime_session.py +97 -0
- sqlseed_web/settings_environment.py +368 -0
- sqlseed_web/sqlite_target.py +86 -0
- sqlseed_web/state.py +348 -0
- sqlseed_web/static/AGENTS.md +180 -0
- sqlseed_web/static/ai.css +57 -0
- sqlseed_web/static/configs.css +50 -0
- sqlseed_web/static/date-picker.css +230 -0
- sqlseed_web/static/disclosure.css +149 -0
- sqlseed_web/static/graph-clarity.css +103 -0
- sqlseed_web/static/index.html +29 -0
- sqlseed_web/static/js/api.js +228 -0
- sqlseed_web/static/js/app.js +97 -0
- sqlseed_web/static/js/dropdown.js +432 -0
- sqlseed_web/static/js/filepicker.js +203 -0
- sqlseed_web/static/js/genform.js +1066 -0
- sqlseed_web/static/js/labels.js +195 -0
- sqlseed_web/static/js/pages/browse.js +209 -0
- sqlseed_web/static/js/pages/configs.js +424 -0
- sqlseed_web/static/js/pages/connect.js +332 -0
- sqlseed_web/static/js/pages/heal.js +395 -0
- sqlseed_web/static/js/pages/meta.js +110 -0
- sqlseed_web/static/js/pages/runs.js +293 -0
- sqlseed_web/static/js/pages/settings.js +942 -0
- sqlseed_web/static/js/pages/wizard.js +751 -0
- sqlseed_web/static/js/pages/workbench.js +3123 -0
- sqlseed_web/static/js/tree.js +126 -0
- sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
- sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
- sqlseed_web/static/js/workbench/ai-stream.js +116 -0
- sqlseed_web/static/js/workbench/ai.js +888 -0
- sqlseed_web/static/js/workbench/connection.js +508 -0
- sqlseed_web/static/js/workbench/date-picker.js +445 -0
- sqlseed_web/static/js/workbench/dependency-view.js +119 -0
- sqlseed_web/static/js/workbench/editor.js +1236 -0
- sqlseed_web/static/js/workbench/focus.js +11 -0
- sqlseed_web/static/js/workbench/graph-layout.js +332 -0
- sqlseed_web/static/js/workbench/graph.js +970 -0
- sqlseed_web/static/js/workbench/guidance.js +29 -0
- sqlseed_web/static/js/workbench/model.js +124 -0
- sqlseed_web/static/js/workbench/plugin-management.js +512 -0
- sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
- sqlseed_web/static/js/workbench/preview.js +572 -0
- sqlseed_web/static/js/workbench/provider-guide.js +33 -0
- sqlseed_web/static/js/workbench/recovery.js +28 -0
- sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
- sqlseed_web/static/js/workbench/session.js +174 -0
- sqlseed_web/static/js/workbench/table-data.js +186 -0
- sqlseed_web/static/js/workbench/ui.js +262 -0
- sqlseed_web/static/navigation.css +92 -0
- sqlseed_web/static/preview.css +29 -0
- sqlseed_web/static/runs.css +53 -0
- sqlseed_web/static/scrollbars.css +42 -0
- sqlseed_web/static/settings.css +108 -0
- sqlseed_web/static/style.css +3382 -0
- sqlseed_web/static/table-data.css +27 -0
- sqlseed_web/static/workbench.css +509 -0
- sqlseed_web/supervised_plugins.py +173 -0
- sqlseed_web/supervisor.py +238 -0
- sqlseed_web/workbench.py +381 -0
- sqlseed_web/workbench_ai.py +887 -0
- sqlseed_web/workbench_ai_relations.py +285 -0
- sqlseed_web/workbench_ai_stream.py +172 -0
- sqlseed_web/workbench_data.py +163 -0
- sqlseed_web/workbench_execution.py +199 -0
- sqlseed_web/workbench_runtime.py +1218 -0
- sqlseed_web/workbench_schema.py +277 -0
- sqlseed_web/workbench_store.py +458 -0
- sqlseed_web/worker_control.py +192 -0
- sqlseed_web-0.2.4.dist-info/METADATA +105 -0
- sqlseed_web-0.2.4.dist-info/RECORD +87 -0
- sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
- sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
- sqlseed_web-0.2.4.dist-info/licenses/LICENSE +679 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// 生成器中文语义标注(参考工具 树节点括号标注的来源)。
|
|
2
|
+
|
|
3
|
+
export const GEN_LABELS = {
|
|
4
|
+
name: '姓名',
|
|
5
|
+
first_name: '名字',
|
|
6
|
+
last_name: '姓氏',
|
|
7
|
+
username: '用户名',
|
|
8
|
+
email: '电子邮箱',
|
|
9
|
+
phone: '电话号码',
|
|
10
|
+
address: '地址',
|
|
11
|
+
city: '城市',
|
|
12
|
+
country: '国家',
|
|
13
|
+
state: '地区',
|
|
14
|
+
zip_code: '邮政编码',
|
|
15
|
+
country_code: '国家代码',
|
|
16
|
+
url: '网址',
|
|
17
|
+
uuid: 'UUID',
|
|
18
|
+
ipv4: 'IP 地址',
|
|
19
|
+
company: '公司名称',
|
|
20
|
+
job_title: '职位名称',
|
|
21
|
+
catch_phrase: '口号',
|
|
22
|
+
date: '日期',
|
|
23
|
+
datetime: '日期时间',
|
|
24
|
+
timestamp: '时间戳',
|
|
25
|
+
time: '时间',
|
|
26
|
+
integer: '整数',
|
|
27
|
+
float: '小数',
|
|
28
|
+
boolean: '布尔',
|
|
29
|
+
choice: '枚举',
|
|
30
|
+
weighted_choice: '加权枚举',
|
|
31
|
+
pattern: '正则表达式',
|
|
32
|
+
template: '模板',
|
|
33
|
+
string: '字符串',
|
|
34
|
+
text: '文本',
|
|
35
|
+
sentence: '句子',
|
|
36
|
+
word: '单词',
|
|
37
|
+
password: '密码',
|
|
38
|
+
json: 'JSON',
|
|
39
|
+
bytes: '图像或二进制',
|
|
40
|
+
skip: '数据库默认值',
|
|
41
|
+
foreign_key: '外键',
|
|
42
|
+
foreign_key_or_integer: '外键',
|
|
43
|
+
autoincrement: '自增',
|
|
44
|
+
__enrich__: '增强',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function genLabel(gen) {
|
|
48
|
+
return GEN_LABELS[gen] || gen || '—';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Output illustrations supplement the live catalogue, never replace real previews.
|
|
52
|
+
const GENERATOR_GUIDES = {
|
|
53
|
+
name: ['人物的完整姓名;商品名请使用枚举或模板。', '张晓明 / Alex Smith'],
|
|
54
|
+
first_name: ['人物的名字部分。', '晓明 / Alex'], last_name: ['人物的姓氏部分。', '张 / Smith'],
|
|
55
|
+
username: ['登录名或账号名称。', 'alex_chen'], email: ['电子邮箱地址;语言与格式受全局引擎影响。', 'alex@example.test'],
|
|
56
|
+
phone: ['电话号码;可用号码格式统一输出。', '13800138000'], address: ['完整地址;使用全局语言与地区。', '北京市朝阳区…'],
|
|
57
|
+
city: ['城市名称。', '北京 / London'], state: ['省、州或地区名称。', '浙江省 / California'],
|
|
58
|
+
country: ['国家名称。', '中国 / Canada'], country_code: ['国家代码。', 'CN'], zip_code: ['邮政编码。', '100000'],
|
|
59
|
+
url: ['网站地址。', 'https://example.test'], uuid: ['通用唯一标识符。', '1f22b412-3d73-4e22-a5e4-40a7f18ce319'],
|
|
60
|
+
ipv4: ['IPv4 地址。', '192.0.2.10'], company: ['公司或组织名称。', '示例科技有限公司'],
|
|
61
|
+
job_title: ['职位名称。', '软件工程师'], catch_phrase: ['营销口号或短标语。', '让每一天更简单'],
|
|
62
|
+
date: ['在日期范围内随机取值,可限定工作日。', '2026-09-07'],
|
|
63
|
+
datetime: ['在日期和时间范围内随机取值。', '2026-09-07 14:30:00'],
|
|
64
|
+
timestamp: ['生成日期时间对象;写入格式由数据库列类型决定。', '2026-09-07 14:30:00'],
|
|
65
|
+
time: ['一天内的时间,可限定营业时段。', '09:30:00'],
|
|
66
|
+
integer: ['范围内的随机整数,适合数量、年龄或库存。', '12'],
|
|
67
|
+
float: ['范围内的随机小数,适合金额或度量值。', '128.50'], boolean: ['真或假,适合启用、完成等标记。', 'true / false'],
|
|
68
|
+
choice: ['从你提供的候选值中随机选一个,适合状态或商品名。', 'pending / paid / shipped'],
|
|
69
|
+
weighted_choice: ['按指定权重选择候选值,适合不均匀分布。', '普通 80% / VIP 20%'],
|
|
70
|
+
pattern: ['生成符合正则表达式的值,适合固定格式编码。', '正则 [A-Z]{3}[0-9]{4} → ABC1234'],
|
|
71
|
+
template: ['按模板生成编号或组合文本,可包含递增序号。', 'SKU-{sequence:04d} → SKU-0001'],
|
|
72
|
+
string: ['从候选字符中生成随机字符串,适合随机代码;名称建议用枚举。', 'aB72xQ'],
|
|
73
|
+
text: ['较长的自然语言文本。', '一段说明文字…'], sentence: ['一条自然语言句子。', '这是一条示例描述。'],
|
|
74
|
+
word: ['一个单词。', 'river'], password: ['随机密码字符串。', 'a9B!x7Qp'],
|
|
75
|
+
json: ['生成 JSON 值,可指定对象结构。', '{"active": true}'], bytes: ['生成字节或图像内容,适合二进制列。', '16 字节 / PNG 图像'],
|
|
76
|
+
};
|
|
77
|
+
export function genGuide(generator) {
|
|
78
|
+
const guide = GENERATOR_GUIDES[generator];
|
|
79
|
+
return guide ? {purpose: guide[0], example: guide[1]} : {purpose: '扩展生成器;参数由当前服务提供。', example: ''};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 生成器参数中文标签(genform 动态参数表单)。未收录的参数原样显示。
|
|
83
|
+
export const PARAM_LABELS = {
|
|
84
|
+
min_length: '最小长度',
|
|
85
|
+
max_length: '最大长度',
|
|
86
|
+
charset: '字符集',
|
|
87
|
+
min_value: '最小值',
|
|
88
|
+
max_value: '最大值',
|
|
89
|
+
precision: '小数位数',
|
|
90
|
+
length: '长度',
|
|
91
|
+
width: '图像宽度',
|
|
92
|
+
height: '图像高度',
|
|
93
|
+
image_format: '图像格式',
|
|
94
|
+
folder: '文件夹路径',
|
|
95
|
+
extensions: '扩展名筛选',
|
|
96
|
+
mask: '号码格式',
|
|
97
|
+
start_year: '起始年份',
|
|
98
|
+
end_year: '结束年份',
|
|
99
|
+
start_date: '开始日期',
|
|
100
|
+
end_date: '结束日期',
|
|
101
|
+
all_day: '一整天',
|
|
102
|
+
start_time: '开始时间',
|
|
103
|
+
end_time: '结束时间',
|
|
104
|
+
weekdays: '星期',
|
|
105
|
+
choices: '候选值',
|
|
106
|
+
weighted_choices: '加权候选值',
|
|
107
|
+
pattern: '正则表达式',
|
|
108
|
+
regex: '正则表达式',
|
|
109
|
+
template: '模板',
|
|
110
|
+
sequence_start: '序列起始值',
|
|
111
|
+
sequence_step: '序列步长',
|
|
112
|
+
schema: 'JSON 结构',
|
|
113
|
+
value: '固定值',
|
|
114
|
+
n: '数量',
|
|
115
|
+
num_words: '单词数',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export function paramLabel(p) {
|
|
119
|
+
return PARAM_LABELS[p] || p;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 生成器分类(参考工具 式分组下拉,严格对照 示例UI/生成数据类型/ 的 7 组)。
|
|
123
|
+
// 顺序即下拉展示顺序;未收录的生成器自动落入末尾「其他」组。
|
|
124
|
+
// 空 gens 的组(支付 / 产品)是 参考工具 有、sqlseed 尚未实现的占位组:
|
|
125
|
+
// 下拉里显示为禁用项,等 P2 生成器落地后自动变为可选(无需改本文件)。
|
|
126
|
+
export const GEN_CATEGORIES = [
|
|
127
|
+
{
|
|
128
|
+
title: '通用',
|
|
129
|
+
gens: ['integer', 'float', 'boolean', 'date', 'datetime', 'timestamp',
|
|
130
|
+
'choice', 'weighted_choice', 'text', 'string', 'sentence', 'word',
|
|
131
|
+
'pattern', 'template', 'uuid', 'json', 'bytes', 'time',
|
|
132
|
+
'skip', 'foreign_key', 'foreign_key_or_integer', 'autoincrement'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
title: '个人',
|
|
136
|
+
gens: ['name', 'first_name', 'last_name', 'username', 'password',
|
|
137
|
+
'email', 'phone', 'job_title'],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
// 参考工具:支付方式 / 信用卡类型 / 信用卡卡号 / 信用卡日期(P2)
|
|
141
|
+
title: '支付',
|
|
142
|
+
gens: [],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
title: '商业',
|
|
146
|
+
gens: ['company', 'catch_phrase'],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
title: '位置',
|
|
150
|
+
gens: ['address', 'city', 'state', 'country', 'zip_code', 'country_code'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
// 参考工具:产品名称 / 产品类别 / 颜色 / 尺寸 / 重量单位 / 条码 / SKU(P2)
|
|
154
|
+
title: '产品',
|
|
155
|
+
gens: [],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
title: '电脑',
|
|
159
|
+
gens: ['url', 'ipv4'],
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
/** 占位组在下拉里显示的提示文案。 */
|
|
164
|
+
export const PENDING_GROUP_HINT = '(暂无生成器)';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 把 meta.names(全部生成器)按分类分组。
|
|
168
|
+
* 返回 [{title, names, pending}]——pending 表示该组在 参考工具 中存在但
|
|
169
|
+
* sqlseed 尚无对应生成器(支付/产品),调用方应渲染为禁用项。
|
|
170
|
+
* 兜底「其他」组保证不丢项。
|
|
171
|
+
*/
|
|
172
|
+
export function groupGenerators(names) {
|
|
173
|
+
const remaining = new Set(names);
|
|
174
|
+
const groups = [];
|
|
175
|
+
for (const cat of GEN_CATEGORIES) {
|
|
176
|
+
const hit = cat.gens.filter((g) => remaining.has(g));
|
|
177
|
+
for (const g of hit) remaining.delete(g);
|
|
178
|
+
groups.push({ title: cat.title, names: hit, pending: hit.length === 0 });
|
|
179
|
+
}
|
|
180
|
+
if (remaining.size) {
|
|
181
|
+
groups.push({ title: '其他', names: [...remaining].sort((left, right) => left < right ? -1 : Number(left > right)), pending: false });
|
|
182
|
+
}
|
|
183
|
+
return groups;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 列的树节点语义标注:优先外键/自增,其次生成器语义。
|
|
187
|
+
export function colAnnotation(col, spec, fkCols) {
|
|
188
|
+
if (fkCols.has(col.name)) return '外键';
|
|
189
|
+
if (col.is_primary_key && col.is_autoincrement) return '序列';
|
|
190
|
+
if (spec?.generator_name && spec.generator_name !== 'skip') {
|
|
191
|
+
return genLabel(spec.generator_name);
|
|
192
|
+
}
|
|
193
|
+
if (spec?.generator_name === 'skip' && !col.is_primary_key) return '默认值';
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// 主工作台(参考工具 主窗口式三栏):
|
|
2
|
+
// 左:连接/表树(含分组标识)
|
|
3
|
+
// 中:数据网格(分页 + SQL 控制台折叠)
|
|
4
|
+
// 右:元数据面板(行数/列数/外键/索引/映射摘要)
|
|
5
|
+
|
|
6
|
+
import { h, get, clear, msg, table, fmt, store, setConnBadge, rememberConnId } from '../api.js';
|
|
7
|
+
let current = {
|
|
8
|
+
connId: null,
|
|
9
|
+
table: null
|
|
10
|
+
};
|
|
11
|
+
let selectionVersion = 0;
|
|
12
|
+
let treeVersion = 0;
|
|
13
|
+
export function render() {
|
|
14
|
+
selectionVersion++;
|
|
15
|
+
treeVersion++;
|
|
16
|
+
const root = h('div', {
|
|
17
|
+
class: 'browse'
|
|
18
|
+
});
|
|
19
|
+
root.append(h('div', {
|
|
20
|
+
class: 'browse-left',
|
|
21
|
+
id: 'browse-left'
|
|
22
|
+
}, h('div', {
|
|
23
|
+
class: 'loading'
|
|
24
|
+
}, '加载连接…')), h('div', {
|
|
25
|
+
class: 'browse-center',
|
|
26
|
+
id: 'browse-center'
|
|
27
|
+
}, h('div', {
|
|
28
|
+
class: 'muted',
|
|
29
|
+
style: 'padding:24px'
|
|
30
|
+
}, '在左侧选择一张表查看数据。')), h('div', {
|
|
31
|
+
class: 'browse-right',
|
|
32
|
+
id: 'browse-right'
|
|
33
|
+
}, h('div', {
|
|
34
|
+
class: 'muted',
|
|
35
|
+
style: 'padding:24px'
|
|
36
|
+
}, '表元数据。')));
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
export async function mount() {
|
|
40
|
+
await loadLeft();
|
|
41
|
+
}
|
|
42
|
+
async function loadLeft() {
|
|
43
|
+
const left = document.getElementById('browse-left');
|
|
44
|
+
if (!left) return;
|
|
45
|
+
const version = ++treeVersion;
|
|
46
|
+
const isCurrent = () => version === treeVersion && left.isConnected;
|
|
47
|
+
let conns;
|
|
48
|
+
try {
|
|
49
|
+
conns = await get('/api/connections');
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isCurrent()) {
|
|
52
|
+
clear(left);
|
|
53
|
+
left.append(msg(error.message));
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!isCurrent()) return;
|
|
58
|
+
clear(left);
|
|
59
|
+
if (!conns.connections.length) {
|
|
60
|
+
left.append(msg('暂无连接。先在「数据库连接」页打开数据库。', 'warn'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const c of conns.connections) {
|
|
64
|
+
let group;
|
|
65
|
+
if (c.group_size > 1) {
|
|
66
|
+
group = h('span', {
|
|
67
|
+
class: 'pill'
|
|
68
|
+
}, c.group_index === 1 ? `主 ${c.group_index}/${c.group_size}` : `并 ${c.group_index}/${c.group_size}`);
|
|
69
|
+
} else {
|
|
70
|
+
group = null;
|
|
71
|
+
}
|
|
72
|
+
const connNode = h('div', {
|
|
73
|
+
class: 'tree-row table-row'
|
|
74
|
+
}, h('span', {
|
|
75
|
+
class: 'tree-icon'
|
|
76
|
+
}, '🗄'), h('span', {
|
|
77
|
+
class: 'tree-name'
|
|
78
|
+
}, (c.target || '').split('/').pop()), group);
|
|
79
|
+
const tablesNode = h('div', {}, h('div', {
|
|
80
|
+
class: 'loading'
|
|
81
|
+
}, '加载表…'));
|
|
82
|
+
left.append(connNode, tablesNode);
|
|
83
|
+
c.tablesNode = tablesNode;
|
|
84
|
+
}
|
|
85
|
+
await Promise.all(conns.connections.map(async c => {
|
|
86
|
+
try {
|
|
87
|
+
const detail = await get(`/api/connections/${c.conn_id}/tables`);
|
|
88
|
+
if (!isCurrent()) return;
|
|
89
|
+
clear(c.tablesNode);
|
|
90
|
+
for (const t of detail.tables) {
|
|
91
|
+
c.tablesNode.append(h('div', {
|
|
92
|
+
class: `tree-row col-row${current.connId === c.conn_id && current.table === t.name ? ' selected' : ''}`,
|
|
93
|
+
onclick: () => selectTable(c.conn_id, t.name)
|
|
94
|
+
}, h('span', {
|
|
95
|
+
class: 'tree-arrow'
|
|
96
|
+
}, ''), h('span', {
|
|
97
|
+
class: 'tree-icon col-icon'
|
|
98
|
+
}, '▦'), h('span', {
|
|
99
|
+
class: 'tree-name'
|
|
100
|
+
}, t.name), h('span', {
|
|
101
|
+
class: 'muted tree-count'
|
|
102
|
+
}, String(t.row_count))));
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (isCurrent()) {
|
|
106
|
+
clear(c.tablesNode);
|
|
107
|
+
c.tablesNode.append(msg(error.message));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
async function selectTable(connId, tableName) {
|
|
113
|
+
const center = document.getElementById('browse-center');
|
|
114
|
+
const right = document.getElementById('browse-right');
|
|
115
|
+
if (!center || !right) return;
|
|
116
|
+
const version = ++selectionVersion;
|
|
117
|
+
const isCurrent = () => version === selectionVersion && center.isConnected && right.isConnected;
|
|
118
|
+
clear(center);
|
|
119
|
+
clear(right);
|
|
120
|
+
center.append(h('div', {
|
|
121
|
+
class: 'loading'
|
|
122
|
+
}, '加载数据…'));
|
|
123
|
+
try {
|
|
124
|
+
const [detail, schema, mapping, rows] = await Promise.all([get(`/api/connections/${connId}/tables`), get(`/api/connections/${connId}/tables/${encodeURIComponent(tableName)}/schema`), get(`/api/connections/${connId}/tables/${encodeURIComponent(tableName)}/mapping`), get(`/api/connections/${connId}/tables/${encodeURIComponent(tableName)}/rows?limit=50&offset=0`)]);
|
|
125
|
+
if (!isCurrent()) return;
|
|
126
|
+
current = {
|
|
127
|
+
connId,
|
|
128
|
+
table: tableName
|
|
129
|
+
};
|
|
130
|
+
store.connId = connId;
|
|
131
|
+
store.target = detail.target;
|
|
132
|
+
store.tables = detail.tables;
|
|
133
|
+
rememberConnId(connId);
|
|
134
|
+
setConnBadge();
|
|
135
|
+
renderCenter(center, connId, tableName, rows);
|
|
136
|
+
renderRight(right, tableName, schema, mapping);
|
|
137
|
+
await loadLeft();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (isCurrent()) {
|
|
140
|
+
clear(center);
|
|
141
|
+
center.append(msg(error.message));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function renderCenter(center, connId, tableName, rowsRes) {
|
|
146
|
+
clear(center);
|
|
147
|
+
const cols = Object.keys(rowsRes.rows[0] || {
|
|
148
|
+
'(空表)': ''
|
|
149
|
+
});
|
|
150
|
+
center.append(h('div', {
|
|
151
|
+
class: 'row'
|
|
152
|
+
}, h('div', {
|
|
153
|
+
class: 'genform-title'
|
|
154
|
+
}, tableName), h('span', {
|
|
155
|
+
class: 'muted'
|
|
156
|
+
}, `共 ${rowsRes.total} 行`), h('span', {
|
|
157
|
+
style: 'flex:1'
|
|
158
|
+
}), h('button', {
|
|
159
|
+
class: 'small',
|
|
160
|
+
onclick: () => selectTable(connId, tableName)
|
|
161
|
+
}, '刷新')), h('div', {
|
|
162
|
+
class: 'table-scroll',
|
|
163
|
+
id: 'grid'
|
|
164
|
+
}, table(cols, rowsRes.rows.map(r => cols.map(c => fmt(r[c]))), {
|
|
165
|
+
monoCols: cols.map((_, i) => i)
|
|
166
|
+
})));
|
|
167
|
+
}
|
|
168
|
+
function renderRight(right, tableName, schema, mapping) {
|
|
169
|
+
clear(right);
|
|
170
|
+
right.append(h('div', {
|
|
171
|
+
class: 'meta-head'
|
|
172
|
+
}, h('div', {
|
|
173
|
+
class: 'meta-icon'
|
|
174
|
+
}, '▦'), h('div', {}, h('div', {
|
|
175
|
+
class: 'genform-title'
|
|
176
|
+
}, tableName), h('div', {
|
|
177
|
+
class: 'muted'
|
|
178
|
+
}, '表'))), h('div', {
|
|
179
|
+
class: 'meta-kv'
|
|
180
|
+
}, h('div', {}, h('div', {
|
|
181
|
+
class: 'muted'
|
|
182
|
+
}, '行数'), h('div', {
|
|
183
|
+
class: 'stat'
|
|
184
|
+
}, schema.row_count)), h('div', {}, h('div', {
|
|
185
|
+
class: 'muted'
|
|
186
|
+
}, '列数'), h('div', {
|
|
187
|
+
class: 'stat'
|
|
188
|
+
}, schema.columns.length)), h('div', {}, h('div', {
|
|
189
|
+
class: 'muted'
|
|
190
|
+
}, '外键'), h('div', {
|
|
191
|
+
class: 'stat'
|
|
192
|
+
}, (schema.foreign_keys || []).length))), h('h3', {
|
|
193
|
+
class: 'section-title'
|
|
194
|
+
}, '列映射'), ...Object.entries(mapping.mapping).map(([col, spec]) => h('div', {
|
|
195
|
+
class: 'meta-map-row'
|
|
196
|
+
}, h('span', {
|
|
197
|
+
class: 'mono'
|
|
198
|
+
}, col), h('span', {
|
|
199
|
+
class: `pill ${spec.generator_name === 'skip' ? 'warn' : 'gen'}`
|
|
200
|
+
}, spec.generator_name))), ...((schema.foreign_keys || []).length ? [h('h3', {
|
|
201
|
+
class: 'section-title'
|
|
202
|
+
}, '外键')] : []), ...(schema.foreign_keys || []).map(fk => h('div', {
|
|
203
|
+
class: 'meta-map-row'
|
|
204
|
+
}, h('span', {
|
|
205
|
+
class: 'mono'
|
|
206
|
+
}, fk.column), h('span', {
|
|
207
|
+
class: 'muted'
|
|
208
|
+
}, `→ ${fk.ref_table}.${fk.ref_column}`))));
|
|
209
|
+
}
|