simplemdg-dev-cli 2.4.4 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -15
- package/USER_GUIDE.md +11 -9
- package/dist/core/db/db-cache.d.ts +5 -0
- package/dist/core/db/db-cache.js +24 -0
- package/dist/core/db/db-cache.js.map +1 -1
- package/dist/core/db/db-hana-adapter.d.ts +4 -0
- package/dist/core/db/db-hana-adapter.js +8 -0
- package/dist/core/db/db-hana-adapter.js.map +1 -1
- package/dist/core/db/db-postgres-adapter.d.ts +4 -0
- package/dist/core/db/db-postgres-adapter.js +14 -0
- package/dist/core/db/db-postgres-adapter.js.map +1 -1
- package/dist/core/db/db-row.d.ts +7 -1
- package/dist/core/db/db-row.js +53 -0
- package/dist/core/db/db-row.js.map +1 -1
- package/dist/core/db/db-studio-client.d.ts +1 -0
- package/dist/core/db/db-studio-client.js +401 -0
- package/dist/core/db/db-studio-client.js.map +1 -0
- package/dist/core/db/db-studio-html.js +54 -408
- package/dist/core/db/db-studio-html.js.map +1 -1
- package/dist/core/db/db-studio-server.js +63 -0
- package/dist/core/db/db-studio-server.js.map +1 -1
- package/dist/core/db/db-studio-styles.d.ts +1 -0
- package/dist/core/db/db-studio-styles.js +225 -0
- package/dist/core/db/db-studio-styles.js.map +1 -0
- package/dist/core/db/db-types.d.ts +40 -0
- package/package.json +1 -1
- package/src/core/db/db-cache.ts +27 -0
- package/src/core/db/db-hana-adapter.ts +12 -0
- package/src/core/db/db-postgres-adapter.ts +18 -0
- package/src/core/db/db-row.ts +65 -1
- package/src/core/db/db-studio-client.ts +397 -0
- package/src/core/db/db-studio-html.ts +54 -408
- package/src/core/db/db-studio-server.ts +70 -3
- package/src/core/db/db-studio-styles.ts +221 -0
- package/src/core/db/db-types.ts +48 -0
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
listPublicConnections,
|
|
11
11
|
removeConnection,
|
|
12
12
|
renameConnection,
|
|
13
|
+
updateConnectionFields,
|
|
13
14
|
upsertConnectionFromDraft,
|
|
14
15
|
} from "./db-cache";
|
|
15
|
-
import type { TConnectionDraft } from "./db-cache";
|
|
16
|
+
import type { TConnectionDraft, TConnectionFieldPatch } from "./db-cache";
|
|
16
17
|
import { testConnectionProfile } from "./db-connection";
|
|
17
18
|
import {
|
|
18
19
|
analyzeSqlSafety,
|
|
@@ -22,14 +23,14 @@ import {
|
|
|
22
23
|
generateSelectSql,
|
|
23
24
|
looksLikeProduction,
|
|
24
25
|
} from "./db-metadata";
|
|
25
|
-
import type { TResolvedDatabaseConnection } from "./db-types";
|
|
26
|
+
import type { TResolvedDatabaseConnection, TTableChangeSet } from "./db-types";
|
|
26
27
|
import {
|
|
27
28
|
deleteSavedQuery,
|
|
28
29
|
listSavedQueries,
|
|
29
30
|
renameSavedQuery,
|
|
30
31
|
saveQuery,
|
|
31
32
|
} from "./db-query-files";
|
|
32
|
-
import { deleteRow, insertRow, updateRow } from "./db-row";
|
|
33
|
+
import { deleteRow, insertRow, saveTableChanges, updateRow } from "./db-row";
|
|
33
34
|
import { appendQueryHistory, listQueryHistory } from "./db-query-history";
|
|
34
35
|
import {
|
|
35
36
|
detectAppDatabaseServices,
|
|
@@ -125,10 +126,20 @@ function getNumber(body: TJsonBody, key: string, fallback: number): number {
|
|
|
125
126
|
return Number.isFinite(value) ? value : fallback;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
const VALID_ENVIRONMENTS = new Set(["DEV", "QAS", "PROD", "SANDBOX", "CUSTOM"]);
|
|
130
|
+
|
|
131
|
+
function getEnvironment(body: TJsonBody): TConnectionDraft["environment"] {
|
|
132
|
+
const value = getString(body, "environment").toUpperCase();
|
|
133
|
+
return VALID_ENVIRONMENTS.has(value) ? (value as TConnectionDraft["environment"]) : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
128
136
|
function draftFromBody(body: TJsonBody): TConnectionDraft {
|
|
129
137
|
const type = getString(body, "type") === "hana" ? "hana" : "postgresql";
|
|
130
138
|
return {
|
|
131
139
|
name: getString(body, "name") || `${type} connection`,
|
|
140
|
+
color: getString(body, "color") || undefined,
|
|
141
|
+
environment: getEnvironment(body),
|
|
142
|
+
isFavorite: body.isFavorite === undefined ? undefined : Boolean(body.isFavorite),
|
|
132
143
|
type,
|
|
133
144
|
host: getString(body, "host"),
|
|
134
145
|
port: getNumber(body, "port", type === "hana" ? 443 : 5432),
|
|
@@ -226,6 +237,21 @@ export async function startStudioServer(options: TStudioServerOptions = {}): Pro
|
|
|
226
237
|
return;
|
|
227
238
|
}
|
|
228
239
|
|
|
240
|
+
if (pathname === "/api/connections/update" && method === "POST") {
|
|
241
|
+
const body = await readJsonBody(req);
|
|
242
|
+
const patch: TConnectionFieldPatch = {};
|
|
243
|
+
if (typeof body.name === "string") patch.name = body.name;
|
|
244
|
+
if (typeof body.color === "string") patch.color = body.color;
|
|
245
|
+
if (body.environment !== undefined) patch.environment = getEnvironment(body);
|
|
246
|
+
if (body.isFavorite !== undefined) patch.isFavorite = Boolean(body.isFavorite);
|
|
247
|
+
if (Array.isArray(body.tags)) patch.tags = body.tags as string[];
|
|
248
|
+
const profile = await updateConnectionFields(getString(body, "id"), patch);
|
|
249
|
+
const { encryptedPassword: _omit, ...publicProfile } = profile;
|
|
250
|
+
void _omit;
|
|
251
|
+
sendJson(res, { connection: publicProfile });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
229
255
|
if (pathname === "/api/connections/duplicate" && method === "POST") {
|
|
230
256
|
const body = await readJsonBody(req);
|
|
231
257
|
const profile = await duplicateConnection(getString(body, "id"));
|
|
@@ -337,6 +363,25 @@ export async function startStudioServer(options: TStudioServerOptions = {}): Pro
|
|
|
337
363
|
return;
|
|
338
364
|
}
|
|
339
365
|
|
|
366
|
+
if (pathname === "/api/catalog/primary-key" && method === "GET") {
|
|
367
|
+
const adapter = await pool.getAdapter(url.searchParams.get("connectionId") ?? "");
|
|
368
|
+
const primaryKey = await adapter.getPrimaryKey(url.searchParams.get("schema") ?? "", url.searchParams.get("table") ?? "");
|
|
369
|
+
sendJson(res, { primaryKey });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (pathname === "/api/catalog/constraints" && method === "GET") {
|
|
374
|
+
const adapter = await pool.getAdapter(url.searchParams.get("connectionId") ?? "");
|
|
375
|
+
const schema = url.searchParams.get("schema") ?? "";
|
|
376
|
+
const table = url.searchParams.get("table") ?? "";
|
|
377
|
+
const [primaryKey, indexes] = await Promise.all([
|
|
378
|
+
adapter.getPrimaryKey(schema, table),
|
|
379
|
+
adapter.listIndexes(schema, table).catch(() => []),
|
|
380
|
+
]);
|
|
381
|
+
sendJson(res, { primaryKey, indexes });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
340
385
|
// --- Table data ----------------------------------------------------------
|
|
341
386
|
if (pathname === "/api/table/data" && method === "POST") {
|
|
342
387
|
const body = await readJsonBody(req);
|
|
@@ -391,6 +436,28 @@ export async function startStudioServer(options: TStudioServerOptions = {}): Pro
|
|
|
391
436
|
return;
|
|
392
437
|
}
|
|
393
438
|
|
|
439
|
+
if (pathname === "/api/table/save-changes" && method === "POST") {
|
|
440
|
+
const body = await readJsonBody(req);
|
|
441
|
+
const readOnly = body.readOnly === undefined ? serverReadOnlyDefault : Boolean(body.readOnly);
|
|
442
|
+
|
|
443
|
+
if (readOnly) {
|
|
444
|
+
sendJson(res, { ok: false, blocked: true, error: "Read-only mode is on. Turn it off to save changes." });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const adapter = await pool.getAdapter(getString(body, "connectionId"));
|
|
449
|
+
const result = await saveTableChanges(adapter, {
|
|
450
|
+
schema: getString(body, "schema"),
|
|
451
|
+
table: getString(body, "table"),
|
|
452
|
+
primaryKeyColumns: Array.isArray(body.primaryKeyColumns) ? (body.primaryKeyColumns as string[]) : [],
|
|
453
|
+
updates: Array.isArray(body.updates) ? (body.updates as TTableChangeSet["updates"]) : [],
|
|
454
|
+
inserts: Array.isArray(body.inserts) ? (body.inserts as TTableChangeSet["inserts"]) : [],
|
|
455
|
+
deletes: Array.isArray(body.deletes) ? (body.deletes as TTableChangeSet["deletes"]) : [],
|
|
456
|
+
});
|
|
457
|
+
sendJson(res, { ok: true, result });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
394
461
|
if (pathname === "/api/table/sql" && method === "POST") {
|
|
395
462
|
const body = await readJsonBody(req);
|
|
396
463
|
const adapter = await pool.getAdapter(getString(body, "connectionId"));
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
export const STUDIO_STYLES = `
|
|
2
|
+
:root{
|
|
3
|
+
--bg:#0b0f17;--bg-2:#0e1420;--bg-3:#121a28;--panel:#0d131e;--elev:#16203250;
|
|
4
|
+
--border:#1f2c44;--border-2:#27374f;--text:#dce6f5;--muted:#8295b5;--faint:#5b6c8a;
|
|
5
|
+
--accent:#3b82f6;--accent-2:#1d4ed8;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;
|
|
6
|
+
--amber:#fbbf24;--chip:#1a2840;--sel:#16243d;--editbg:#3a2f08;--delbg:#3a1414;--insbg:#0f2e18;
|
|
7
|
+
}
|
|
8
|
+
*{box-sizing:border-box}
|
|
9
|
+
html,body{height:100%;margin:0}
|
|
10
|
+
body{background:var(--bg);color:var(--text);font:13px/1.45 "Segoe UI",Roboto,Arial,sans-serif;overflow:hidden}
|
|
11
|
+
button,input,select,textarea{font:inherit;color:inherit;outline:none}
|
|
12
|
+
::-webkit-scrollbar{width:10px;height:10px}
|
|
13
|
+
::-webkit-scrollbar-thumb{background:#22314c;border-radius:8px}
|
|
14
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
15
|
+
.app{display:flex;flex-direction:column;height:100vh}
|
|
16
|
+
.hidden{display:none!important}
|
|
17
|
+
.spin{width:14px;height:14px;border:2px solid var(--border-2);border-top-color:var(--accent);border-radius:50%;display:inline-block;animation:sp .7s linear infinite;vertical-align:middle}
|
|
18
|
+
@keyframes sp{to{transform:rotate(360deg)}}
|
|
19
|
+
svg.ic{width:15px;height:15px;vertical-align:-2px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
20
|
+
|
|
21
|
+
/* top bar */
|
|
22
|
+
.topbar{display:flex;align-items:center;gap:9px;padding:8px 12px;background:linear-gradient(180deg,#101a2b,#0d1521);border-bottom:1px solid var(--border);flex:0 0 auto}
|
|
23
|
+
.brand{font-weight:700;color:#cfe0ff;letter-spacing:.2px;white-space:nowrap}
|
|
24
|
+
.brand .b2{color:var(--faint);font-weight:600}
|
|
25
|
+
.badge{padding:3px 9px;border-radius:999px;background:var(--chip);border:1px solid var(--border);color:var(--muted);font-size:12px;white-space:nowrap;display:inline-flex;align-items:center;gap:5px}
|
|
26
|
+
.badge.on{color:#cfe0ff;border-color:var(--accent)}
|
|
27
|
+
.badge.hana{color:#7dd3fc;border-color:#0e7490}
|
|
28
|
+
.badge.pg{color:#a5b4fc;border-color:#4338ca}
|
|
29
|
+
.badge.ro{cursor:pointer}
|
|
30
|
+
.badge.ro.active{color:#fff;background:#7c2d12;border-color:#b45309}
|
|
31
|
+
.badge.prod{color:#fff;background:#7f1d1d;border-color:#b91c1c}
|
|
32
|
+
.grow{flex:1}
|
|
33
|
+
.top-search{flex:0 1 320px}
|
|
34
|
+
.iconbtn{background:transparent;border:1px solid var(--border);border-radius:8px;padding:6px 8px;color:var(--muted);cursor:pointer;display:inline-flex;align-items:center;gap:6px}
|
|
35
|
+
.iconbtn:hover{color:#cfe0ff;border-color:var(--border-2);background:var(--bg-3)}
|
|
36
|
+
.iconbtn.primary{background:var(--accent);border-color:var(--accent-2);color:#fff}
|
|
37
|
+
.iconbtn.primary:hover{filter:brightness(1.08);color:#fff}
|
|
38
|
+
|
|
39
|
+
/* body */
|
|
40
|
+
.body{display:flex;flex:1;min-height:0}
|
|
41
|
+
.sidebar{width:320px;min-width:220px;max-width:560px;background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
|
|
42
|
+
.resizer{width:5px;cursor:col-resize}
|
|
43
|
+
.resizer:hover{background:var(--accent)}
|
|
44
|
+
.workspace{flex:1;display:flex;flex-direction:column;min-width:0}
|
|
45
|
+
|
|
46
|
+
/* sidebar sections */
|
|
47
|
+
.side-sec{display:flex;flex-direction:column;min-height:0;border-bottom:1px solid var(--border)}
|
|
48
|
+
.side-sec.flex{flex:1}
|
|
49
|
+
.side-head{display:flex;align-items:center;gap:7px;padding:9px 11px;cursor:pointer;color:var(--muted);user-select:none}
|
|
50
|
+
.side-head:hover{color:#cfe0ff}
|
|
51
|
+
.side-head .h-title{font-size:11px;text-transform:uppercase;letter-spacing:.7px;font-weight:700;flex:1}
|
|
52
|
+
.side-head .chev{transition:transform .15s}
|
|
53
|
+
.side-sec.collapsed .chev{transform:rotate(-90deg)}
|
|
54
|
+
.side-sec.collapsed .side-body{display:none}
|
|
55
|
+
.side-body{padding:0 9px 10px;overflow:auto;min-height:0}
|
|
56
|
+
.side-sec.flex .side-body{flex:1}
|
|
57
|
+
.side-actions{display:flex;gap:6px;margin-bottom:7px}
|
|
58
|
+
|
|
59
|
+
/* search box */
|
|
60
|
+
.searchbox{display:flex;align-items:center;gap:6px;background:var(--bg-3);border:1px solid var(--border);border-radius:8px;padding:5px 8px;margin-bottom:7px}
|
|
61
|
+
.searchbox svg{color:var(--faint)}
|
|
62
|
+
.searchbox input{flex:1;background:transparent;border:0;color:var(--text)}
|
|
63
|
+
.searchbox .sbtn{background:transparent;border:0;color:var(--muted);cursor:pointer}
|
|
64
|
+
|
|
65
|
+
/* buttons + inputs */
|
|
66
|
+
.btn{background:var(--accent);border:1px solid var(--accent-2);color:#fff;border-radius:8px;padding:6px 11px;cursor:pointer}
|
|
67
|
+
.btn:hover{filter:brightness(1.08)}
|
|
68
|
+
.btn.sec{background:#22304a;border-color:var(--border-2);color:#cfe0ff}
|
|
69
|
+
.btn.ghost{background:transparent;border-color:var(--border);color:var(--muted)}
|
|
70
|
+
.btn.danger{background:#7f1d1d;border-color:#b91c1c;color:#fff}
|
|
71
|
+
.btn.sm{padding:3px 8px;font-size:12px}
|
|
72
|
+
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
|
|
73
|
+
.input,.select{width:100%;background:var(--bg-3);border:1px solid var(--border);border-radius:8px;padding:7px 9px;color:var(--text)}
|
|
74
|
+
.field{display:flex;flex-direction:column;gap:4px;margin-bottom:9px}
|
|
75
|
+
.field label{color:var(--muted);font-size:12px}
|
|
76
|
+
.note{color:var(--muted);font-size:12px}
|
|
77
|
+
.faint{color:var(--faint)}
|
|
78
|
+
.row{display:flex;gap:7px;align-items:center;flex-wrap:wrap}
|
|
79
|
+
.right{justify-content:flex-end}
|
|
80
|
+
|
|
81
|
+
/* connection cards */
|
|
82
|
+
.conn-card{position:relative;background:var(--bg-3);border:1px solid var(--border);border-left:3px solid var(--faint);border-radius:9px;padding:8px 10px;margin-bottom:7px;cursor:pointer}
|
|
83
|
+
.conn-card:hover{border-color:var(--border-2);background:#152033}
|
|
84
|
+
.conn-card.active{border-color:var(--accent);background:var(--sel)}
|
|
85
|
+
.conn-top{display:flex;align-items:center;gap:7px}
|
|
86
|
+
.conn-name{font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
87
|
+
.conn-sub{color:var(--muted);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
|
|
88
|
+
.conn-tags{display:flex;gap:5px;margin-top:5px;flex-wrap:wrap}
|
|
89
|
+
.tag{font-size:10.5px;padding:1px 7px;border-radius:999px;background:var(--chip);border:1px solid var(--border);color:var(--muted)}
|
|
90
|
+
.tag.env-DEV{color:#86efac;border-color:#166534}
|
|
91
|
+
.tag.env-QAS{color:#fde68a;border-color:#a16207}
|
|
92
|
+
.tag.env-PROD{color:#fecaca;border-color:#b91c1c}
|
|
93
|
+
.tag.env-SANDBOX{color:#a5b4fc;border-color:#4338ca}
|
|
94
|
+
.tag.type{color:#7dd3fc}
|
|
95
|
+
.star{cursor:pointer;color:var(--faint)}
|
|
96
|
+
.star.on{color:var(--amber)}
|
|
97
|
+
.skel{height:56px;border-radius:9px;margin-bottom:7px;background:linear-gradient(90deg,#121a28,#1a2740,#121a28);background-size:200% 100%;animation:shimmer 1.3s infinite}
|
|
98
|
+
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
|
99
|
+
|
|
100
|
+
/* tree */
|
|
101
|
+
.tree{font-size:12.5px}
|
|
102
|
+
.trow{display:flex;align-items:center;gap:5px;padding:3px 5px;border-radius:6px;cursor:pointer;white-space:nowrap}
|
|
103
|
+
.trow:hover{background:var(--bg-3)}
|
|
104
|
+
.trow.sel{background:var(--sel)}
|
|
105
|
+
.tchev{width:13px;color:var(--faint);flex:0 0 auto;text-align:center;transition:transform .12s}
|
|
106
|
+
.tchev.open{transform:rotate(90deg)}
|
|
107
|
+
.tchev.leaf{visibility:hidden}
|
|
108
|
+
.ticon{flex:0 0 auto;color:var(--muted);display:inline-flex}
|
|
109
|
+
.ticon.tbl{color:#7dd3fc}.ticon.viw{color:#c4b5fd}.ticon.prc{color:#fca5a5}.ticon.fun{color:#fcd34d}.ticon.syn{color:#67e8f9}.ticon.sch{color:#93c5fd}.ticon.db{color:#86efac}.ticon.fld{color:var(--muted)}
|
|
110
|
+
.tlabel{flex:1;overflow:hidden;text-overflow:ellipsis}
|
|
111
|
+
.tbadge{color:var(--faint);font-size:11px}
|
|
112
|
+
.tchildren{margin-left:13px;border-left:1px solid var(--border);padding-left:3px}
|
|
113
|
+
.tnote{color:var(--faint);font-size:11.5px;padding:3px 6px}
|
|
114
|
+
.tsearch{margin:3px 0 4px}
|
|
115
|
+
|
|
116
|
+
/* workspace tabs */
|
|
117
|
+
.tabbar{display:flex;align-items:stretch;background:var(--bg-2);border-bottom:1px solid var(--border);overflow-x:auto;flex:0 0 auto;min-height:38px}
|
|
118
|
+
.wtab{display:flex;align-items:center;gap:7px;padding:8px 13px;border-right:1px solid var(--border);color:var(--muted);cursor:pointer;white-space:nowrap;max-width:260px}
|
|
119
|
+
.wtab:hover{color:var(--text);background:var(--bg-3)}
|
|
120
|
+
.wtab.active{color:#bcd4ff;background:var(--bg-3);box-shadow:inset 0 -2px 0 var(--accent)}
|
|
121
|
+
.wtab .t-ico{display:inline-flex;color:var(--faint)}
|
|
122
|
+
.wtab .t-title{overflow:hidden;text-overflow:ellipsis}
|
|
123
|
+
.wtab .dot{width:7px;height:7px;border-radius:50%;background:var(--amber)}
|
|
124
|
+
.wtab .x{color:var(--faint);border-radius:4px;padding:0 3px}
|
|
125
|
+
.wtab .x:hover{color:#fff;background:#37425c}
|
|
126
|
+
.tabcontent{flex:1;min-height:0;position:relative;background:var(--bg)}
|
|
127
|
+
.tabpane{position:absolute;inset:0;display:flex;flex-direction:column;overflow:auto}
|
|
128
|
+
|
|
129
|
+
/* welcome */
|
|
130
|
+
.welcome{padding:26px;max-width:1000px;margin:0 auto;width:100%}
|
|
131
|
+
.welcome h1{margin:0 0 4px;font-size:22px}
|
|
132
|
+
.welcome .lede{color:var(--muted);margin-bottom:22px}
|
|
133
|
+
.wcards{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px;margin-bottom:24px}
|
|
134
|
+
.wcard{background:var(--bg-2);border:1px solid var(--border);border-radius:12px;padding:16px;cursor:pointer}
|
|
135
|
+
.wcard:hover{border-color:var(--accent);background:var(--bg-3)}
|
|
136
|
+
.wcard .wc-ic{color:var(--accent);margin-bottom:8px}
|
|
137
|
+
.wcard h3{margin:0 0 4px;font-size:14px}
|
|
138
|
+
.wcard p{margin:0;color:var(--muted);font-size:12px}
|
|
139
|
+
.wcols{display:grid;grid-template-columns:1fr 1fr;gap:18px}
|
|
140
|
+
.wcol h4{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.6px;margin:0 0 8px}
|
|
141
|
+
.wlist .wli{padding:8px 10px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;cursor:pointer;background:var(--bg-2)}
|
|
142
|
+
.wlist .wli:hover{border-color:var(--border-2);background:var(--bg-3)}
|
|
143
|
+
|
|
144
|
+
/* toolbar + editor */
|
|
145
|
+
.toolbar{display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg-2)}
|
|
146
|
+
.pane-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:10px;gap:8px;overflow:auto}
|
|
147
|
+
.editor{width:100%;min-height:160px;resize:vertical;background:#0a1018;border:1px solid var(--border);border-radius:8px;padding:11px;color:#e7eefc;font-family:Consolas,"Cascadia Code",monospace;font-size:13px}
|
|
148
|
+
.errbox{background:#2a0f10;border:1px solid #7f1d1d;color:#fca5a5;border-radius:8px;padding:9px 11px;white-space:pre-wrap;font-family:Consolas,monospace;font-size:12px}
|
|
149
|
+
|
|
150
|
+
/* grid */
|
|
151
|
+
.gridwrap{flex:1;min-height:120px;overflow:auto;border:1px solid var(--border);border-radius:8px;background:var(--bg-3)}
|
|
152
|
+
table.grid{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px}
|
|
153
|
+
table.grid th,table.grid td{border-bottom:1px solid var(--border);border-right:1px solid var(--border);padding:6px 9px;text-align:left;white-space:nowrap;max-width:440px;overflow:hidden;text-overflow:ellipsis}
|
|
154
|
+
table.grid th{position:sticky;top:0;z-index:1;background:#172339;color:#cfe0ff;cursor:pointer;user-select:none}
|
|
155
|
+
table.grid th .sort{color:var(--accent);margin-left:4px}
|
|
156
|
+
table.grid tr:hover td{background:#13203450}
|
|
157
|
+
table.grid td.num{text-align:right;color:#a7f3d0}
|
|
158
|
+
table.grid th.rowhdr,table.grid td.rowhdr{width:54px;text-align:right;color:var(--faint);background:#15203550;cursor:pointer;position:sticky;left:0}
|
|
159
|
+
table.grid tr.selrow td{background:var(--sel)}
|
|
160
|
+
table.grid td.edited{background:var(--editbg);box-shadow:inset 0 0 0 1px var(--amber)}
|
|
161
|
+
table.grid tr.row-del td{background:var(--delbg);text-decoration:line-through;color:#fca5a5}
|
|
162
|
+
table.grid tr.row-ins td{background:var(--insbg)}
|
|
163
|
+
table.grid tr.row-err td{box-shadow:inset 0 0 0 1px var(--red)}
|
|
164
|
+
.rowflag{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px}
|
|
165
|
+
.rowflag.d{background:var(--amber)}.rowflag.del{background:var(--red)}.rowflag.ins{background:var(--green)}
|
|
166
|
+
.cellinput{width:100%;background:#0a1018;border:1px solid var(--accent);border-radius:4px;color:#fff;padding:3px 5px;font:inherit}
|
|
167
|
+
.rowerr-msg{color:#fca5a5;font-size:11px}
|
|
168
|
+
|
|
169
|
+
/* sql tabs inside editor */
|
|
170
|
+
.sqltabs{display:flex;gap:4px;flex-wrap:wrap;padding:6px 10px 0}
|
|
171
|
+
.sqltab{padding:4px 10px;background:var(--bg-3);border:1px solid var(--border);border-bottom:0;border-radius:7px 7px 0 0;cursor:pointer;color:var(--muted);font-size:12px}
|
|
172
|
+
.sqltab.active{color:#cfe0ff;border-color:var(--accent);background:var(--sel)}
|
|
173
|
+
.sqltab .x{margin-left:7px}
|
|
174
|
+
|
|
175
|
+
/* status bar */
|
|
176
|
+
.statusbar{display:flex;align-items:center;gap:16px;padding:5px 12px;background:var(--bg-2);border-top:1px solid var(--border);color:var(--muted);font-size:12px;flex:0 0 auto}
|
|
177
|
+
.st-item{display:inline-flex;align-items:center;gap:6px}
|
|
178
|
+
.st-dot{width:8px;height:8px;border-radius:50%;background:var(--faint)}
|
|
179
|
+
.st-dot.ok{background:var(--green)}.st-dot.err{background:var(--red)}.st-dot.run{background:var(--amber);animation:sp 1s linear infinite}
|
|
180
|
+
.st-pending{color:var(--amber)}
|
|
181
|
+
|
|
182
|
+
/* metadata */
|
|
183
|
+
.meta-tabs{display:flex;gap:5px;padding:8px 10px 0}
|
|
184
|
+
.meta-tab{padding:5px 11px;border:1px solid var(--border);border-bottom:0;border-radius:7px 7px 0 0;cursor:pointer;color:var(--muted);font-size:12px}
|
|
185
|
+
.meta-tab.active{color:#cfe0ff;border-color:var(--accent);background:var(--sel)}
|
|
186
|
+
.kvs{display:grid;grid-template-columns:160px 1fr;gap:5px 14px;padding:6px}
|
|
187
|
+
.kvs .k{color:var(--muted)}
|
|
188
|
+
.pill{display:inline-block;padding:1px 7px;border-radius:999px;background:var(--chip);border:1px solid var(--border);font-size:11px;color:var(--muted)}
|
|
189
|
+
.pill.pk{color:#fde68a;border-color:#a16207}
|
|
190
|
+
|
|
191
|
+
/* modal + wizard */
|
|
192
|
+
.modal{position:fixed;inset:0;background:rgba(2,6,15,.62);display:flex;align-items:center;justify-content:center;z-index:60}
|
|
193
|
+
.dialog{background:var(--bg-2);border:1px solid var(--border-2);border-radius:14px;padding:18px;width:560px;max-width:94vw;max-height:90vh;overflow:auto;box-shadow:0 24px 80px rgba(0,0,0,.5)}
|
|
194
|
+
.dialog h3{margin:0 0 14px}
|
|
195
|
+
.steps{display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap}
|
|
196
|
+
.step{flex:1;min-width:80px;text-align:center;font-size:11px;color:var(--faint);padding:6px 4px;border-radius:8px;background:var(--bg-3);border:1px solid var(--border)}
|
|
197
|
+
.step.active{color:#cfe0ff;border-color:var(--accent)}
|
|
198
|
+
.step.done{color:var(--green)}
|
|
199
|
+
.wlistbox{max-height:240px;overflow:auto;border:1px solid var(--border);border-radius:9px}
|
|
200
|
+
.wrow{padding:8px 10px;border-bottom:1px solid var(--border);cursor:pointer;display:flex;align-items:center;gap:8px}
|
|
201
|
+
.wrow:hover{background:var(--bg-3)}
|
|
202
|
+
.wrow.sel{background:var(--sel)}
|
|
203
|
+
.swatches{display:flex;gap:7px;flex-wrap:wrap}
|
|
204
|
+
.swatch{width:24px;height:24px;border-radius:7px;cursor:pointer;border:2px solid transparent}
|
|
205
|
+
.swatch.sel{border-color:#fff}
|
|
206
|
+
|
|
207
|
+
/* context menu */
|
|
208
|
+
.ctxmenu{position:fixed;z-index:80;background:var(--bg-2);border:1px solid var(--border-2);border-radius:9px;padding:5px;min-width:190px;box-shadow:0 16px 50px rgba(0,0,0,.5)}
|
|
209
|
+
.ctxitem{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:7px;cursor:pointer;color:var(--text);font-size:12.5px}
|
|
210
|
+
.ctxitem:hover{background:var(--accent);color:#fff}
|
|
211
|
+
.ctxitem.danger{color:#fca5a5}
|
|
212
|
+
.ctxitem.danger:hover{background:#7f1d1d;color:#fff}
|
|
213
|
+
.ctxsep{height:1px;background:var(--border);margin:4px 6px}
|
|
214
|
+
|
|
215
|
+
/* toasts */
|
|
216
|
+
.toasts{position:fixed;right:16px;bottom:16px;display:flex;flex-direction:column;gap:8px;z-index:90}
|
|
217
|
+
.toast{background:var(--bg-2);border:1px solid var(--border-2);border-left:3px solid var(--accent);border-radius:9px;padding:9px 13px;min-width:240px;max-width:380px;box-shadow:0 10px 34px rgba(0,0,0,.4);animation:slideup .18s ease}
|
|
218
|
+
.toast.ok{border-left-color:var(--green)}.toast.err{border-left-color:var(--red)}.toast.warn{border-left-color:var(--yellow)}
|
|
219
|
+
@keyframes slideup{from{transform:translateY(8px);opacity:0}to{transform:translateY(0);opacity:1}}
|
|
220
|
+
.empty{color:var(--faint);padding:16px;text-align:center}
|
|
221
|
+
`;
|
package/src/core/db/db-types.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export type TDatabaseType = "hana" | "postgresql";
|
|
2
2
|
|
|
3
|
+
export type TConnectionEnvironment = "DEV" | "QAS" | "PROD" | "SANDBOX" | "CUSTOM";
|
|
4
|
+
|
|
3
5
|
export type TDatabaseConnectionProfile = {
|
|
4
6
|
id: string;
|
|
5
7
|
name: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
environment?: TConnectionEnvironment;
|
|
10
|
+
isFavorite?: boolean;
|
|
6
11
|
type: TDatabaseType;
|
|
7
12
|
region?: string;
|
|
8
13
|
org?: string;
|
|
@@ -174,11 +179,54 @@ export type TTableDataOptions = {
|
|
|
174
179
|
* specifics (quoting, system catalog queries) so the rest of the studio is
|
|
175
180
|
* dialect-agnostic.
|
|
176
181
|
*/
|
|
182
|
+
export type TDatabasePrimaryKey = {
|
|
183
|
+
columns: string[];
|
|
184
|
+
constraintName?: string;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type TRowUpdate = {
|
|
188
|
+
key: Record<string, unknown>;
|
|
189
|
+
changes: Record<string, unknown>;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type TRowInsert = {
|
|
193
|
+
values: Record<string, unknown>;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type TRowDelete = {
|
|
197
|
+
key: Record<string, unknown>;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export type TTableChangeSet = {
|
|
201
|
+
schema: string;
|
|
202
|
+
table: string;
|
|
203
|
+
primaryKeyColumns: string[];
|
|
204
|
+
updates: TRowUpdate[];
|
|
205
|
+
inserts: TRowInsert[];
|
|
206
|
+
deletes: TRowDelete[];
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export type TSaveRowResult = {
|
|
210
|
+
type: "update" | "insert" | "delete";
|
|
211
|
+
success: boolean;
|
|
212
|
+
key?: Record<string, unknown>;
|
|
213
|
+
error?: string;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export type TSaveTableChangesResult = {
|
|
217
|
+
success: boolean;
|
|
218
|
+
updated: number;
|
|
219
|
+
inserted: number;
|
|
220
|
+
deleted: number;
|
|
221
|
+
rowResults: TSaveRowResult[];
|
|
222
|
+
};
|
|
223
|
+
|
|
177
224
|
export interface IDatabaseAdapter {
|
|
178
225
|
readonly type: TDatabaseType;
|
|
179
226
|
connect(): Promise<void>;
|
|
180
227
|
disconnect(): Promise<void>;
|
|
181
228
|
testConnection(): Promise<TConnectionTestResult>;
|
|
229
|
+
getPrimaryKey(schema: string, table: string): Promise<TDatabasePrimaryKey>;
|
|
182
230
|
runQuery(sql: string, options?: { maxRows?: number }): Promise<TDatabaseQueryResult>;
|
|
183
231
|
/** Run a parameterized statement. Placeholders come from `placeholder()`. */
|
|
184
232
|
runParameterized(sql: string, params: unknown[], options?: { maxRows?: number }): Promise<TDatabaseQueryResult>;
|