Files
dbtool-cli-v1/gui/prototype/app.js
Paperclip CTO d5f69462b0
Some checks failed
release-smoke / macos-13 / x86_64-apple-darwin (push) Has been cancelled
release-smoke / ubuntu-latest / x86_64-unknown-linux-gnu (push) Has been cancelled
release-smoke / windows-latest / x86_64-pc-windows-msvc (push) Has been cancelled
feat(usable): integrate current dbtool implementation snapshot
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-02 08:26:18 +00:00

1455 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const STATE_ORDER = ["running", "success", "empty", "error", "exported"];
const QUERY_ORDER = ["success", "empty", "error", "export"];
const workspaceFixtures = {
"pg-prod-readonly": {
defaultSelection: "public.orders",
catalog: [
{
name: "public",
items: [
{
name: "orders",
kind: "Table",
descriptor: "table · 12 columns",
columns: 12,
primaryKey: "id",
search: "orders public table created_at status customer_id",
},
{
name: "customers",
kind: "Table",
descriptor: "table · 9 columns",
columns: 9,
primaryKey: "id",
search: "customers public table email country",
},
{
name: "payments",
kind: "Table",
descriptor: "table · 7 columns",
columns: 7,
primaryKey: "id",
search: "payments public table status amount",
},
],
},
{
name: "analytics",
items: [
{
name: "daily_orders",
kind: "View",
descriptor: "view · materialized summary",
columns: 6,
primaryKey: "—",
search: "daily_orders analytics view",
},
{
name: "top_customers",
kind: "View",
descriptor: "view · ranking output",
columns: 5,
primaryKey: "—",
search: "top_customers analytics view",
},
],
},
{
name: "ops",
items: [
{
name: "job_runs",
kind: "Table",
descriptor: "table · execution log",
columns: 8,
primaryKey: "id",
search: "job_runs ops table status finished_at",
},
{
name: "incident_log",
kind: "Table",
descriptor: "table · support evidence",
columns: 10,
primaryKey: "id",
search: "incident_log ops table created_at severity",
},
],
},
],
queries: {
success: {
label: "orders audit.sql",
selection: "public.orders",
sql: `select
id,
customer_id,
status,
created_at
from public.orders
where created_at > now() - interval '7 days'
order by created_at desc
limit 50;`,
settledState: "success",
columns: ["id", "customer_id", "status", "created_at"],
successRows: [
["8ec1...", "7cb3...", "paid", "2026-03-25 13:55:01 UTC"],
["80f8...", "5d10...", "processing", "2026-03-25 13:44:15 UTC"],
["5aa2...", "9ab1...", "failed", "2026-03-25 13:12:48 UTC"],
["44fe...", "7cb3...", "paid", "2026-03-25 12:58:22 UTC"],
],
successRowCount: 50,
successDuration: "182 ms",
emptyDuration: "95 ms",
errorDuration: "21 ms",
errorMessage: "Syntax error near `form`",
exportFormat: "csv",
exportPath: "/tmp/orders-last-7-days.csv",
},
empty: {
label: "empty result.sql",
selection: "public.orders",
sql: `select
id,
status,
created_at
from public.orders
where created_at > now() - interval '5 minutes'
and status = 'refunded'
order by created_at desc;`,
settledState: "empty",
columns: ["id", "status", "created_at"],
successRows: [],
successRowCount: 0,
successDuration: "95 ms",
emptyDuration: "95 ms",
errorDuration: "18 ms",
errorMessage: "No result envelope available",
exportFormat: "csv",
exportPath: "/tmp/orders-empty.csv",
},
error: {
label: "bad syntax.sql",
selection: "public.orders",
sql: `select
id,
status
form public.orders
where status = 'paid';`,
settledState: "error",
columns: ["id", "status"],
successRows: [],
successRowCount: 0,
successDuration: "—",
emptyDuration: "—",
errorDuration: "21 ms",
errorMessage: "Syntax error near `form`",
exportFormat: "csv",
exportPath: "/tmp/orders-error.csv",
},
export: {
label: "export review.sql",
selection: "public.orders",
sql: `select
id,
customer_id,
status,
created_at
from public.orders
where created_at >= date_trunc('day', now())
order by created_at desc;`,
settledState: "exported",
columns: ["id", "customer_id", "status", "created_at"],
successRows: [
["8ec1...", "7cb3...", "paid", "2026-03-25 13:55:01 UTC"],
["80f8...", "5d10...", "processing", "2026-03-25 13:44:15 UTC"],
["5aa2...", "9ab1...", "failed", "2026-03-25 13:12:48 UTC"],
["44fe...", "7cb3...", "paid", "2026-03-25 12:58:22 UTC"],
],
successRowCount: 50,
successDuration: "182 ms",
emptyDuration: "95 ms",
errorDuration: "21 ms",
errorMessage: "Export source query failed",
exportFormat: "csv",
exportPath: "/tmp/orders-last-7-days.csv",
},
},
},
"mysql-staging": {
defaultSelection: "qa_demo.orders",
catalog: [
{
name: "qa_demo",
items: [
{
name: "orders",
kind: "Table",
descriptor: "table · 11 columns",
columns: 11,
primaryKey: "id",
search: "orders qa_demo table created_at status region",
},
{
name: "order_audit",
kind: "Table",
descriptor: "table · change history",
columns: 8,
primaryKey: "audit_id",
search: "order_audit qa_demo table before after actor",
},
{
name: "refunds",
kind: "Table",
descriptor: "table · finance review",
columns: 6,
primaryKey: "id",
search: "refunds qa_demo table amount processed_at",
},
],
},
{
name: "reporting",
items: [
{
name: "daily_orders",
kind: "View",
descriptor: "view · daily summary",
columns: 5,
primaryKey: "—",
search: "daily_orders reporting view",
},
{
name: "refund_watchlist",
kind: "View",
descriptor: "view · issue triage",
columns: 4,
primaryKey: "—",
search: "refund_watchlist reporting view",
},
],
},
{
name: "ops",
items: [
{
name: "job_runs",
kind: "Table",
descriptor: "table · pipeline status",
columns: 7,
primaryKey: "id",
search: "job_runs ops table status finished_at",
},
{
name: "dead_letter_queue",
kind: "Table",
descriptor: "table · retry backlog",
columns: 5,
primaryKey: "id",
search: "dead_letter_queue ops table retry backlog",
},
],
},
],
queries: {
success: {
label: "staging diff.sql",
selection: "qa_demo.orders",
sql: `select
id,
region,
status,
updated_at
from qa_demo.orders
where updated_at >= now() - interval 1 day
order by updated_at desc
limit 50;`,
settledState: "success",
columns: ["id", "region", "status", "updated_at"],
successRows: [
["1024", "us-east", "paid", "2026-03-25 13:55:01 UTC"],
["1021", "eu-west", "processing", "2026-03-25 13:44:15 UTC"],
["1015", "ap-south", "failed", "2026-03-25 13:12:48 UTC"],
["1007", "us-west", "paid", "2026-03-25 12:58:22 UTC"],
],
successRowCount: 37,
successDuration: "164 ms",
emptyDuration: "88 ms",
errorDuration: "17 ms",
errorMessage: "Unknown column `statsu` in `field list`",
exportFormat: "json",
exportPath: "/tmp/mysql-staging-diff.json",
},
empty: {
label: "no drift.sql",
selection: "reporting.daily_orders",
sql: `select
order_date,
failed_count
from reporting.daily_orders
where order_date = current_date()
and failed_count > 50;`,
settledState: "empty",
columns: ["order_date", "failed_count"],
successRows: [],
successRowCount: 0,
successDuration: "88 ms",
emptyDuration: "88 ms",
errorDuration: "16 ms",
errorMessage: "No result envelope available",
exportFormat: "json",
exportPath: "/tmp/mysql-no-drift.json",
},
error: {
label: "bad filter.sql",
selection: "qa_demo.orders",
sql: `select
id,
statsu
from qa_demo.orders
where status = 'paid';`,
settledState: "error",
columns: ["id", "status"],
successRows: [],
successRowCount: 0,
successDuration: "—",
emptyDuration: "—",
errorDuration: "17 ms",
errorMessage: "Unknown column `statsu` in `field list`",
exportFormat: "json",
exportPath: "/tmp/mysql-error.json",
},
export: {
label: "handoff export.sql",
selection: "qa_demo.order_audit",
sql: `select
audit_id,
order_id,
actor,
changed_at
from qa_demo.order_audit
order by changed_at desc
limit 100;`,
settledState: "exported",
columns: ["audit_id", "order_id", "actor", "changed_at"],
successRows: [
["5008", "1024", "api", "2026-03-25 13:55:01 UTC"],
["5007", "1021", "worker", "2026-03-25 13:44:15 UTC"],
["5006", "1015", "ops", "2026-03-25 13:12:48 UTC"],
["5005", "1007", "api", "2026-03-25 12:58:22 UTC"],
],
successRowCount: 100,
successDuration: "201 ms",
emptyDuration: "112 ms",
errorDuration: "22 ms",
errorMessage: "Export source query failed",
exportFormat: "json",
exportPath: "/tmp/mysql-audit-export.json",
},
},
},
"sqlite-local": {
defaultSelection: "main.audit_events",
catalog: [
{
name: "main",
items: [
{
name: "audit_events",
kind: "Table",
descriptor: "table · local evidence log",
columns: 7,
primaryKey: "id",
search: "audit_events main table action actor event_time",
},
{
name: "attachments",
kind: "Table",
descriptor: "table · exported evidence",
columns: 5,
primaryKey: "id",
search: "attachments main table file_path created_at",
},
{
name: "query_history",
kind: "Table",
descriptor: "table · recent drafts",
columns: 4,
primaryKey: "id",
search: "query_history main table sql_label saved_at",
},
],
},
{
name: "temp",
items: [
{
name: "staged_rows",
kind: "Table",
descriptor: "table · transient compare set",
columns: 4,
primaryKey: "—",
search: "staged_rows temp table transient compare",
},
],
},
],
queries: {
success: {
label: "evidence review.sql",
selection: "main.audit_events",
sql: `select
id,
actor,
action,
event_time
from main.audit_events
order by event_time desc
limit 50;`,
settledState: "success",
columns: ["id", "actor", "action", "event_time"],
successRows: [
["91", "qa-bot", "attach_export", "2026-03-25 13:55:01 UTC"],
["90", "frontend", "query_ok", "2026-03-25 13:44:15 UTC"],
["89", "backend", "inspect_table", "2026-03-25 13:12:48 UTC"],
["88", "qa-bot", "open_file", "2026-03-25 12:58:22 UTC"],
],
successRowCount: 24,
successDuration: "14 ms",
emptyDuration: "8 ms",
errorDuration: "4 ms",
errorMessage: "SQLite error: no such column: acton",
exportFormat: "csv",
exportPath: "/tmp/sqlite-audit-events.csv",
},
empty: {
label: "no incidents.sql",
selection: "main.audit_events",
sql: `select
id,
action,
event_time
from main.audit_events
where action = 'incident_opened'
and event_time >= datetime('now', '-5 minutes');`,
settledState: "empty",
columns: ["id", "action", "event_time"],
successRows: [],
successRowCount: 0,
successDuration: "8 ms",
emptyDuration: "8 ms",
errorDuration: "4 ms",
errorMessage: "No result envelope available",
exportFormat: "csv",
exportPath: "/tmp/sqlite-empty.csv",
},
error: {
label: "bad column.sql",
selection: "main.audit_events",
sql: `select
id,
acton
from main.audit_events
where actor = 'qa-bot';`,
settledState: "error",
columns: ["id", "action"],
successRows: [],
successRowCount: 0,
successDuration: "—",
emptyDuration: "—",
errorDuration: "4 ms",
errorMessage: "SQLite error: no such column: acton",
exportFormat: "csv",
exportPath: "/tmp/sqlite-error.csv",
},
export: {
label: "attachment export.sql",
selection: "main.attachments",
sql: `select
id,
file_path,
created_at
from main.attachments
order by created_at desc
limit 25;`,
settledState: "exported",
columns: ["id", "file_path", "created_at"],
successRows: [
["17", "/tmp/evidence-1.csv", "2026-03-25 13:55:01 UTC"],
["16", "/tmp/evidence-2.csv", "2026-03-25 13:44:15 UTC"],
["15", "/tmp/evidence-3.csv", "2026-03-25 13:12:48 UTC"],
["14", "/tmp/evidence-4.csv", "2026-03-25 12:58:22 UTC"],
],
successRowCount: 25,
successDuration: "11 ms",
emptyDuration: "6 ms",
errorDuration: "4 ms",
errorMessage: "Export source query failed",
exportFormat: "csv",
exportPath: "/tmp/sqlite-attachments.csv",
},
},
},
};
const connectionCards = document.querySelectorAll(".connection-card");
const bottomTabs = document.querySelectorAll(".bottom-tab");
const tabContents = document.querySelectorAll(".tab-content");
const workflowStepButtons = document.querySelectorAll("[data-workflow-step]");
const reviewPanels = document.querySelectorAll("[data-review-panel]");
const activeConnection = document.getElementById("active-connection");
const driverPill = document.getElementById("driver-pill");
const detailTarget = document.getElementById("detail-target");
const detailMode = document.getElementById("detail-mode");
const detailExport = document.getElementById("detail-export");
const detailTheme = document.getElementById("detail-theme");
const contextTarget = document.getElementById("context-target");
const contextMode = document.getElementById("context-mode");
const contextSelection = document.getElementById("context-selection");
const contextValidated = document.getElementById("context-validated");
const inspectorObject = document.getElementById("inspector-object");
const selectionKind = document.getElementById("selection-kind");
const selectionPrimaryKey = document.getElementById("selection-primary-key");
const selectionColumns = document.getElementById("selection-columns");
const summaryWorkspace = document.getElementById("summary-workspace");
const summaryWorkspaceCopy = document.getElementById("summary-workspace-copy");
const summarySelection = document.getElementById("summary-selection");
const summarySelectionCopy = document.getElementById("summary-selection-copy");
const summaryRun = document.getElementById("summary-run");
const summaryRunCopy = document.getElementById("summary-run-copy");
const summaryExport = document.getElementById("summary-export");
const summaryExportCopy = document.getElementById("summary-export-copy");
const executionBanner = document.getElementById("execution-banner");
const executionTitle = document.getElementById("execution-title");
const executionCopy = document.getElementById("execution-copy");
const durationValue = document.getElementById("duration-value");
const rowCount = document.getElementById("row-count");
const stateLabel = document.getElementById("state-label");
const editorCode = document.getElementById("editor-code");
const editorGutter = document.getElementById("editor-gutter");
const toolbarTabsContainer = document.getElementById("toolbar-tabs");
const schemaTree = document.getElementById("schema-tree");
const resultsHead = document.getElementById("results-head");
const resultsBody = document.getElementById("results-body");
const historyList = document.getElementById("history-list");
const resultSummary = document.getElementById("result-summary");
const resultCopy = document.getElementById("result-copy");
const exportTitle = document.getElementById("export-title");
const exportCopy = document.getElementById("export-copy");
const problemTitle = document.getElementById("problem-title");
const problemCopy = document.getElementById("problem-copy");
const contractTitle = document.getElementById("contract-title");
const contractCopy = document.getElementById("contract-copy");
const contractJson = document.getElementById("contract-json");
const statusMessage = document.getElementById("status-message");
const themeToggle = document.getElementById("theme-toggle");
const schemaSearch = document.getElementById("schema-search");
const clearSearch = document.getElementById("clear-search");
const runQueryButton = document.getElementById("run-query");
const cycleStateButton = document.getElementById("cycle-state");
const exportCurrentButton = document.getElementById("export-current");
const exportCsvButton = document.getElementById("export-csv");
const exportJsonButton = document.getElementById("export-json");
const commandButton = document.getElementById("command-button");
const commandPalette = document.getElementById("command-palette");
const commandBackdrop = document.getElementById("command-backdrop");
const commandCloseButton = document.getElementById("command-close");
const commandStateButtons = document.querySelectorAll("[data-command-state]");
const commandConnectionButtons = document.querySelectorAll("[data-command-connection]");
const commandActionButtons = document.querySelectorAll("[data-command-action]");
const workflowCopy = document.getElementById("workflow-copy");
const appState = {
connection: "pg-prod-readonly",
query: "success",
selection: "public.orders",
activeState: "success",
activeTab: "results",
workflowStep: "connect",
runTimer: null,
};
const workflowStepCopy = {
connect: "当前聚焦 Connect先确认活动目标、环境标签和连接摘要清晰可见。",
inspect: "当前聚焦 Inspect核对 schema browser、当前对象和 inspector 元数据保持同步。",
query: "当前聚焦 Query确认 SQL 草稿、执行反馈和结果区在同一工作台上下文里。",
export: "当前聚焦 Export复核默认导出路径、空结果说明和错误态禁用约束。",
};
function getActiveConnectionCard() {
return document.querySelector(`.connection-card[data-connection="${appState.connection}"]`);
}
function getActiveConnectionData() {
return getActiveConnectionCard().dataset;
}
function getWorkspaceFixture() {
return workspaceFixtures[appState.connection];
}
function getQueryPreset(queryKey = appState.query) {
return getWorkspaceFixture().queries[queryKey];
}
function getSelectionMeta(selection = appState.selection) {
const fixture = getWorkspaceFixture();
for (const group of fixture.catalog) {
for (const item of group.items) {
if (`${group.name}.${item.name}` === selection) {
return {
schema: group.name,
selection,
...item,
};
}
}
}
return null;
}
function getTreeItems() {
return schemaTree.querySelectorAll(".tree-item");
}
function getSchemaGroups() {
return schemaTree.querySelectorAll(".tree-group");
}
function setActiveTab(tabName) {
appState.activeTab = tabName;
bottomTabs.forEach((tab) => {
tab.classList.toggle("is-active", tab.dataset.tab === tabName);
});
tabContents.forEach((content) => {
content.classList.toggle("is-active", content.dataset.content === tabName);
});
}
function renderWorkflowStep() {
workflowStepButtons.forEach((button) => {
const isActive = button.dataset.workflowStep === appState.workflowStep;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
});
reviewPanels.forEach((panel) => {
panel.classList.toggle("is-review-focus", panel.dataset.reviewPanel === appState.workflowStep);
});
workflowCopy.textContent = workflowStepCopy[appState.workflowStep];
}
function renderToolbarTabs() {
const fixture = getWorkspaceFixture();
toolbarTabsContainer.innerHTML = QUERY_ORDER.map((queryKey) => {
const preset = fixture.queries[queryKey];
const isActive = queryKey === appState.query ? " is-active" : "";
return `<button class="toolbar-tab${isActive}" data-query="${queryKey}">${preset.label}</button>`;
}).join("");
}
function renderSchemaTree() {
const fixture = getWorkspaceFixture();
schemaTree.innerHTML = fixture.catalog
.map((group) => {
const groupSearch = [group.name, ...group.items.map((item) => item.search)].join(" ").toLowerCase();
const items = group.items
.map((item) => {
const selection = `${group.name}.${item.name}`;
const isSelected = selection === appState.selection ? " is-selected" : "";
return `
<button class="tree-item${isSelected}" data-selection="${selection}" data-search="${item.search.toLowerCase()}">
<strong>${item.name}</strong>
<span>${item.descriptor}</span>
</button>
`;
})
.join("");
return `
<details open class="tree-group" data-search="${groupSearch}">
<summary>${group.name}</summary>
${items}
</details>
`;
})
.join("");
applySchemaFilter();
}
function renderResults(columns, rows, tableMessage) {
const activeColumns = columns.length > 0 ? columns : ["message"];
resultsHead.innerHTML = activeColumns.map((column) => `<th>${column}</th>`).join("");
if (tableMessage) {
resultsBody.innerHTML = `<tr><td colspan="${activeColumns.length}">${tableMessage}</td></tr>`;
return;
}
resultsBody.innerHTML = rows
.map(
(row) => `
<tr>
${row.map((cell) => `<td>${cell}</td>`).join("")}
</tr>
`,
)
.join("");
}
function renderHistory(items) {
historyList.innerHTML = items
.map(
([title, meta]) => `
<div class="log-row">
<strong>${title}</strong>
<span>${meta}</span>
</div>
`,
)
.join("");
}
function renderEditor(sql) {
editorCode.textContent = sql;
const lineCount = sql.split("\n").length;
editorGutter.innerHTML = Array.from({ length: lineCount }, (_, index) => `<span>${index + 1}</span>`).join("");
}
function normalizeDriver(driver) {
const driverMap = {
PostgreSQL: "postgres",
MySQL: "mysql",
SQLite: "sqlite",
};
return driverMap[driver] || driver.toLowerCase();
}
function buildHistoryMeta(queryKey, state = getQueryPreset(queryKey).settledState) {
const query = getQueryPreset(queryKey);
const connection = getActiveConnectionData();
if (state === "running") {
return `Running · executing against ${connection.connection}`;
}
if (state === "success") {
return `Success · ${query.successDuration} · ${query.successRowCount} rows`;
}
if (state === "empty") {
return `Success · ${query.emptyDuration} · 0 rows`;
}
if (state === "error") {
return `Error · ${query.errorMessage}`;
}
return `Exported · ${query.exportFormat.toUpperCase()} · ${query.successRowCount} rows`;
}
function buildHistory() {
const orderedKeys = [appState.query, ...QUERY_ORDER.filter((queryKey) => queryKey !== appState.query)];
return orderedKeys.map((queryKey) => {
const preset = getQueryPreset(queryKey);
const state = queryKey === appState.query ? appState.activeState : preset.settledState;
return [preset.label, buildHistoryMeta(queryKey, state)];
});
}
function buildContractSnapshot() {
const connection = getActiveConnectionData();
const query = getQueryPreset();
const selection = getSelectionMeta();
const baseTarget = {
profile_name: connection.connection,
driver: normalizeDriver(connection.driver),
};
if (appState.activeState === "running") {
return {
title: "UI transient execution preview",
copy: "`running` 仅是界面暂态;共享契约仍等待 `QueryResponse` 或 `AppError` 返回。",
snapshot: {
ui_state: "running",
pending_operation: "query",
target: baseTarget,
source: {
kind: "inline",
label: query.label,
},
context_object: selection?.selection || appState.selection,
note: "Transient frontend state before the settled backend envelope is available.",
},
};
}
if (appState.activeState === "error") {
return {
title: "Query error envelope preview",
copy: "失败态映射到 `status: error`,重点看 `error.kind` 与 `error.message`,而不是把失败伪装成空结果。",
snapshot: {
status: "error",
operation: "query",
error: {
operation: "query",
kind: "query",
message: query.errorMessage,
},
context_object: selection?.selection || appState.selection,
},
};
}
if (appState.activeState === "exported") {
return {
title: "Export success envelope preview",
copy: "导出完成后切换到 `operation: export`QA 可重点核对 `export.output_path` 与 `row_count`。",
snapshot: {
status: "success",
operation: "export",
data: {
target: baseTarget,
source: {
kind: "inline",
label: query.label,
},
context_object: selection?.selection || appState.selection,
export: {
format: query.exportFormat,
output_path: query.exportPath,
overwrite: false,
},
row_count: query.successRowCount,
},
},
};
}
if (appState.activeState === "empty") {
return {
title: "Query empty-result envelope preview",
copy: "空结果仍然属于 `status: success`;用 `data.result.row_count: 0` 与问题态区分。",
snapshot: {
status: "success",
operation: "query",
data: {
target: baseTarget,
source: {
kind: "inline",
label: query.label,
},
context_object: selection?.selection || appState.selection,
result: {
columns: query.columns,
rows: [],
row_count: 0,
},
},
},
};
}
return {
title: "Query success envelope preview",
copy: "成功态对齐 `operation: query`,重点确认 `data.result.columns`、`rows` 与 `row_count` 能驱动工作区渲染。",
snapshot: {
status: "success",
operation: "query",
data: {
target: baseTarget,
source: {
kind: "inline",
label: query.label,
},
context_object: selection?.selection || appState.selection,
result: {
columns: query.columns,
rows: query.successRows.slice(0, 2),
row_count: query.successRowCount,
},
},
},
};
}
function buildStatePreset() {
const query = getQueryPreset();
const connection = getActiveConnectionData();
const activeLabel = query.label;
if (appState.activeState === "running") {
return {
title: "Query is running",
copy: `${activeLabel} stays visible in-place while the workspace keeps target, selection, and export context readable.`,
duration: "—",
rows: "—",
label: "Running",
resultSummary: `Execution in progress · ${activeLabel}`,
resultCopy: "执行中时不隐藏结果区,而是保留当前 SQL、目标连接和对象浏览上下文。",
exportTitle: "Export is unavailable while the query is running",
exportCopy: "等待当前执行完成后,再决定是否导出当前结果集。",
problemTitle: "No blocking problem yet",
problemCopy: "如果执行时间异常增长,未来正式 GUI 应提供 cancel / timeout 反馈。",
tableMessage: `${activeLabel} is running… results will appear here when the execution finishes.`,
rowsData: [],
runSummary: "Running · waiting for rows",
runCopy: `${connection.connection} 正在执行当前草稿。`,
lastExport: "Waiting for current run",
statusMessage: `Running preview active for ${activeLabel}.`,
columns: query.columns,
};
}
if (appState.activeState === "success") {
return {
title: "Last run succeeded",
copy: `${query.successRowCount} rows returned in ${query.successDuration}. Export is available.`,
duration: query.successDuration,
rows: String(query.successRowCount),
label: "Success",
resultSummary: `${query.successRowCount} rows · ${activeLabel}`,
resultCopy: "结果区保留紧邻执行反馈的上下文,不让表格单独承担状态信息。",
exportTitle: "Ready to export the current result set",
exportCopy: `\`${query.exportPath}\` is available as the default target.`,
problemTitle: "No blocking problem",
problemCopy: "当前成功态下,问题区保留错误说明预留位,但不制造多余警告。",
rowsData: query.successRows,
runSummary: `Success · ${query.successRowCount} rows`,
runCopy: `${query.successDuration},结果可导出。`,
lastExport: "Not started",
statusMessage: `Success preview active for ${activeLabel}.`,
columns: query.columns,
};
}
if (appState.activeState === "empty") {
return {
title: "Query returned no rows",
copy: `Execution still succeeded in ${query.emptyDuration}. Empty result should not look like a failure.`,
duration: query.emptyDuration,
rows: "0",
label: "Empty",
resultSummary: `0 rows · ${activeLabel}`,
resultCopy: "空结果是可解释的完成态,仍保留 SQL、耗时和连接上下文。",
exportTitle: "No rows to export from the current result",
exportCopy: "保持导出入口可见,但要清楚说明当前不会产出文件。",
problemTitle: "No blocking problem",
problemCopy: "如需下一步,可建议用户放宽过滤条件或切换对象,而不是显示错误。",
tableMessage: "No rows in the current result set.",
rowsData: [],
runSummary: "Success · 0 rows",
runCopy: `${query.emptyDuration},空结果但执行成功。`,
lastExport: "Skipped · no rows",
statusMessage: `Empty-result preview active for ${activeLabel}.`,
columns: query.columns,
};
}
if (appState.activeState === "error") {
return {
title: "Last run failed",
copy: `${query.errorMessage}. The error summary should stay visible in the workspace.`,
duration: query.errorDuration,
rows: "0",
label: "Error",
resultSummary: `No table output · ${activeLabel}`,
resultCopy: "错误信息应与 SQL 片段、目标连接和恢复动作放在同一工作区上下文中。",
exportTitle: "Export disabled until the query succeeds",
exportCopy: "失败态下导出区不隐藏,而是明确说明为何当前不可导出。",
problemTitle: query.errorMessage,
problemCopy: "建议动作:回到编辑区修正关键字、重试执行,或切换到最近一次成功结果。",
tableMessage: "Execution failed before a result set was returned.",
rowsData: [],
runSummary: "Error · query failed",
runCopy: `${query.errorDuration},错误摘要持续可见。`,
lastExport: "Blocked by query error",
statusMessage: `Error preview active for ${activeLabel}.`,
columns: query.columns,
};
}
return {
title: "Export completed",
copy: `${query.successRowCount} rows exported to \`${query.exportPath}\`. The result state remains reviewable.`,
duration: query.successDuration,
rows: String(query.successRowCount),
label: "Exported",
resultSummary: `${query.successRowCount} rows · ${activeLabel}`,
resultCopy: "导出完成后仍保留结果上下文,避免把确认信息丢到瞬时 toast。",
exportTitle: "Export completed successfully",
exportCopy: `\`${query.exportPath}\` was prepared as the last output path in this prototype.`,
problemTitle: "No blocking problem",
problemCopy: "导出成功是工作流完成态的一部分,应与执行成功和结果表格形成闭环。",
rowsData: query.successRows,
runSummary: `Exported · ${query.successRowCount} rows`,
runCopy: `${query.exportFormat.toUpperCase()} 导出反馈已留在工作台中。`,
lastExport: query.exportPath,
statusMessage: `Export preview active for ${activeLabel}.`,
columns: query.columns,
};
}
function renderSelection() {
const selection = getSelectionMeta();
if (!selection) {
return;
}
contextSelection.textContent = selection.selection;
inspectorObject.textContent = selection.selection;
selectionKind.textContent = selection.kind;
selectionPrimaryKey.textContent = selection.primaryKey;
selectionColumns.textContent = String(selection.columns);
summarySelection.textContent = selection.selection;
summarySelectionCopy.textContent = `${selection.descriptor} · schema browser 与 inspector 保持同步。`;
}
function renderConnection() {
const connection = getActiveConnectionData();
activeConnection.textContent = connection.connection;
driverPill.textContent = connection.driver;
detailTarget.textContent = connection.connection;
detailMode.textContent = connection.mode;
contextTarget.textContent = connection.connection;
contextMode.textContent = connection.mode;
contextValidated.textContent = connection.lastValidated;
summaryWorkspace.textContent = connection.workspace;
summaryWorkspaceCopy.textContent = connection.workspaceCopy;
}
function renderThemeLabel() {
const theme = document.documentElement.dataset.theme;
const isDark = theme === "dark";
themeToggle.textContent = isDark ? "切换到 Light" : "切换到 Dark";
detailTheme.textContent = isDark ? "Dark" : "Light";
}
function renderState() {
const preset = buildStatePreset();
const contract = buildContractSnapshot();
const exportBlocked = appState.activeState === "running" || appState.activeState === "error";
executionBanner.className = `execution-banner is-${appState.activeState}`;
executionTitle.textContent = preset.title;
executionCopy.textContent = preset.copy;
durationValue.textContent = preset.duration;
rowCount.textContent = preset.rows;
stateLabel.textContent = preset.label;
resultSummary.textContent = preset.resultSummary;
resultCopy.textContent = preset.resultCopy;
exportTitle.textContent = preset.exportTitle;
exportCopy.textContent = preset.exportCopy;
problemTitle.textContent = preset.problemTitle;
problemCopy.textContent = preset.problemCopy;
contractTitle.textContent = contract.title;
contractCopy.textContent = contract.copy;
contractJson.textContent = JSON.stringify(contract.snapshot, null, 2);
summaryRun.textContent = preset.runSummary;
summaryRunCopy.textContent = preset.runCopy;
summaryExport.textContent = preset.lastExport === "Not started" ? getQueryPreset().exportPath : preset.lastExport;
summaryExportCopy.textContent =
preset.lastExport === "Not started"
? "当前展示默认导出目标,不执行真实写盘。"
: "导出反馈保留在当前工作台,便于 QA 与 PM 直接复核。";
detailExport.textContent = preset.lastExport;
statusMessage.textContent = preset.statusMessage;
runQueryButton.disabled = appState.activeState === "running";
runQueryButton.textContent = appState.activeState === "running" ? "Running…" : "Run Query";
exportCurrentButton.disabled = exportBlocked;
exportCsvButton.disabled = exportBlocked;
exportJsonButton.disabled = exportBlocked;
renderResults(preset.columns, preset.rowsData, preset.tableMessage);
renderHistory(buildHistory());
if (appState.activeState === "running") {
setActiveTab("results");
}
if (appState.activeState === "error") {
setActiveTab("problems");
}
if (appState.activeState === "exported") {
setActiveTab("export");
}
}
function renderQuery() {
const preset = getQueryPreset();
renderEditor(preset.sql);
renderState();
}
function applySchemaFilter() {
const keyword = schemaSearch.value.trim().toLowerCase();
const items = getTreeItems();
const groups = getSchemaGroups();
items.forEach((item) => {
const matches = item.dataset.search.includes(keyword);
item.hidden = keyword.length > 0 && !matches;
});
groups.forEach((group) => {
const groupMatch = group.dataset.search.includes(keyword);
const hasVisibleChildren = Array.from(group.querySelectorAll(".tree-item")).some((item) => !item.hidden);
group.hidden = keyword.length > 0 && !groupMatch && !hasVisibleChildren;
if (!group.hidden && keyword.length > 0) {
group.open = true;
}
});
}
function renderAll() {
renderToolbarTabs();
renderSchemaTree();
renderConnection();
renderSelection();
renderThemeLabel();
renderWorkflowStep();
renderQuery();
setActiveTab(appState.activeTab);
}
function clearRunTimer() {
if (appState.runTimer) {
clearTimeout(appState.runTimer);
appState.runTimer = null;
}
}
function closeCommandPalette() {
commandPalette.hidden = true;
document.body.classList.remove("has-modal");
}
function openCommandPalette() {
commandPalette.hidden = false;
document.body.classList.add("has-modal");
statusMessage.textContent = "Quick actions open. Choose a state, connection, or workflow shortcut.";
commandStateButtons[0]?.focus();
}
function syncSelection(selection) {
appState.selection = selection;
renderSchemaTree();
renderSelection();
}
function setWorkflowStep(step, { syncTab = false, status = null } = {}) {
appState.workflowStep = step;
if (syncTab) {
if (step === "query") {
setActiveTab("results");
}
if (step === "export") {
setActiveTab("export");
}
}
renderWorkflowStep();
if (status) {
statusMessage.textContent = status;
}
}
function setQuery(queryKey, syncState = true) {
const preset = getQueryPreset(queryKey);
appState.query = queryKey;
appState.selection = preset.selection;
if (syncState) {
appState.activeState = preset.settledState;
}
renderAll();
}
function startRunPreview() {
clearRunTimer();
setWorkflowStep("query", { syncTab: true });
appState.activeState = "running";
setActiveTab("results");
renderState();
appState.runTimer = setTimeout(() => {
appState.runTimer = null;
appState.activeState = getQueryPreset().settledState;
renderState();
}, 1100);
}
connectionCards.forEach((card) => {
card.addEventListener("click", () => {
clearRunTimer();
connectionCards.forEach((item) => item.classList.remove("is-active"));
card.classList.add("is-active");
appState.connection = card.dataset.connection;
appState.query = "success";
appState.selection = getWorkspaceFixture().defaultSelection;
appState.activeState = getQueryPreset().settledState;
schemaSearch.value = "";
setActiveTab("results");
renderAll();
setWorkflowStep("connect", {
status: `${card.dataset.connection} loaded. Catalog, draft labels, and inspector now reflect the active target.`,
});
});
});
schemaTree.addEventListener("click", (event) => {
const item = event.target.closest(".tree-item");
if (!item) {
return;
}
syncSelection(item.dataset.selection);
setWorkflowStep("inspect", {
status: `Selection changed to ${item.dataset.selection}. Inspector and workspace stay aligned.`,
});
});
toolbarTabsContainer.addEventListener("click", (event) => {
const tab = event.target.closest(".toolbar-tab");
if (!tab) {
return;
}
clearRunTimer();
setWorkflowStep("query", { syncTab: true });
setActiveTab("results");
setQuery(tab.dataset.query, true);
});
bottomTabs.forEach((tab) => {
tab.addEventListener("click", () => {
setActiveTab(tab.dataset.tab);
});
});
workflowStepButtons.forEach((button) => {
button.addEventListener("click", () => {
const step = button.dataset.workflowStep;
const syncTab = step === "query" || step === "export";
const statusMap = {
connect: "Connect review active. Confirm target health, environment label, and workspace summary.",
inspect: "Inspect review active. Check schema tree, selection, and inspector metadata alignment.",
query: "Query review active. Validate SQL draft, execution banner, and results context together.",
export: "Export review active. Check default path, blocked states, and empty-result guidance.",
};
setWorkflowStep(step, { syncTab, status: statusMap[step] });
});
});
runQueryButton.addEventListener("click", () => {
startRunPreview();
});
cycleStateButton.addEventListener("click", () => {
clearRunTimer();
const currentIndex = STATE_ORDER.indexOf(appState.activeState);
const nextState = STATE_ORDER[(currentIndex + 1) % STATE_ORDER.length];
appState.activeState = nextState;
renderState();
});
function activateExport(format) {
clearRunTimer();
setWorkflowStep("export", { syncTab: true });
if (appState.activeState === "running") {
renderState();
statusMessage.textContent = `${format} export is locked until the running query finishes.`;
return;
}
if (appState.activeState === "error") {
renderState();
statusMessage.textContent = `${format} export is blocked because the current query failed.`;
return;
}
setActiveTab("export");
if (appState.activeState === "empty") {
renderState();
statusMessage.textContent = `${format} export preview open. Current result is empty, so no file would be written.`;
return;
}
appState.activeState = "exported";
renderState();
statusMessage.textContent = `${format} export preview active. No file is written in this prototype.`;
}
exportCurrentButton.addEventListener("click", () => activateExport("Workspace"));
exportCsvButton.addEventListener("click", () => activateExport("CSV"));
exportJsonButton.addEventListener("click", () => activateExport("JSON"));
themeToggle.addEventListener("click", () => {
document.documentElement.dataset.theme =
document.documentElement.dataset.theme === "dark" ? "light" : "dark";
renderThemeLabel();
});
commandButton.addEventListener("click", () => {
if (commandPalette.hidden) {
openCommandPalette();
return;
}
closeCommandPalette();
});
commandBackdrop.addEventListener("click", closeCommandPalette);
commandCloseButton.addEventListener("click", closeCommandPalette);
commandStateButtons.forEach((button) => {
button.addEventListener("click", () => {
clearRunTimer();
appState.activeState = button.dataset.commandState;
renderState();
closeCommandPalette();
});
});
commandConnectionButtons.forEach((button) => {
button.addEventListener("click", () => {
const card = document.querySelector(`.connection-card[data-connection="${button.dataset.commandConnection}"]`);
card?.click();
closeCommandPalette();
});
});
commandActionButtons.forEach((button) => {
button.addEventListener("click", () => {
closeCommandPalette();
if (button.dataset.commandAction === "run") {
startRunPreview();
return;
}
if (button.dataset.commandAction === "search") {
schemaSearch.focus();
statusMessage.textContent = "Schema search focused.";
return;
}
if (button.dataset.commandAction === "theme") {
themeToggle.click();
}
});
});
schemaSearch.addEventListener("input", () => {
applySchemaFilter();
});
clearSearch.addEventListener("click", () => {
schemaSearch.value = "";
applySchemaFilter();
statusMessage.textContent = "Schema search cleared.";
});
document.addEventListener("keydown", (event) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
if (commandPalette.hidden) {
openCommandPalette();
return;
}
closeCommandPalette();
return;
}
if (!commandPalette.hidden && event.key === "Escape") {
event.preventDefault();
closeCommandPalette();
statusMessage.textContent = "Quick actions closed.";
return;
}
if (!commandPalette.hidden) {
return;
}
if (event.target === schemaSearch) {
return;
}
const workflowHotkeyMap = {
c: "connect",
i: "inspect",
q: "query",
e: "export",
};
const workflowStep = workflowHotkeyMap[event.key.toLowerCase()];
if (workflowStep && !event.metaKey && !event.ctrlKey) {
event.preventDefault();
setWorkflowStep(workflowStep, {
syncTab: workflowStep === "query" || workflowStep === "export",
status: `${workflowStep[0].toUpperCase()}${workflowStep.slice(1)} review active.`,
});
return;
}
if (event.key === "/") {
event.preventDefault();
schemaSearch.focus();
statusMessage.textContent = "Schema search focused.";
return;
}
if (event.key.toLowerCase() === "t") {
event.preventDefault();
themeToggle.click();
statusMessage.textContent = "Theme toggled.";
return;
}
if (event.key.toLowerCase() === "r") {
event.preventDefault();
startRunPreview();
return;
}
const hotkeyMap = {
1: "running",
2: "success",
3: "empty",
4: "error",
5: "exported",
};
if (hotkeyMap[event.key]) {
event.preventDefault();
clearRunTimer();
appState.activeState = hotkeyMap[event.key];
renderState();
}
});
renderAll();