1365 lines
43 KiB
JavaScript
1365 lines
43 KiB
JavaScript
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 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 appState = {
|
||
connection: "pg-prod-readonly",
|
||
query: "success",
|
||
selection: "public.orders",
|
||
activeState: "success",
|
||
activeTab: "results",
|
||
runTimer: null,
|
||
};
|
||
|
||
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 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();
|
||
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 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();
|
||
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();
|
||
statusMessage.textContent = `${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);
|
||
statusMessage.textContent = `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();
|
||
setActiveTab("results");
|
||
setQuery(tab.dataset.query, true);
|
||
});
|
||
|
||
bottomTabs.forEach((tab) => {
|
||
tab.addEventListener("click", () => {
|
||
setActiveTab(tab.dataset.tab);
|
||
});
|
||
});
|
||
|
||
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();
|
||
|
||
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 (event.target === schemaSearch) {
|
||
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();
|