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,94 @@
|
|
|
1
|
+
/** Keep one vertical scroll owner; the table stays intact for column alignment. */
|
|
2
|
+
export function createPreviewScrollLayout({dialog,results,inline=false}) {
|
|
3
|
+
const {el,body}=dialog;
|
|
4
|
+
let table=null,closed=false,frame=null;
|
|
5
|
+
const number=value=>Number.parseFloat(value)||0;
|
|
6
|
+
const outerHeight=node=>{
|
|
7
|
+
const style=getComputedStyle(node);
|
|
8
|
+
return node.getBoundingClientRect().height+number(style.marginTop)+number(style.marginBottom);
|
|
9
|
+
};
|
|
10
|
+
function captureVertical() {
|
|
11
|
+
if(closed || inline || !table || typeof getComputedStyle!=='function')return null;
|
|
12
|
+
// Store one position per table, independent of the current scroll owner.
|
|
13
|
+
// A body's migrated table offset must not also become shared tab state.
|
|
14
|
+
return {tableTop:(table.scrollTop||0)+(body.scrollTop||0),bodyTop:0};
|
|
15
|
+
}
|
|
16
|
+
function restoreVertical(tableTop=0,bodyTop=0) {
|
|
17
|
+
if(closed || inline || !table || typeof getComputedStyle!=='function')return;
|
|
18
|
+
const ownScroll=el.classList.contains('wb-preview-table-scroll');
|
|
19
|
+
const position=tableTop+bodyTop;
|
|
20
|
+
body.scrollTop=ownScroll?0:position;
|
|
21
|
+
table.scrollTop=ownScroll?position:0;
|
|
22
|
+
}
|
|
23
|
+
function update(preserve=true) {
|
|
24
|
+
if(closed || inline || !table || !el.isConnected || typeof getComputedStyle!=='function')return;
|
|
25
|
+
const modalStyle=getComputedStyle(el),bodyStyle=getComputedStyle(body),resultStyle=getComputedStyle(results);
|
|
26
|
+
// Use the modal's allowed height, not its current content height, so changing
|
|
27
|
+
// the scroll owner cannot make this measurement oscillate.
|
|
28
|
+
const bodyHeight=number(modalStyle.maxHeight)
|
|
29
|
+
-number(modalStyle.paddingTop)-number(modalStyle.paddingBottom)
|
|
30
|
+
-number(modalStyle.borderTopWidth)-number(modalStyle.borderBottomWidth)
|
|
31
|
+
-[...el.children].filter(node=>node!==body).reduce((height,node)=>height+outerHeight(node),0)
|
|
32
|
+
-number(bodyStyle.marginTop)-number(bodyStyle.marginBottom)
|
|
33
|
+
-number(bodyStyle.borderTopWidth)-number(bodyStyle.borderBottomWidth);
|
|
34
|
+
const above=table.getBoundingClientRect().top-body.getBoundingClientRect().top+(body.scrollTop||0)-(body.clientTop||0);
|
|
35
|
+
const below=number(bodyStyle.paddingBottom)+number(resultStyle.paddingBottom)
|
|
36
|
+
+number(resultStyle.borderBottomWidth)+number(resultStyle.marginBottom);
|
|
37
|
+
const available=Math.floor(bodyHeight-above-below);
|
|
38
|
+
const header=table.querySelector('thead');
|
|
39
|
+
const rows=[...(table.querySelector('tbody')?.children || [])].slice(0,3);
|
|
40
|
+
const minimum=(header?.getBoundingClientRect().height || 0)
|
|
41
|
+
+rows.reduce((height,row)=>height+row.getBoundingClientRect().height,0)
|
|
42
|
+
+table.offsetHeight-table.clientHeight;
|
|
43
|
+
const width=document.documentElement?.clientWidth || innerWidth;
|
|
44
|
+
// Without size observation a later error could overflow a fixed body.
|
|
45
|
+
const ownScroll=Boolean(observer && width>600 && minimum>0 && available>=minimum);
|
|
46
|
+
const changed=el.classList.contains('wb-preview-table-scroll')!==ownScroll;
|
|
47
|
+
const position=(body.scrollTop||0)+(table.scrollTop||0);
|
|
48
|
+
const limit=ownScroll?`${available}px`:'';
|
|
49
|
+
if(table.style.maxHeight!==limit)table.style.maxHeight=limit;
|
|
50
|
+
el.classList.toggle('wb-preview-table-scroll',ownScroll);
|
|
51
|
+
if(changed && preserve) {
|
|
52
|
+
// A resize may change owners without rebuilding the preview. Carry its
|
|
53
|
+
// vertical offset across instead of letting the old owner clamp it to 0.
|
|
54
|
+
restoreVertical(position);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function schedule() {
|
|
58
|
+
if(closed || frame!==null)return;
|
|
59
|
+
if(typeof requestAnimationFrame!=='function'){update();return;}
|
|
60
|
+
frame=requestAnimationFrame(()=>{frame=null;update();});
|
|
61
|
+
}
|
|
62
|
+
const observer=!inline && typeof ResizeObserver!=='undefined'?new ResizeObserver(schedule):null;
|
|
63
|
+
function setTable(node) {
|
|
64
|
+
if(closed)return;
|
|
65
|
+
if(table && table!==node)table.style.maxHeight='';
|
|
66
|
+
table=node;
|
|
67
|
+
observer?.disconnect();
|
|
68
|
+
if(inline) {
|
|
69
|
+
table?.classList.toggle('wb-preview-long',(table.querySelector('tbody')?.children.length || 0)>10);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
el.classList.remove('wb-preview-table-scroll');
|
|
73
|
+
if(!table)return;
|
|
74
|
+
// Direct children include status, errors and wrapping table tabs. Observe
|
|
75
|
+
// their actual heights as well as the rows used for the readable minimum.
|
|
76
|
+
for(const node of [el,...el.children,...body.children,table.querySelector('thead'),
|
|
77
|
+
...[...(table.querySelector('tbody')?.children || [])].slice(0,3)]) {
|
|
78
|
+
if(node)observer?.observe(node);
|
|
79
|
+
}
|
|
80
|
+
update(false);
|
|
81
|
+
}
|
|
82
|
+
if(!inline)window.addEventListener('resize',schedule);
|
|
83
|
+
function destroy() {
|
|
84
|
+
closed=true;
|
|
85
|
+
observer?.disconnect();
|
|
86
|
+
if(frame!==null)cancelAnimationFrame(frame);
|
|
87
|
+
frame=null;
|
|
88
|
+
if(!inline)window.removeEventListener('resize',schedule);
|
|
89
|
+
if(table)table.style.maxHeight='';
|
|
90
|
+
el.classList.remove('wb-preview-table-scroll');
|
|
91
|
+
table=null;
|
|
92
|
+
}
|
|
93
|
+
return {setTable,update,captureVertical,restoreVertical,destroy};
|
|
94
|
+
}
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { h } from '../api.js';
|
|
2
|
+
import { createDropdown } from '../dropdown.js';
|
|
3
|
+
import { modal, button, valueText } from './ui.js';
|
|
4
|
+
import { createPreviewScrollLayout } from './preview-scroll-layout.js';
|
|
5
|
+
let nextPreviewId = 0;
|
|
6
|
+
const validCount = value => /^\d+$/.test(String(value)) && Number(value) >= 1 && Number(value) <= 100;
|
|
7
|
+
|
|
8
|
+
/** Readonly preview state stays local; the caller owns configuration and transport. */
|
|
9
|
+
export function openDataPreview({
|
|
10
|
+
tables,
|
|
11
|
+
currentTable,
|
|
12
|
+
selectedTables,
|
|
13
|
+
initialScope = 'current',
|
|
14
|
+
initialCount = 10,
|
|
15
|
+
generate,
|
|
16
|
+
guard = task => task,
|
|
17
|
+
isCurrent = () => true,
|
|
18
|
+
onOptionsChange,
|
|
19
|
+
onResult,
|
|
20
|
+
onError,
|
|
21
|
+
onColumnAction,
|
|
22
|
+
container = null,
|
|
23
|
+
fixedScope = false,
|
|
24
|
+
initialResult = null,
|
|
25
|
+
initialView = null,
|
|
26
|
+
initialStale = false
|
|
27
|
+
}) {
|
|
28
|
+
function showPreviewLoading() {
|
|
29
|
+
if (!result) {
|
|
30
|
+
scrollLayout.setTable(null);
|
|
31
|
+
results.replaceChildren(h('p', {
|
|
32
|
+
class: 'wb-preview-loading'
|
|
33
|
+
}, '正在生成预览数据…'));
|
|
34
|
+
}
|
|
35
|
+
if (result) {
|
|
36
|
+
if (stale) {
|
|
37
|
+
status.textContent = '正在更新;规则已改变,当前为旧样例。';
|
|
38
|
+
} else {
|
|
39
|
+
status.textContent = '正在更新,当前为上次结果。';
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
status.textContent = '正在生成预览数据…';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const id = `wb-preview-${++nextPreviewId}`;
|
|
47
|
+
const schema = new Map(tables.map(table => [table.name, table]));
|
|
48
|
+
const selected = [...new Set(selectedTables)];
|
|
49
|
+
const tableScroll = new Map(Object.entries(initialView?.tableScroll || {}));
|
|
50
|
+
let scope = initialScope === 'selected' ? 'selected' : 'current';
|
|
51
|
+
let count;
|
|
52
|
+
if (validCount(initialView?.count)) {
|
|
53
|
+
count = Number(initialView.count);
|
|
54
|
+
} else if (validCount(initialCount)) {
|
|
55
|
+
count = Number(initialCount);
|
|
56
|
+
} else {
|
|
57
|
+
count = 10;
|
|
58
|
+
}
|
|
59
|
+
let closed = false,
|
|
60
|
+
busy = false,
|
|
61
|
+
sequence = 0,
|
|
62
|
+
scopeControl = null,
|
|
63
|
+
result = null,
|
|
64
|
+
shownTable = initialView?.shownTable || currentTable;
|
|
65
|
+
let selectedColumn = initialView?.column || null,
|
|
66
|
+
stale = initialStale;
|
|
67
|
+
let columnAction = initialView?.columnAction || 'information';
|
|
68
|
+
let scrollLayout = null;
|
|
69
|
+
const destroy = () => {
|
|
70
|
+
closed = true;
|
|
71
|
+
sequence++;
|
|
72
|
+
scopeControl?.destroy();
|
|
73
|
+
scrollLayout?.destroy();
|
|
74
|
+
};
|
|
75
|
+
let dialog;
|
|
76
|
+
if (container) {
|
|
77
|
+
dialog = {
|
|
78
|
+
el: container,
|
|
79
|
+
body: container,
|
|
80
|
+
close: destroy
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
dialog = modal(scope === 'selected' ? '预览已选表' : '预览数据', {
|
|
84
|
+
wide: true,
|
|
85
|
+
onClose: destroy
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
dialog.el.classList.add('wb-data-preview');
|
|
89
|
+
const help = h('p', {
|
|
90
|
+
id: `${id}-help`,
|
|
91
|
+
class: 'wb-preview-help'
|
|
92
|
+
}, '按当前规则生成临时记录,不写入数据库。正式生成时会重新取值。');
|
|
93
|
+
const status = h('p', {
|
|
94
|
+
class: 'wb-preview-status',
|
|
95
|
+
role: 'status',
|
|
96
|
+
'aria-live': 'polite'
|
|
97
|
+
}, '点击“生成预览”查看当前规则的效果。');
|
|
98
|
+
const error = h('p', {
|
|
99
|
+
id: `${id}-error`,
|
|
100
|
+
class: 'wb-preview-error',
|
|
101
|
+
role: 'alert'
|
|
102
|
+
});
|
|
103
|
+
const issues = h('div', {
|
|
104
|
+
class: 'wb-preview-issues'
|
|
105
|
+
});
|
|
106
|
+
const tabs = h('div', {
|
|
107
|
+
class: 'wb-preview-tables',
|
|
108
|
+
role: 'group',
|
|
109
|
+
'aria-label': '切换预览表'
|
|
110
|
+
});
|
|
111
|
+
const results = h('section', {
|
|
112
|
+
class: 'wb-preview-results',
|
|
113
|
+
'aria-label': '预览记录'
|
|
114
|
+
});
|
|
115
|
+
scrollLayout = createPreviewScrollLayout({
|
|
116
|
+
dialog,
|
|
117
|
+
results,
|
|
118
|
+
inline: Boolean(container)
|
|
119
|
+
});
|
|
120
|
+
const scopeHolder = h('div', {
|
|
121
|
+
class: 'wb-preview-field'
|
|
122
|
+
}, h('span', {}, '预览范围'));
|
|
123
|
+
const scopeSlot = h('div', {});
|
|
124
|
+
scopeHolder.append(scopeSlot);
|
|
125
|
+
const countInput = h('input', {
|
|
126
|
+
type: 'number',
|
|
127
|
+
min: 1,
|
|
128
|
+
max: 100,
|
|
129
|
+
step: 1,
|
|
130
|
+
value: String(count),
|
|
131
|
+
'aria-label': '每表预览行数',
|
|
132
|
+
'data-db-action': '',
|
|
133
|
+
'aria-describedby': `${id}-count-help ${id}-error`
|
|
134
|
+
});
|
|
135
|
+
const countField = h('label', {
|
|
136
|
+
class: 'wb-preview-field'
|
|
137
|
+
}, h('span', {}, '每表预览行数'), countInput);
|
|
138
|
+
const controls = h('div', {
|
|
139
|
+
class: 'wb-preview-controls'
|
|
140
|
+
}, ...(fixedScope ? [] : [scopeHolder]), countField);
|
|
141
|
+
function live() {
|
|
142
|
+
return !closed && dialog.el.isConnected && isCurrent();
|
|
143
|
+
}
|
|
144
|
+
function renderScope() {
|
|
145
|
+
scopeControl?.destroy();
|
|
146
|
+
if (fixedScope) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
scopeControl = createDropdown({
|
|
150
|
+
label: '预览范围',
|
|
151
|
+
value: scope,
|
|
152
|
+
options: [{
|
|
153
|
+
value: 'current',
|
|
154
|
+
label: `当前表 · ${currentTable || '未选择'}`
|
|
155
|
+
}, {
|
|
156
|
+
value: 'selected',
|
|
157
|
+
label: `已选表 · ${selected.length} 张`
|
|
158
|
+
}],
|
|
159
|
+
onChange: value => {
|
|
160
|
+
if (closed || busy) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
scope = value;
|
|
164
|
+
shownTable = scope === 'current' ? currentTable : selected[0];
|
|
165
|
+
selectedColumn = null;
|
|
166
|
+
optionsChanged();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
scopeSlot.replaceChildren(scopeControl.el);
|
|
170
|
+
}
|
|
171
|
+
function countValid() {
|
|
172
|
+
if (!validCount(countInput.value)) {
|
|
173
|
+
countInput.setAttribute('aria-invalid', 'true');
|
|
174
|
+
error.textContent = '每表预览行数必须是 1–100 之间的整数。';
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
countInput.removeAttribute('aria-invalid');
|
|
178
|
+
count = Number(countInput.value);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function optionsChanged() {
|
|
182
|
+
if (closed || busy) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
scrollLayout.setTable(null);
|
|
186
|
+
sequence++;
|
|
187
|
+
result = null;
|
|
188
|
+
stale = false;
|
|
189
|
+
tableScroll.clear();
|
|
190
|
+
error.textContent = '';
|
|
191
|
+
results.replaceChildren();
|
|
192
|
+
tabs.replaceChildren();
|
|
193
|
+
issues.replaceChildren();
|
|
194
|
+
status.textContent = '设置已改变,请重新预览。';
|
|
195
|
+
if (countValid()) {
|
|
196
|
+
onOptionsChange?.({
|
|
197
|
+
scope,
|
|
198
|
+
count
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
countInput.addEventListener('input', optionsChanged);
|
|
203
|
+
function setBusy(value) {
|
|
204
|
+
busy = value;
|
|
205
|
+
countInput.disabled = value;
|
|
206
|
+
refreshButton.disabled = value;
|
|
207
|
+
controls.setAttribute('aria-busy', String(value));
|
|
208
|
+
results.setAttribute('aria-busy', String(value));
|
|
209
|
+
for (const tab of tabs.querySelectorAll('button')) {
|
|
210
|
+
tab.disabled = value;
|
|
211
|
+
}
|
|
212
|
+
for (const input of results.querySelectorAll('[data-preview-column]')) {
|
|
213
|
+
input.disabled = value;
|
|
214
|
+
}
|
|
215
|
+
if (value) {
|
|
216
|
+
// Close a floating list before disabling it; its options live outside
|
|
217
|
+
// this holder and must not remain interactive during the request.
|
|
218
|
+
scopeControl?.destroy();
|
|
219
|
+
if (scopeControl) {
|
|
220
|
+
scopeControl.el.querySelector('.dropdown-btn').disabled = true;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
renderScope();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function cellText(table, column, row) {
|
|
227
|
+
if (Object.hasOwn(row, column.name)) {
|
|
228
|
+
return {
|
|
229
|
+
text: valueText(row[column.name]),
|
|
230
|
+
placeholder: false
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (table?.omittedColumns?.[column.name]) {
|
|
234
|
+
return {
|
|
235
|
+
text: table.omittedColumns[column.name],
|
|
236
|
+
placeholder: true
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (column.is_autoincrement || column.is_rowid_alias) {
|
|
240
|
+
return {
|
|
241
|
+
text: '数据库分配',
|
|
242
|
+
placeholder: true
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (column.is_computed) {
|
|
246
|
+
return {
|
|
247
|
+
text: '数据库计算',
|
|
248
|
+
placeholder: true
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
text: '暂不可预览',
|
|
253
|
+
placeholder: true
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const viewport = node => ({
|
|
257
|
+
left: node.scrollLeft || 0,
|
|
258
|
+
top: node.scrollTop || 0
|
|
259
|
+
});
|
|
260
|
+
const bodyViewport = () => ({
|
|
261
|
+
...viewport(dialog.body),
|
|
262
|
+
top: scrollLayout.captureVertical()?.bodyTop ?? (dialog.body.scrollTop || 0)
|
|
263
|
+
});
|
|
264
|
+
function rememberTableScroll() {
|
|
265
|
+
const scroll = results.querySelector('.wb-preview-scroll');
|
|
266
|
+
if (scroll) {
|
|
267
|
+
tableScroll.set(shownTable, {
|
|
268
|
+
...viewport(scroll),
|
|
269
|
+
top: scrollLayout.captureVertical()?.tableTop ?? (scroll.scrollTop || 0)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function restoreScroll(bodyPosition) {
|
|
274
|
+
const scroll = results.querySelector('.wb-preview-scroll'),
|
|
275
|
+
tablePosition = tableScroll.get(shownTable);
|
|
276
|
+
if (scroll) {
|
|
277
|
+
restoreViewport(scroll, tablePosition);
|
|
278
|
+
}
|
|
279
|
+
restoreViewport(dialog.body, bodyPosition);
|
|
280
|
+
scrollLayout.restoreVertical(tablePosition?.top || 0, bodyPosition?.top || 0);
|
|
281
|
+
}
|
|
282
|
+
function getView() {
|
|
283
|
+
rememberTableScroll();
|
|
284
|
+
return {
|
|
285
|
+
shownTable,
|
|
286
|
+
column: selectedColumn,
|
|
287
|
+
columnAction,
|
|
288
|
+
count,
|
|
289
|
+
scope,
|
|
290
|
+
tableScroll: Object.fromEntries(tableScroll),
|
|
291
|
+
bodyScroll: bodyViewport(),
|
|
292
|
+
result,
|
|
293
|
+
stale
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function columnHeader(table, column) {
|
|
297
|
+
const metadata = [column.type];
|
|
298
|
+
if (column.is_primary_key || table?.primary_key?.includes(column.name)) {
|
|
299
|
+
metadata.push('PK');
|
|
300
|
+
}
|
|
301
|
+
if (table?.foreign_keys?.some(key => key.columns?.includes(column.name))) {
|
|
302
|
+
metadata.push('FK');
|
|
303
|
+
}
|
|
304
|
+
if (typeof column.nullable === 'boolean') {
|
|
305
|
+
metadata.push(column.nullable ? '允许 NULL' : 'NOT NULL');
|
|
306
|
+
}
|
|
307
|
+
const label = h('span', {
|
|
308
|
+
class: 'wb-preview-column-name'
|
|
309
|
+
}, column.name),
|
|
310
|
+
details = h('small', {
|
|
311
|
+
class: 'wb-preview-column-meta'
|
|
312
|
+
}, metadata.filter(Boolean).join(' · '));
|
|
313
|
+
if (!onColumnAction) {
|
|
314
|
+
return h('th', {
|
|
315
|
+
scope: 'col'
|
|
316
|
+
}, label, details);
|
|
317
|
+
}
|
|
318
|
+
const activate = action => () => {
|
|
319
|
+
if (!live() || busy) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
selectedColumn = column.name;
|
|
323
|
+
columnAction = action;
|
|
324
|
+
for (const input of results.querySelectorAll('[data-preview-column]')) {
|
|
325
|
+
input.setAttribute('aria-pressed', String(input.dataset.previewColumn === selectedColumn));
|
|
326
|
+
}
|
|
327
|
+
return onColumnAction(action, {
|
|
328
|
+
table: shownTable,
|
|
329
|
+
column: column.name,
|
|
330
|
+
view: getView()
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
const attributes = action => ({
|
|
334
|
+
plain: true,
|
|
335
|
+
class: `wb-preview-column-button wb-preview-column-${action}`,
|
|
336
|
+
'data-preview-column': column.name,
|
|
337
|
+
'data-preview-entry': action,
|
|
338
|
+
'aria-pressed': String(column.name === selectedColumn),
|
|
339
|
+
'aria-haspopup': 'dialog',
|
|
340
|
+
disabled: busy
|
|
341
|
+
});
|
|
342
|
+
const name = button('', activate('information'), {
|
|
343
|
+
...attributes('information'),
|
|
344
|
+
'aria-label': `查看 ${shownTable}.${column.name} 的字段信息`,
|
|
345
|
+
title: '查看字段信息'
|
|
346
|
+
});
|
|
347
|
+
name.append(label);
|
|
348
|
+
const rule = table?.ruleSummaries?.[column.name];
|
|
349
|
+
const properties = button(`规则:${rule?.label || '取值规则'}`, activate('rule'), {
|
|
350
|
+
...attributes('rule'),
|
|
351
|
+
'aria-label': `调整 ${shownTable}.${column.name} 的生成规则`,
|
|
352
|
+
title: rule?.detail || '调整生成规则'
|
|
353
|
+
});
|
|
354
|
+
details.title = '数据库结构';
|
|
355
|
+
return h('th', {
|
|
356
|
+
scope: 'col',
|
|
357
|
+
class: 'wb-preview-interactive-header'
|
|
358
|
+
}, name, details, properties);
|
|
359
|
+
}
|
|
360
|
+
function renderRows() {
|
|
361
|
+
if (!result) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const bodyScroll = bodyViewport();
|
|
365
|
+
const name = shownTable;
|
|
366
|
+
const table = schema.get(name);
|
|
367
|
+
const rows = Array.isArray(result.samples?.[name]) ? result.samples[name] : [];
|
|
368
|
+
const summary = h('p', {
|
|
369
|
+
class: 'wb-preview-summary'
|
|
370
|
+
}, `${name} · 实际展示 ${rows.length} 行 · 每表最多 ${count} 行`);
|
|
371
|
+
if (!rows.length) {
|
|
372
|
+
let empty;
|
|
373
|
+
if (result.preview_complete === false) {
|
|
374
|
+
empty = '本表暂未返回可预览记录,关联依赖数据可能尚未就绪。请查看依赖检查及上方提示。';
|
|
375
|
+
} else if (result.ok === false) {
|
|
376
|
+
empty = '本表预览未完成,请处理上方提示后重新预览。';
|
|
377
|
+
} else {
|
|
378
|
+
empty = '本表未返回预览记录。';
|
|
379
|
+
}
|
|
380
|
+
scrollLayout.setTable(null);
|
|
381
|
+
results.replaceChildren(summary, h('p', {
|
|
382
|
+
class: 'wb-preview-empty'
|
|
383
|
+
}, empty));
|
|
384
|
+
restoreViewport(dialog.body, bodyScroll);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const columns = table?.columns?.length ? table.columns : [...new Set(rows.flatMap(row => Object.keys(row)))].map(name => ({
|
|
388
|
+
name
|
|
389
|
+
}));
|
|
390
|
+
const data = h('table', {
|
|
391
|
+
class: 'wb-preview-data'
|
|
392
|
+
}, h('caption', {}, `${name} · 预览记录`), h('thead', {}, h('tr', {}, h('th', {
|
|
393
|
+
scope: 'col'
|
|
394
|
+
}, '#'), ...columns.map(column => columnHeader(table, column)))), h('tbody', {}, ...rows.map((row, index) => h('tr', {}, h('th', {
|
|
395
|
+
scope: 'row'
|
|
396
|
+
}, String(index + 1)), ...columns.map(column => {
|
|
397
|
+
const value = cellText(table, column, row);
|
|
398
|
+
return h('td', {
|
|
399
|
+
class: value.placeholder ? 'wb-preview-placeholder' : ''
|
|
400
|
+
}, value.text);
|
|
401
|
+
})))));
|
|
402
|
+
const scroll = h('div', {
|
|
403
|
+
class: 'wb-preview-scroll',
|
|
404
|
+
tabindex: 0,
|
|
405
|
+
'aria-label': `${name} 预览数据,可横向滚动`
|
|
406
|
+
}, data);
|
|
407
|
+
results.replaceChildren(summary, scroll);
|
|
408
|
+
scrollLayout.setTable(scroll);
|
|
409
|
+
restoreScroll(bodyScroll);
|
|
410
|
+
}
|
|
411
|
+
function renderResult() {
|
|
412
|
+
const bodyScroll = bodyViewport();
|
|
413
|
+
rememberTableScroll();
|
|
414
|
+
const names = scope === 'current' ? [currentTable] : [...new Set([...(result.order || []).filter(name => selected.includes(name)), ...selected])];
|
|
415
|
+
if (!names.includes(shownTable)) {
|
|
416
|
+
shownTable = names[0];
|
|
417
|
+
}
|
|
418
|
+
tabs.replaceChildren(...(scope === 'selected' ? names.map(name => button(name, () => {
|
|
419
|
+
if (closed || busy) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
rememberTableScroll();
|
|
423
|
+
shownTable = name;
|
|
424
|
+
selectedColumn = null;
|
|
425
|
+
for (const item of tabs.querySelectorAll('button')) {
|
|
426
|
+
item.setAttribute('aria-pressed', String(item.textContent === name));
|
|
427
|
+
}
|
|
428
|
+
renderRows();
|
|
429
|
+
}, {
|
|
430
|
+
small: true,
|
|
431
|
+
class: 'wb-preview-table-button',
|
|
432
|
+
'aria-pressed': String(name === shownTable)
|
|
433
|
+
})) : []));
|
|
434
|
+
function previewIssueList() {
|
|
435
|
+
if (result.issues?.length) {
|
|
436
|
+
return [h('ul', {}, ...result.issues.map(issue => h('li', {
|
|
437
|
+
class: issue.severity === 'error' ? 'wb-error' : 'wb-preview-warning'
|
|
438
|
+
}, [issue.table, issue.column].filter(Boolean).join('.') + (issue.table || issue.column ? ':' : '') + String(issue.message || '预览存在待处理项'))))];
|
|
439
|
+
} else {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
issues.replaceChildren(...previewIssueList());
|
|
444
|
+
renderRows();
|
|
445
|
+
restoreScroll(bodyScroll);
|
|
446
|
+
}
|
|
447
|
+
const refresh = guard(async () => {
|
|
448
|
+
if (closed || busy) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
error.textContent = '';
|
|
452
|
+
if (!live()) {
|
|
453
|
+
error.textContent = '配置已变化,请重新打开预览。';
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!countValid()) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (scope === 'selected' && !selected.length) {
|
|
460
|
+
error.textContent = '请先勾选需要预览的表,或切换到当前表。';
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (scope === 'current' && !schema.has(currentTable)) {
|
|
464
|
+
error.textContent = '请先选择当前表。';
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const request = ++sequence,
|
|
468
|
+
options = {
|
|
469
|
+
scope,
|
|
470
|
+
count,
|
|
471
|
+
table: currentTable
|
|
472
|
+
};
|
|
473
|
+
const stillCurrent = () => live() && request === sequence;
|
|
474
|
+
showPreviewLoading();
|
|
475
|
+
setBusy(true);
|
|
476
|
+
try {
|
|
477
|
+
let response;
|
|
478
|
+
try {
|
|
479
|
+
response = await generate(options);
|
|
480
|
+
} catch (error_) {
|
|
481
|
+
if (stillCurrent()) {
|
|
482
|
+
showPreviewFailure(error_);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!stillCurrent()) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (!response) {
|
|
490
|
+
error.textContent = '配置已变化或结果已过期,请重新打开预览。';
|
|
491
|
+
status.textContent = result ? '当前为上次结果,请重新打开预览。' : '';
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
acceptPreview(response);
|
|
495
|
+
return response;
|
|
496
|
+
} finally {
|
|
497
|
+
finishPreviewRequest();
|
|
498
|
+
}
|
|
499
|
+
function showPreviewFailure(error_) {
|
|
500
|
+
error.textContent = error_?.message || String(error_);
|
|
501
|
+
if (result) {
|
|
502
|
+
if (stale) {
|
|
503
|
+
status.textContent = '更新失败;规则已改变,当前为旧样例,请重新预览。';
|
|
504
|
+
} else {
|
|
505
|
+
status.textContent = '更新失败,当前为上次结果,可重新预览。';
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
status.textContent = '预览未完成,可重试。';
|
|
509
|
+
}
|
|
510
|
+
onError?.(error_);
|
|
511
|
+
}
|
|
512
|
+
function finishPreviewRequest() {
|
|
513
|
+
if (!closed && dialog.el.isConnected) {
|
|
514
|
+
if (!result) {
|
|
515
|
+
results.replaceChildren();
|
|
516
|
+
}
|
|
517
|
+
setBusy(false);
|
|
518
|
+
if (!isCurrent()) {
|
|
519
|
+
status.textContent = result ? '当前为上次结果,配置已变化。' : '';
|
|
520
|
+
error.textContent = '配置已变化,请重新打开预览。';
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function acceptPreview(response) {
|
|
525
|
+
result = response;
|
|
526
|
+
stale = false;
|
|
527
|
+
renderResult();
|
|
528
|
+
refreshButton.textContent = '重新预览';
|
|
529
|
+
if (response.ok === false) {
|
|
530
|
+
status.textContent = '预览存在待处理项,请查看下方提示。';
|
|
531
|
+
} else if (response.preview_complete === false) {
|
|
532
|
+
status.textContent = '已展示可预览数据;部分关联记录需等待依赖数据就绪。';
|
|
533
|
+
} else {
|
|
534
|
+
status.textContent = '预览数据已更新,数据库未写入。';
|
|
535
|
+
}
|
|
536
|
+
onResult?.(response, options);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
const refreshButton = button(container ? '生成预览' : '重新预览', refresh, {
|
|
540
|
+
glyph: 'refresh',
|
|
541
|
+
'data-db-action': ''
|
|
542
|
+
});
|
|
543
|
+
controls.append(refreshButton);
|
|
544
|
+
renderScope();
|
|
545
|
+
dialog.body.append(help, controls, h('p', {
|
|
546
|
+
id: `${id}-count-help`,
|
|
547
|
+
class: 'wb-preview-help'
|
|
548
|
+
}, '每表最多预览 1–100 行,且不超过该表配置的生成数量;此设置不会修改正式生成行数。'), status, error, issues, tabs, results);
|
|
549
|
+
if (dialog.actions) {
|
|
550
|
+
dialog.actions.append(button('关闭', dialog.close));
|
|
551
|
+
}
|
|
552
|
+
if (initialResult) {
|
|
553
|
+
result = initialResult;
|
|
554
|
+
renderResult();
|
|
555
|
+
refreshButton.textContent = '重新预览';
|
|
556
|
+
status.textContent = stale ? '规则已改变,当前为旧样例,请重新预览。' : '上次预览结果;重新预览可更新取值。';
|
|
557
|
+
}
|
|
558
|
+
restoreScroll(initialView?.bodyScroll);
|
|
559
|
+
return {
|
|
560
|
+
dialog,
|
|
561
|
+
refresh,
|
|
562
|
+
destroy,
|
|
563
|
+
getView
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function restoreViewport(node, position) {
|
|
567
|
+
if (!position) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
node.scrollLeft = Math.max(0, Math.min(position.left, (node.scrollWidth || 0) - (node.clientWidth || 0)));
|
|
571
|
+
node.scrollTop = Math.max(0, Math.min(position.top, (node.scrollHeight || 0) - (node.clientHeight || 0)));
|
|
572
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Verified against sqlseed's provider adapters and the linked upstream docs.
|
|
2
|
+
// Examples illustrate formats only; this module never runs a provider or edits
|
|
3
|
+
// the selected configuration. Recommendation is guidance, not a default change.
|
|
4
|
+
export function providerGuide(provider,locale='zh_CN') {
|
|
5
|
+
const chinese=String(locale).startsWith('zh');
|
|
6
|
+
const exampleNote='格式示例,非实时生成;实际结果取决于引擎、语言地区、字段规则与随机种子。';
|
|
7
|
+
const commonLimit='引擎负责生成取值;字段业务含义、唯一性与关联关系仍需通过规则、预览和依赖检查确认。';
|
|
8
|
+
if(provider==='faker')return {
|
|
9
|
+
id:provider,title:'Faker',choice:'自然格式,首次使用推荐',recommended:true,
|
|
10
|
+
summary:'适合姓名、地址、电话等自然格式的测试数据,支持多种语言与地区。',
|
|
11
|
+
features:['按语言与地区生成姓名、地址和电话,便于检查本地化页面。','支持常见语义生成器;需要更细控制时可使用 Faker 原生方法。'],
|
|
12
|
+
limits:['部分生成器未覆盖所选语言时,Faker 可能回退为英语内容。',commonLimit],
|
|
13
|
+
examples:[{label:'姓名',value:chinese?'王小明':'Alex Morgan'},{label:'整数范围 1–5',value:'3'}],
|
|
14
|
+
exampleNote,sources:[{label:'Faker 官方说明',url:'https://faker.readthedocs.io/en/master/'}],
|
|
15
|
+
};
|
|
16
|
+
if(provider==='mimesis')return {
|
|
17
|
+
id:provider,title:'Mimesis',choice:'高性能取值',recommended:false,
|
|
18
|
+
summary:'以高性能取值为特点,适合需要大量姓名、地址等多语言数据的场景。',
|
|
19
|
+
features:['姓名、地址、公司等使用 Mimesis 数据源,与 Faker 的内容和格式可能不同。','支持 Mimesis 原生方法;本项目对中文姓名采用姓在前、名在后的格式。'],
|
|
20
|
+
limits:['整体生成速度还取决于字段规则、外键处理和数据库写入。','高性能是 Mimesis 官方强调的特点;上游基准不等同于 sqlseed 的写库速度,具体语言支持以当前安装版本为准。',commonLimit],
|
|
21
|
+
examples:[{label:'姓名',value:chinese?'陈子涵':'Taylor Reed'},{label:'整数范围 1–5',value:'3'}],
|
|
22
|
+
exampleNote,sources:[{label:'Mimesis 官方特点',url:'https://mimesis.name/master/about.html'},{label:'Mimesis 上游基准',url:'https://mimesis.name/master/benchmarks.html'},{label:'Mimesis 语言与地区',url:'https://mimesis.name/master/locales.html'}],
|
|
23
|
+
};
|
|
24
|
+
if(provider==='base')return {
|
|
25
|
+
id:provider,title:'Base',choice:'基础占位',recommended:false,
|
|
26
|
+
summary:'内置占位引擎,适合验证字段类型、规则与生成流程。',
|
|
27
|
+
features:['整数、日期、字符串等按规则生成;姓名、地址等语义字段使用程序构造的占位值。','由 sqlseed 内置,无需额外安装。'],
|
|
28
|
+
limits:['选择中文不会把占位姓名转换成真实风格的中文姓名。',commonLimit],
|
|
29
|
+
examples:[{label:'姓名',value:'first_001_1234 last_001_1234'},{label:'整数范围 1–5',value:'3'}],
|
|
30
|
+
exampleNote,sources:[],
|
|
31
|
+
};
|
|
32
|
+
return {id:provider,title:String(provider||'未选择引擎'),choice:'自定义引擎',recommended:false,summary:'当前引擎暂无内置说明,请先查看样例并核对其字段规则。',features:[],limits:[commonLimit],examples:[],exampleNote,sources:[]};
|
|
33
|
+
}
|