Compare commits
No commits in common. "546242e11a64eca6c10eeae666b8d045835ec269" and "f18ea55a12f67cfe3ab0089458eff1480bc2630a" have entirely different histories.
546242e11a
...
f18ea55a12
@ -98,14 +98,6 @@ class Driver(abc.ABC):
|
|||||||
def list_tables(self, conn: dict, **qualifiers) -> list[RemoteTable]:
|
def list_tables(self, conn: dict, **qualifiers) -> list[RemoteTable]:
|
||||||
"""Fetch tables/views matching the qualifiers."""
|
"""Fetch tables/views matching the qualifiers."""
|
||||||
|
|
||||||
def list_schemas(self, conn: dict, **qualifiers) -> list[str]:
|
|
||||||
"""Return schema/library names available on the source.
|
|
||||||
|
|
||||||
Default returns []; drivers opt in by overriding. ``qualifiers`` lets
|
|
||||||
drivers like MSSQL scope the lookup to a database/linked-server.
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_columns(self, conn: dict, table: str, **qualifiers) -> list[RemoteColumn]:
|
def get_columns(self, conn: dict, table: str, **qualifiers) -> list[RemoteColumn]:
|
||||||
"""Fetch column metadata for one table."""
|
"""Fetch column metadata for one table."""
|
||||||
|
|||||||
@ -44,12 +44,6 @@ class DB2Driver(Driver):
|
|||||||
help="e.g. RLDBF12"),
|
help="e.g. RLDBF12"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def list_schemas(self, conn, **_) -> list[str]:
|
|
||||||
result = self.query(
|
|
||||||
conn, "SELECT SCHEMA_NAME FROM QSYS2.SYSSCHEMAS "
|
|
||||||
"ORDER BY SCHEMA_NAME")
|
|
||||||
return [r[0].strip() for r in result.rows if r and r[0]]
|
|
||||||
|
|
||||||
def list_tables(self, conn, *, schema: str) -> list[RemoteTable]:
|
def list_tables(self, conn, *, schema: str) -> list[RemoteTable]:
|
||||||
validate_identifier(schema, "schema")
|
validate_identifier(schema, "schema")
|
||||||
sql = (
|
sql = (
|
||||||
|
|||||||
@ -49,20 +49,6 @@ class MSSQLDriver(Driver):
|
|||||||
required=False, default="dbo"),
|
required=False, default="dbo"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def list_schemas(
|
|
||||||
self, conn, *, linked_server: str | None = None,
|
|
||||||
database: str | None = None, **_,
|
|
||||||
) -> list[str]:
|
|
||||||
self._validate(linked_server, database, None)
|
|
||||||
prefix = self._info_schema_prefix(linked_server, database)
|
|
||||||
sql = (
|
|
||||||
f"SELECT DISTINCT TABLE_SCHEMA FROM {prefix}INFORMATION_SCHEMA.TABLES "
|
|
||||||
f"WHERE TABLE_TYPE IN ('BASE TABLE','VIEW') "
|
|
||||||
f"ORDER BY TABLE_SCHEMA"
|
|
||||||
)
|
|
||||||
result = self.query(conn, sql)
|
|
||||||
return [r[0].strip() for r in result.rows if r and r[0]]
|
|
||||||
|
|
||||||
def list_tables(
|
def list_tables(
|
||||||
self, conn, *, linked_server: str | None = None,
|
self, conn, *, linked_server: str | None = None,
|
||||||
database: str | None = None, schema: str | None = None,
|
database: str | None = None, schema: str | None = None,
|
||||||
|
|||||||
@ -41,15 +41,6 @@ class PGDriver(Driver):
|
|||||||
required=False, default="public"),
|
required=False, default="public"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def list_schemas(self, conn, **_) -> list[str]:
|
|
||||||
result = self.query(
|
|
||||||
conn,
|
|
||||||
"SELECT schema_name FROM information_schema.schemata "
|
|
||||||
"WHERE schema_name NOT IN ('pg_catalog','information_schema') "
|
|
||||||
"AND schema_name NOT LIKE 'pg\\_%' ESCAPE '\\' "
|
|
||||||
"ORDER BY schema_name")
|
|
||||||
return [r[0].strip() for r in result.rows if r and r[0]]
|
|
||||||
|
|
||||||
def list_tables(self, conn, *, schema: str | None = None) -> list[RemoteTable]:
|
def list_tables(self, conn, *, schema: str | None = None) -> list[RemoteTable]:
|
||||||
if schema:
|
if schema:
|
||||||
validate_identifier(schema, "schema")
|
validate_identifier(schema, "schema")
|
||||||
|
|||||||
@ -295,29 +295,6 @@ 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,9 +206,6 @@ 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; }
|
||||||
}
|
}
|
||||||
@ -264,16 +261,11 @@ 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,12 +22,10 @@
|
|||||||
<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>
|
||||||
@ -66,112 +64,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
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