Wizard step 2: schema browser panel with datalist autocomplete
Adds a /wizard/schemas JSON endpoint and a live-filtered schema picker panel on step 2. Clicking a row fills the schema input; the datalist also powers browser autocomplete. MSSQL refetches when database or linked_server qualifiers change. CSS fixes prevent picker tables and two-col grid items from overflowing their containers. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ff19ae9b81
commit
546242e11a
@ -295,6 +295,29 @@ def wizard_step2(request: Request,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@_router.get("/wizard/schemas")
|
||||||
|
def wizard_schemas(request: Request,
|
||||||
|
source_connection_id: int = Query(...)):
|
||||||
|
"""JSON list of schema names. Drivers that need scoping (e.g. MSSQL by
|
||||||
|
database) read those qualifiers from the querystring."""
|
||||||
|
conn = repo.get_connection(source_connection_id)
|
||||||
|
if conn is None:
|
||||||
|
raise HTTPException(404, f"connection id={source_connection_id} not found")
|
||||||
|
drv = _driver_for_conn(conn)
|
||||||
|
if drv is None:
|
||||||
|
raise HTTPException(500, "driver row missing for connection")
|
||||||
|
qp = dict(request.query_params)
|
||||||
|
qvals = {f.name: qp[f.name] for f in drv.browse_fields()
|
||||||
|
if f.name != "schema" and qp.get(f.name)}
|
||||||
|
try:
|
||||||
|
names = drv.list_schemas(conn, **qvals)
|
||||||
|
except (jrunner.JrunnerError, ValueError) as e:
|
||||||
|
return {"error": str(e), "schemas": []}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return {"error": f"{type(e).__name__}: {e}", "schemas": []}
|
||||||
|
return {"schemas": names}
|
||||||
|
|
||||||
|
|
||||||
@_router.get("/wizard/columns", response_class=HTMLResponse)
|
@_router.get("/wizard/columns", response_class=HTMLResponse)
|
||||||
def wizard_step3(request: Request,
|
def wizard_step3(request: Request,
|
||||||
source_connection_id: int = Query(...),
|
source_connection_id: int = Query(...),
|
||||||
|
|||||||
@ -206,6 +206,9 @@ form.inline { display: inline; }
|
|||||||
grid-template-columns: 2fr 1fr;
|
grid-template-columns: 2fr 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
/* Grid items default to min-width:auto which is content-sized; let them
|
||||||
|
shrink so wide tables/inputs inside don't blow out the layout. */
|
||||||
|
.two-col > * { min-width: 0; }
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.two-col { grid-template-columns: 1fr; }
|
.two-col { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
@ -261,11 +264,16 @@ label.field .help { grid-column: 2; color: var(--text-muted); font-size: 12px; }
|
|||||||
.steps .step .num { font-weight: 700; margin-right: 0.4rem; }
|
.steps .step .num { font-weight: 700; margin-right: 0.4rem; }
|
||||||
|
|
||||||
/* Radio/checkbox-in-row tables */
|
/* Radio/checkbox-in-row tables */
|
||||||
|
table.picker { table-layout: fixed; }
|
||||||
table.picker td.pick { width: 2.5rem; text-align: center; }
|
table.picker td.pick { width: 2.5rem; text-align: center; }
|
||||||
table.picker input[type="radio"],
|
table.picker input[type="radio"],
|
||||||
table.picker input[type="checkbox"] { margin: 0; }
|
table.picker input[type="checkbox"] { margin: 0; }
|
||||||
table.picker tbody tr { cursor: pointer; }
|
table.picker tbody tr { cursor: pointer; }
|
||||||
table.picker tbody tr:hover td { background: #1c2128; }
|
table.picker tbody tr:hover td { background: #1c2128; }
|
||||||
|
/* Drop the global 14rem min-width for in-row text inputs so the picker
|
||||||
|
table can shrink to its container instead of pushing past it. */
|
||||||
|
table.picker input[type="text"] { min-width: 0; }
|
||||||
|
table.picker td { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
/* Flash messages */
|
/* Flash messages */
|
||||||
.flash {
|
.flash {
|
||||||
|
|||||||
@ -22,10 +22,12 @@
|
|||||||
<input type="text" name="{{ f.name }}"
|
<input type="text" name="{{ f.name }}"
|
||||||
value="{{ qvals.get(f.name, '') }}"
|
value="{{ qvals.get(f.name, '') }}"
|
||||||
{% if f.required %}required{% endif %}
|
{% if f.required %}required{% endif %}
|
||||||
|
{% if f.name == 'schema' %}list="schema-options" autocomplete="off" id="schema-input"{% endif %}
|
||||||
placeholder="{{ f.default or '' }}">
|
placeholder="{{ f.default or '' }}">
|
||||||
{% if f.help %}<span class="help">{{ f.help }}</span>{% endif %}
|
{% if f.help %}<span class="help">{{ f.help }}</span>{% endif %}
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<datalist id="schema-options"></datalist>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>table (skip browse)</span>
|
<span>table (skip browse)</span>
|
||||||
@ -64,11 +66,112 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
updateBtn();
|
updateBtn();
|
||||||
|
|
||||||
|
// Populate <datalist> + visible "Schemas" panel from the
|
||||||
|
// source DB. Refetch when scoping qualifiers change (mssql:
|
||||||
|
// database, linked_server).
|
||||||
|
var dlist = document.getElementById('schema-options');
|
||||||
|
var countTag = document.getElementById('schema-count');
|
||||||
|
var gridBody = document.getElementById('schema-grid-body');
|
||||||
|
var emptyTag = document.getElementById('schema-empty');
|
||||||
|
var filterInput = document.getElementById('schema-filter');
|
||||||
|
var dbInput = document.querySelector('input[name="database"]');
|
||||||
|
var lsInput = document.querySelector('input[name="linked_server"]');
|
||||||
|
var connId = {{ connection.id }};
|
||||||
|
var allSchemas = [];
|
||||||
|
var inFlight = 0;
|
||||||
|
|
||||||
|
function applySchemaFilter() {
|
||||||
|
var q = (filterInput.value || '').toLowerCase().trim();
|
||||||
|
var visible = 0;
|
||||||
|
Array.prototype.forEach.call(gridBody.children, function (tr) {
|
||||||
|
var n = tr.getAttribute('data-name') || '';
|
||||||
|
var show = !q || n.toLowerCase().indexOf(q) !== -1;
|
||||||
|
tr.style.display = show ? '' : 'none';
|
||||||
|
if (show) visible++;
|
||||||
|
});
|
||||||
|
countTag.textContent = q
|
||||||
|
? visible + ' of ' + allSchemas.length + ' shown'
|
||||||
|
: allSchemas.length + ' total';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSchemas() {
|
||||||
|
dlist.innerHTML = '';
|
||||||
|
gridBody.innerHTML = '';
|
||||||
|
allSchemas.forEach(function (s) {
|
||||||
|
var o = document.createElement('option');
|
||||||
|
o.value = s;
|
||||||
|
dlist.appendChild(o);
|
||||||
|
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
tr.setAttribute('data-name', s);
|
||||||
|
tr.style.cursor = 'pointer';
|
||||||
|
var td = document.createElement('td');
|
||||||
|
td.className = 'mono';
|
||||||
|
td.textContent = s;
|
||||||
|
tr.appendChild(td);
|
||||||
|
tr.addEventListener('click', function () {
|
||||||
|
if (schemaInput) schemaInput.value = s;
|
||||||
|
});
|
||||||
|
gridBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
emptyTag.style.display = allSchemas.length ? 'none' : '';
|
||||||
|
applySchemaFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSchemas() {
|
||||||
|
var qs = new URLSearchParams({ source_connection_id: connId });
|
||||||
|
if (dbInput && dbInput.value.trim()) qs.set('database', dbInput.value.trim());
|
||||||
|
if (lsInput && lsInput.value.trim()) qs.set('linked_server', lsInput.value.trim());
|
||||||
|
var token = ++inFlight;
|
||||||
|
countTag.textContent = 'loading…';
|
||||||
|
fetch('/wizard/schemas?' + qs.toString())
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (j) {
|
||||||
|
if (token !== inFlight) return; // stale
|
||||||
|
allSchemas = j.schemas || [];
|
||||||
|
renderSchemas();
|
||||||
|
if (j.error) {
|
||||||
|
countTag.textContent = 'load failed: ' + j.error;
|
||||||
|
countTag.style.color = 'var(--danger)';
|
||||||
|
} else {
|
||||||
|
countTag.style.color = '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
if (token !== inFlight) return;
|
||||||
|
countTag.textContent = 'load failed: ' + e;
|
||||||
|
countTag.style.color = 'var(--danger)';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
filterInput.addEventListener('input', applySchemaFilter);
|
||||||
|
if (dbInput) dbInput.addEventListener('change', loadSchemas);
|
||||||
|
if (lsInput) lsInput.addEventListener('change', loadSchemas);
|
||||||
|
loadSchemas();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" id="schema-panel">
|
||||||
|
<header>
|
||||||
|
Schemas
|
||||||
|
<span class="subtitle" id="schema-count">loading…</span>
|
||||||
|
<span style="margin-left:auto">
|
||||||
|
<input type="text" id="schema-filter"
|
||||||
|
placeholder="filter (substring)"
|
||||||
|
autocomplete="off" spellcheck="false"
|
||||||
|
style="min-width:18rem;font-family:var(--mono);font-size:12px">
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div class="body tight" style="max-height:18rem;overflow:auto">
|
||||||
|
<table class="grid picker" id="schema-grid">
|
||||||
|
<tbody id="schema-grid-body"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="schema-empty" class="empty" style="display:none">no schemas</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if fetch_error %}
|
{% if fetch_error %}
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<header>Browse failed</header>
|
<header>Browse failed</header>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user