@mountsqli/studio 0.1.6 → 0.1.7
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/dist/index.js +1063 -306
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,40 +6,91 @@ function getDashboardHTML() {
|
|
|
6
6
|
<meta charset="UTF-8">
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
8
|
<title>MountSQLi Studio</title>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
</style>
|
|
9
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>\u25C6</text></svg>">
|
|
10
|
+
<style>${getCSS()}</style>
|
|
12
11
|
</head>
|
|
13
12
|
<body>
|
|
14
13
|
<div id="app">
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
14
|
+
<!-- Top bar -->
|
|
15
|
+
<header id="topbar">
|
|
16
|
+
<div class="topbar-left">
|
|
17
|
+
<div class="logo">
|
|
18
|
+
<span class="logo-icon">\u25C6</span>
|
|
19
|
+
<span class="logo-text">MountSQLi</span>
|
|
20
|
+
<span class="logo-badge">Studio</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="topbar-center">
|
|
24
|
+
<div class="search-box">
|
|
25
|
+
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
26
|
+
<input type="text" id="global-search" placeholder="Search tables, columns... (Ctrl+K)" autocomplete="off">
|
|
27
|
+
<kbd class="search-kbd">\u2318K</kbd>
|
|
28
|
+
</div>
|
|
20
29
|
</div>
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
<div class="topbar-right">
|
|
31
|
+
<div id="connection-pill" class="pill pill-ok">
|
|
32
|
+
<span class="pill-dot"></span>
|
|
33
|
+
<span id="conn-dialect">PostgreSQL</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<div id="body">
|
|
39
|
+
<!-- Sidebar -->
|
|
40
|
+
<nav id="sidebar">
|
|
41
|
+
<div class="nav-section">
|
|
42
|
+
<div class="nav-label">Explorer</div>
|
|
43
|
+
<a href="#" data-page="tables" class="nav-link active">
|
|
44
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
45
|
+
<span>Tables</span>
|
|
46
|
+
</a>
|
|
47
|
+
<a href="#" data-page="query" class="nav-link">
|
|
48
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|
49
|
+
<span>Query</span>
|
|
50
|
+
</a>
|
|
51
|
+
<a href="#" data-page="erd" class="nav-link">
|
|
52
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/><line x1="12" y1="8" x2="5" y2="16"/><line x1="12" y1="8" x2="19" y2="16"/></svg>
|
|
53
|
+
<span>ER Diagram</span>
|
|
54
|
+
</a>
|
|
55
|
+
<a href="#" data-page="migrations" class="nav-link">
|
|
56
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
|
|
57
|
+
<span>Migrations</span>
|
|
58
|
+
</a>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="nav-section">
|
|
61
|
+
<div class="nav-label">System</div>
|
|
62
|
+
<a href="#" data-page="settings" class="nav-link">
|
|
63
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
64
|
+
<span>Settings</span>
|
|
65
|
+
</a>
|
|
66
|
+
</div>
|
|
67
|
+
</nav>
|
|
68
|
+
|
|
69
|
+
<!-- Main content -->
|
|
70
|
+
<main id="content">
|
|
71
|
+
<div id="page-tables" class="page active"></div>
|
|
72
|
+
<div id="page-query" class="page"></div>
|
|
73
|
+
<div id="page-erd" class="page"></div>
|
|
74
|
+
<div id="page-migrations" class="page"></div>
|
|
75
|
+
<div id="page-settings" class="page"></div>
|
|
76
|
+
</main>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Search modal -->
|
|
81
|
+
<div id="search-modal" class="modal" style="display:none">
|
|
82
|
+
<div class="modal-backdrop"></div>
|
|
83
|
+
<div class="modal-content search-modal-content">
|
|
84
|
+
<div class="search-modal-input">
|
|
85
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
86
|
+
<input type="text" id="modal-search-input" placeholder="Search tables, columns..." autocomplete="off">
|
|
87
|
+
<kbd>ESC</kbd>
|
|
30
88
|
</div>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<div id="page-tables" class="page active"></div>
|
|
34
|
-
<div id="page-query" class="page"></div>
|
|
35
|
-
<div id="page-erd" class="page"></div>
|
|
36
|
-
<div id="page-migrations" class="page"></div>
|
|
37
|
-
<div id="page-settings" class="page"></div>
|
|
38
|
-
</main>
|
|
89
|
+
<div id="search-results" class="search-results"></div>
|
|
90
|
+
</div>
|
|
39
91
|
</div>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</script>
|
|
92
|
+
|
|
93
|
+
<script>${getJS()}</script>
|
|
43
94
|
</body>
|
|
44
95
|
</html>`;
|
|
45
96
|
}
|
|
@@ -48,367 +99,767 @@ function getCSS() {
|
|
|
48
99
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
49
100
|
|
|
50
101
|
:root {
|
|
51
|
-
--bg: #
|
|
52
|
-
--bg-
|
|
53
|
-
--bg-
|
|
54
|
-
--bg-
|
|
55
|
-
--
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
--
|
|
64
|
-
--
|
|
65
|
-
--
|
|
102
|
+
--bg-0: #0a0b10;
|
|
103
|
+
--bg-1: #111219;
|
|
104
|
+
--bg-2: #181924;
|
|
105
|
+
--bg-3: #1f2133;
|
|
106
|
+
--bg-4: #272a3f;
|
|
107
|
+
--border: rgba(255,255,255,0.06);
|
|
108
|
+
--border-strong: rgba(255,255,255,0.12);
|
|
109
|
+
--text: #e8eaed;
|
|
110
|
+
--text-2: #9ba1b0;
|
|
111
|
+
--text-3: #5f6577;
|
|
112
|
+
--accent: #6c5ce7;
|
|
113
|
+
--accent-2: #a29bfe;
|
|
114
|
+
--accent-bg: rgba(108,92,231,0.12);
|
|
115
|
+
--green: #00d68f;
|
|
116
|
+
--green-bg: rgba(0,214,143,0.1);
|
|
117
|
+
--yellow: #ffc048;
|
|
118
|
+
--yellow-bg: rgba(255,192,72,0.1);
|
|
119
|
+
--red: #ff6b6b;
|
|
120
|
+
--red-bg: rgba(255,107,107,0.1);
|
|
121
|
+
--blue: #4fc3f7;
|
|
122
|
+
--blue-bg: rgba(79,195,247,0.1);
|
|
123
|
+
--radius-sm: 6px;
|
|
124
|
+
--radius: 10px;
|
|
125
|
+
--radius-lg: 14px;
|
|
126
|
+
--shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
127
|
+
--shadow-lg: 0 8px 32px rgba(0,0,0,0.4);
|
|
128
|
+
--font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
|
|
129
|
+
--font: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
|
|
130
|
+
--transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
66
131
|
}
|
|
67
132
|
|
|
68
133
|
body {
|
|
69
|
-
font-family: var(--font
|
|
70
|
-
background: var(--bg);
|
|
134
|
+
font-family: var(--font);
|
|
135
|
+
background: var(--bg-0);
|
|
71
136
|
color: var(--text);
|
|
72
137
|
height: 100vh;
|
|
73
138
|
overflow: hidden;
|
|
139
|
+
-webkit-font-smoothing: antialiased;
|
|
74
140
|
}
|
|
75
141
|
|
|
76
142
|
#app {
|
|
77
143
|
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
78
145
|
height: 100vh;
|
|
79
146
|
}
|
|
80
147
|
|
|
81
|
-
/*
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
background: var(--bg-
|
|
85
|
-
border-
|
|
148
|
+
/* ---- Top Bar ---- */
|
|
149
|
+
#topbar {
|
|
150
|
+
height: 52px;
|
|
151
|
+
background: var(--bg-1);
|
|
152
|
+
border-bottom: 1px solid var(--border);
|
|
86
153
|
display: flex;
|
|
87
|
-
|
|
88
|
-
padding: 16px
|
|
154
|
+
align-items: center;
|
|
155
|
+
padding: 0 16px;
|
|
156
|
+
gap: 16px;
|
|
157
|
+
flex-shrink: 0;
|
|
158
|
+
z-index: 10;
|
|
89
159
|
}
|
|
90
160
|
|
|
161
|
+
.topbar-left { display: flex; align-items: center; }
|
|
162
|
+
.topbar-center { flex: 1; display: flex; justify-content: center; max-width: 480px; margin: 0 auto; }
|
|
163
|
+
.topbar-right { display: flex; align-items: center; gap: 12px; }
|
|
164
|
+
|
|
91
165
|
.logo {
|
|
92
166
|
display: flex;
|
|
93
167
|
align-items: center;
|
|
94
168
|
gap: 8px;
|
|
95
|
-
padding: 0 20px 20px;
|
|
96
|
-
border-bottom: 1px solid var(--border);
|
|
97
169
|
}
|
|
98
170
|
|
|
99
|
-
.logo-icon { font-size:
|
|
100
|
-
.logo-text { font-weight: 700; font-size:
|
|
171
|
+
.logo-icon { font-size: 18px; color: var(--accent); }
|
|
172
|
+
.logo-text { font-weight: 700; font-size: 14px; letter-spacing: -0.3px; }
|
|
101
173
|
.logo-badge {
|
|
102
|
-
font-size:
|
|
174
|
+
font-size: 9px;
|
|
103
175
|
background: var(--accent);
|
|
104
176
|
color: white;
|
|
105
177
|
padding: 2px 6px;
|
|
106
178
|
border-radius: 4px;
|
|
179
|
+
font-weight: 700;
|
|
180
|
+
letter-spacing: 0.5px;
|
|
181
|
+
text-transform: uppercase;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.search-box {
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 8px;
|
|
188
|
+
background: var(--bg-2);
|
|
189
|
+
border: 1px solid var(--border);
|
|
190
|
+
border-radius: var(--radius);
|
|
191
|
+
padding: 6px 12px;
|
|
192
|
+
width: 100%;
|
|
193
|
+
cursor: text;
|
|
194
|
+
transition: border-color var(--transition);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.search-box:focus-within { border-color: var(--accent); }
|
|
198
|
+
.search-icon { color: var(--text-3); flex-shrink: 0; }
|
|
199
|
+
.search-box input {
|
|
200
|
+
background: none;
|
|
201
|
+
border: none;
|
|
202
|
+
outline: none;
|
|
203
|
+
color: var(--text);
|
|
204
|
+
font-size: 13px;
|
|
205
|
+
width: 100%;
|
|
206
|
+
font-family: var(--font);
|
|
207
|
+
}
|
|
208
|
+
.search-box input::placeholder { color: var(--text-3); }
|
|
209
|
+
.search-kbd {
|
|
210
|
+
font-size: 10px;
|
|
211
|
+
padding: 2px 5px;
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
background: var(--bg-3);
|
|
214
|
+
border: 1px solid var(--border);
|
|
215
|
+
color: var(--text-3);
|
|
216
|
+
font-family: var(--font);
|
|
217
|
+
flex-shrink: 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.pill {
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: 6px;
|
|
224
|
+
padding: 4px 10px;
|
|
225
|
+
border-radius: 20px;
|
|
226
|
+
font-size: 11px;
|
|
107
227
|
font-weight: 600;
|
|
108
228
|
}
|
|
109
229
|
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
230
|
+
.pill-ok { background: var(--green-bg); color: var(--green); }
|
|
231
|
+
.pill-err { background: var(--red-bg); color: var(--red); }
|
|
232
|
+
.pill-dot {
|
|
233
|
+
width: 6px;
|
|
234
|
+
height: 6px;
|
|
235
|
+
border-radius: 50%;
|
|
236
|
+
background: currentColor;
|
|
237
|
+
animation: pulse 2s infinite;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@keyframes pulse {
|
|
241
|
+
0%, 100% { opacity: 1; }
|
|
242
|
+
50% { opacity: 0.4; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ---- Body ---- */
|
|
246
|
+
#body {
|
|
247
|
+
display: flex;
|
|
113
248
|
flex: 1;
|
|
249
|
+
overflow: hidden;
|
|
114
250
|
}
|
|
115
251
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
252
|
+
/* ---- Sidebar ---- */
|
|
253
|
+
#sidebar {
|
|
254
|
+
width: 200px;
|
|
255
|
+
background: var(--bg-1);
|
|
256
|
+
border-right: 1px solid var(--border);
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
padding: 12px 8px;
|
|
260
|
+
gap: 16px;
|
|
261
|
+
flex-shrink: 0;
|
|
123
262
|
}
|
|
124
263
|
|
|
125
|
-
.nav-
|
|
126
|
-
.nav-
|
|
264
|
+
.nav-section { display: flex; flex-direction: column; gap: 2px; }
|
|
265
|
+
.nav-label {
|
|
266
|
+
font-size: 10px;
|
|
267
|
+
font-weight: 700;
|
|
268
|
+
text-transform: uppercase;
|
|
269
|
+
letter-spacing: 0.8px;
|
|
270
|
+
color: var(--text-3);
|
|
271
|
+
padding: 4px 12px 6px;
|
|
272
|
+
}
|
|
127
273
|
|
|
128
|
-
.nav-
|
|
129
|
-
|
|
130
|
-
|
|
274
|
+
.nav-link {
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
gap: 10px;
|
|
278
|
+
padding: 8px 12px;
|
|
279
|
+
border-radius: var(--radius-sm);
|
|
280
|
+
color: var(--text-2);
|
|
281
|
+
text-decoration: none;
|
|
282
|
+
font-size: 13px;
|
|
283
|
+
font-weight: 500;
|
|
284
|
+
transition: all var(--transition);
|
|
131
285
|
}
|
|
132
286
|
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
display: inline-block;
|
|
287
|
+
.nav-link:hover { background: var(--bg-3); color: var(--text); }
|
|
288
|
+
.nav-link.active {
|
|
289
|
+
background: var(--accent-bg);
|
|
290
|
+
color: var(--accent-2);
|
|
138
291
|
}
|
|
139
292
|
|
|
140
|
-
.
|
|
141
|
-
.
|
|
293
|
+
.nav-link svg { flex-shrink: 0; opacity: 0.7; }
|
|
294
|
+
.nav-link.active svg { opacity: 1; }
|
|
142
295
|
|
|
143
|
-
/* Main
|
|
296
|
+
/* ---- Main Content ---- */
|
|
144
297
|
#content {
|
|
145
298
|
flex: 1;
|
|
146
299
|
overflow: auto;
|
|
147
|
-
padding: 24px;
|
|
300
|
+
padding: 20px 24px;
|
|
148
301
|
}
|
|
149
302
|
|
|
150
|
-
.page { display: none; }
|
|
303
|
+
.page { display: none; animation: fadeIn 200ms ease; }
|
|
151
304
|
.page.active { display: block; }
|
|
152
305
|
|
|
153
|
-
|
|
306
|
+
@keyframes fadeIn {
|
|
307
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
308
|
+
to { opacity: 1; transform: translateY(0); }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.page-title {
|
|
312
|
+
font-size: 20px;
|
|
313
|
+
font-weight: 700;
|
|
314
|
+
margin-bottom: 4px;
|
|
315
|
+
letter-spacing: -0.3px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.page-subtitle {
|
|
319
|
+
font-size: 13px;
|
|
320
|
+
color: var(--text-3);
|
|
321
|
+
margin-bottom: 20px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* ---- Cards ---- */
|
|
325
|
+
.card {
|
|
326
|
+
background: var(--bg-2);
|
|
327
|
+
border: 1px solid var(--border);
|
|
328
|
+
border-radius: var(--radius-lg);
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.card-header {
|
|
333
|
+
padding: 14px 18px;
|
|
334
|
+
border-bottom: 1px solid var(--border);
|
|
335
|
+
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
justify-content: space-between;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.card-title {
|
|
341
|
+
font-size: 13px;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 8px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.card-body { padding: 0; }
|
|
349
|
+
|
|
350
|
+
/* ---- Table Grid ---- */
|
|
154
351
|
.table-grid {
|
|
155
352
|
display: grid;
|
|
156
|
-
grid-template-columns:
|
|
353
|
+
grid-template-columns: 240px 1fr;
|
|
157
354
|
gap: 16px;
|
|
158
|
-
height: calc(100vh -
|
|
355
|
+
height: calc(100vh - 140px);
|
|
159
356
|
}
|
|
160
357
|
|
|
161
|
-
.table-
|
|
162
|
-
|
|
163
|
-
|
|
358
|
+
.table-sidebar {
|
|
359
|
+
display: flex;
|
|
360
|
+
flex-direction: column;
|
|
361
|
+
gap: 12px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.table-search {
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 8px;
|
|
368
|
+
background: var(--bg-2);
|
|
164
369
|
border: 1px solid var(--border);
|
|
165
|
-
|
|
370
|
+
border-radius: var(--radius);
|
|
371
|
+
padding: 8px 12px;
|
|
166
372
|
}
|
|
167
373
|
|
|
168
|
-
.table-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
374
|
+
.table-search input {
|
|
375
|
+
background: none;
|
|
376
|
+
border: none;
|
|
377
|
+
outline: none;
|
|
378
|
+
color: var(--text);
|
|
379
|
+
font-size: 12px;
|
|
380
|
+
width: 100%;
|
|
381
|
+
font-family: var(--font);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.table-search input::placeholder { color: var(--text-3); }
|
|
385
|
+
|
|
386
|
+
.table-list {
|
|
387
|
+
flex: 1;
|
|
388
|
+
overflow-y: auto;
|
|
389
|
+
display: flex;
|
|
390
|
+
flex-direction: column;
|
|
391
|
+
gap: 1px;
|
|
174
392
|
}
|
|
175
393
|
|
|
176
394
|
.table-item {
|
|
177
|
-
padding:
|
|
395
|
+
padding: 9px 14px;
|
|
178
396
|
cursor: pointer;
|
|
179
397
|
font-size: 13px;
|
|
180
|
-
|
|
181
|
-
|
|
398
|
+
font-family: var(--font-mono);
|
|
399
|
+
border-radius: var(--radius-sm);
|
|
400
|
+
transition: all var(--transition);
|
|
401
|
+
display: flex;
|
|
402
|
+
align-items: center;
|
|
403
|
+
gap: 8px;
|
|
404
|
+
color: var(--text-2);
|
|
182
405
|
}
|
|
183
406
|
|
|
184
|
-
.table-item:hover { background: var(--bg-
|
|
185
|
-
.table-item.active {
|
|
407
|
+
.table-item:hover { background: var(--bg-3); color: var(--text); }
|
|
408
|
+
.table-item.active { background: var(--accent-bg); color: var(--accent-2); }
|
|
409
|
+
.table-item .table-icon { font-size: 12px; opacity: 0.5; }
|
|
410
|
+
.table-item .table-count {
|
|
411
|
+
margin-left: auto;
|
|
412
|
+
font-size: 10px;
|
|
413
|
+
background: var(--bg-4);
|
|
414
|
+
padding: 1px 6px;
|
|
415
|
+
border-radius: 10px;
|
|
416
|
+
color: var(--text-3);
|
|
417
|
+
}
|
|
186
418
|
|
|
187
|
-
|
|
419
|
+
.table-count-wrap {
|
|
420
|
+
display: flex;
|
|
421
|
+
justify-content: space-between;
|
|
422
|
+
padding: 8px 14px;
|
|
423
|
+
font-size: 11px;
|
|
424
|
+
color: var(--text-3);
|
|
425
|
+
border-bottom: 1px solid var(--border);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* ---- Data Table ---- */
|
|
188
429
|
.data-panel {
|
|
189
|
-
background: var(--bg-surface);
|
|
190
|
-
border-radius: var(--radius);
|
|
191
|
-
border: 1px solid var(--border);
|
|
192
|
-
overflow: hidden;
|
|
193
430
|
display: flex;
|
|
194
431
|
flex-direction: column;
|
|
432
|
+
gap: 12px;
|
|
433
|
+
overflow: hidden;
|
|
195
434
|
}
|
|
196
435
|
|
|
197
|
-
.data-
|
|
198
|
-
padding: 12px 16px;
|
|
199
|
-
border-bottom: 1px solid var(--border);
|
|
436
|
+
.data-toolbar {
|
|
200
437
|
display: flex;
|
|
201
|
-
justify-content: space-between;
|
|
202
438
|
align-items: center;
|
|
439
|
+
gap: 10px;
|
|
203
440
|
}
|
|
204
441
|
|
|
205
|
-
.data-
|
|
206
|
-
|
|
442
|
+
.data-toolbar-title {
|
|
443
|
+
font-size: 16px;
|
|
444
|
+
font-weight: 700;
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: 8px;
|
|
448
|
+
}
|
|
207
449
|
|
|
208
|
-
.data-
|
|
450
|
+
.data-toolbar-title .badge {
|
|
451
|
+
font-size: 11px;
|
|
452
|
+
font-weight: 600;
|
|
453
|
+
background: var(--bg-4);
|
|
454
|
+
padding: 2px 8px;
|
|
455
|
+
border-radius: 10px;
|
|
456
|
+
color: var(--text-2);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.data-filter {
|
|
460
|
+
display: flex;
|
|
461
|
+
align-items: center;
|
|
462
|
+
gap: 6px;
|
|
463
|
+
margin-left: auto;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.data-filter input {
|
|
467
|
+
background: var(--bg-2);
|
|
468
|
+
border: 1px solid var(--border);
|
|
469
|
+
border-radius: var(--radius-sm);
|
|
470
|
+
padding: 6px 10px;
|
|
471
|
+
font-size: 12px;
|
|
472
|
+
color: var(--text);
|
|
473
|
+
outline: none;
|
|
474
|
+
width: 180px;
|
|
475
|
+
font-family: var(--font);
|
|
476
|
+
transition: border-color var(--transition);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.data-filter input:focus { border-color: var(--accent); }
|
|
480
|
+
.data-filter input::placeholder { color: var(--text-3); }
|
|
481
|
+
|
|
482
|
+
.data-table-card {
|
|
209
483
|
flex: 1;
|
|
484
|
+
overflow: hidden;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.data-table-scroll {
|
|
210
488
|
overflow: auto;
|
|
489
|
+
height: calc(100vh - 230px);
|
|
211
490
|
}
|
|
212
491
|
|
|
213
492
|
table {
|
|
214
493
|
width: 100%;
|
|
215
494
|
border-collapse: collapse;
|
|
216
495
|
font-size: 13px;
|
|
217
|
-
font-family: var(--font);
|
|
496
|
+
font-family: var(--font-mono);
|
|
218
497
|
}
|
|
219
498
|
|
|
220
|
-
th {
|
|
499
|
+
thead th {
|
|
221
500
|
position: sticky;
|
|
222
501
|
top: 0;
|
|
223
|
-
background: var(--bg-
|
|
224
|
-
padding: 10px
|
|
502
|
+
background: var(--bg-3);
|
|
503
|
+
padding: 10px 14px;
|
|
225
504
|
text-align: left;
|
|
226
505
|
font-weight: 600;
|
|
227
506
|
font-size: 11px;
|
|
228
507
|
text-transform: uppercase;
|
|
229
|
-
|
|
230
|
-
|
|
508
|
+
letter-spacing: 0.5px;
|
|
509
|
+
color: var(--text-3);
|
|
510
|
+
border-bottom: 1px solid var(--border-strong);
|
|
511
|
+
white-space: nowrap;
|
|
512
|
+
z-index: 1;
|
|
231
513
|
}
|
|
232
514
|
|
|
233
|
-
|
|
234
|
-
|
|
515
|
+
thead th:hover { color: var(--text-2); }
|
|
516
|
+
|
|
517
|
+
tbody td {
|
|
518
|
+
padding: 8px 14px;
|
|
235
519
|
border-bottom: 1px solid var(--border);
|
|
236
|
-
max-width:
|
|
520
|
+
max-width: 280px;
|
|
237
521
|
overflow: hidden;
|
|
238
522
|
text-overflow: ellipsis;
|
|
239
523
|
white-space: nowrap;
|
|
524
|
+
font-size: 12px;
|
|
525
|
+
color: var(--text-2);
|
|
526
|
+
transition: background var(--transition);
|
|
240
527
|
}
|
|
241
528
|
|
|
242
|
-
tr:hover td { background: var(--bg-
|
|
529
|
+
tbody tr:hover td { background: var(--bg-3); color: var(--text); }
|
|
243
530
|
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
|
|
531
|
+
td.cell-null { color: var(--text-3); font-style: italic; }
|
|
532
|
+
td.cell-pk { color: var(--yellow); }
|
|
533
|
+
|
|
534
|
+
/* ---- Buttons ---- */
|
|
535
|
+
.btn {
|
|
536
|
+
padding: 7px 14px;
|
|
537
|
+
border-radius: var(--radius-sm);
|
|
247
538
|
border: 1px solid var(--border);
|
|
248
|
-
|
|
249
|
-
|
|
539
|
+
background: var(--bg-2);
|
|
540
|
+
color: var(--text-2);
|
|
541
|
+
cursor: pointer;
|
|
542
|
+
font-size: 12px;
|
|
543
|
+
font-weight: 500;
|
|
250
544
|
font-family: var(--font);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
resize: vertical;
|
|
256
|
-
outline: none;
|
|
545
|
+
transition: all var(--transition);
|
|
546
|
+
display: inline-flex;
|
|
547
|
+
align-items: center;
|
|
548
|
+
gap: 6px;
|
|
257
549
|
}
|
|
258
550
|
|
|
259
|
-
.
|
|
551
|
+
.btn:hover { background: var(--bg-3); color: var(--text); border-color: var(--border-strong); }
|
|
552
|
+
.btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
|
|
553
|
+
.btn-primary:hover { background: #5b4bd5; }
|
|
554
|
+
.btn-sm { padding: 4px 10px; font-size: 11px; }
|
|
260
555
|
|
|
261
|
-
|
|
262
|
-
|
|
556
|
+
/* ---- Query Console ---- */
|
|
557
|
+
.query-layout {
|
|
263
558
|
display: flex;
|
|
264
|
-
|
|
559
|
+
flex-direction: column;
|
|
560
|
+
gap: 16px;
|
|
561
|
+
height: calc(100vh - 140px);
|
|
265
562
|
}
|
|
266
563
|
|
|
267
|
-
.
|
|
268
|
-
|
|
269
|
-
border-radius: var(--radius);
|
|
564
|
+
.query-editor-wrap {
|
|
565
|
+
background: var(--bg-2);
|
|
270
566
|
border: 1px solid var(--border);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
cursor: pointer;
|
|
274
|
-
font-size: 13px;
|
|
275
|
-
transition: all 0.15s;
|
|
567
|
+
border-radius: var(--radius);
|
|
568
|
+
overflow: hidden;
|
|
276
569
|
}
|
|
277
570
|
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
571
|
+
.query-editor-header {
|
|
572
|
+
padding: 8px 14px;
|
|
573
|
+
border-bottom: 1px solid var(--border);
|
|
574
|
+
display: flex;
|
|
575
|
+
align-items: center;
|
|
576
|
+
justify-content: space-between;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.query-editor-header span {
|
|
580
|
+
font-size: 11px;
|
|
581
|
+
color: var(--text-3);
|
|
582
|
+
font-weight: 600;
|
|
583
|
+
text-transform: uppercase;
|
|
584
|
+
letter-spacing: 0.5px;
|
|
585
|
+
}
|
|
281
586
|
|
|
282
|
-
.query-
|
|
283
|
-
|
|
587
|
+
.query-editor {
|
|
588
|
+
background: transparent;
|
|
589
|
+
border: none;
|
|
590
|
+
padding: 14px;
|
|
591
|
+
font-family: var(--font-mono);
|
|
592
|
+
font-size: 13px;
|
|
593
|
+
color: var(--text);
|
|
594
|
+
width: 100%;
|
|
595
|
+
min-height: 100px;
|
|
596
|
+
max-height: 200px;
|
|
597
|
+
resize: vertical;
|
|
598
|
+
outline: none;
|
|
599
|
+
line-height: 1.6;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.query-result-area {
|
|
603
|
+
flex: 1;
|
|
604
|
+
overflow: hidden;
|
|
284
605
|
}
|
|
285
606
|
|
|
286
607
|
.query-stats {
|
|
608
|
+
display: flex;
|
|
609
|
+
align-items: center;
|
|
610
|
+
gap: 12px;
|
|
611
|
+
padding: 8px 14px;
|
|
287
612
|
font-size: 12px;
|
|
288
|
-
color: var(--text-
|
|
289
|
-
|
|
613
|
+
color: var(--text-3);
|
|
614
|
+
border-top: 1px solid var(--border);
|
|
290
615
|
}
|
|
291
616
|
|
|
292
|
-
|
|
293
|
-
.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
height: calc(100vh - 120px);
|
|
617
|
+
.query-stats .stat { display: flex; align-items: center; gap: 4px; }
|
|
618
|
+
.query-stats .stat-ok { color: var(--green); }
|
|
619
|
+
.query-stats .stat-err { color: var(--red); }
|
|
620
|
+
|
|
621
|
+
.query-result-table {
|
|
298
622
|
overflow: auto;
|
|
299
|
-
|
|
623
|
+
flex: 1;
|
|
624
|
+
height: calc(100vh - 360px);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* ---- ER Diagram ---- */
|
|
628
|
+
.erd-canvas {
|
|
300
629
|
display: flex;
|
|
301
630
|
flex-wrap: wrap;
|
|
302
|
-
gap:
|
|
631
|
+
gap: 20px;
|
|
303
632
|
align-items: flex-start;
|
|
633
|
+
padding: 8px 0;
|
|
304
634
|
}
|
|
305
635
|
|
|
306
|
-
.erd-
|
|
307
|
-
background: var(--bg-
|
|
636
|
+
.erd-card {
|
|
637
|
+
background: var(--bg-2);
|
|
308
638
|
border: 1px solid var(--border);
|
|
309
639
|
border-radius: var(--radius);
|
|
310
|
-
min-width:
|
|
640
|
+
min-width: 220px;
|
|
641
|
+
overflow: hidden;
|
|
642
|
+
transition: border-color var(--transition);
|
|
311
643
|
}
|
|
312
644
|
|
|
313
|
-
.erd-
|
|
645
|
+
.erd-card:hover { border-color: var(--accent); }
|
|
646
|
+
|
|
647
|
+
.erd-card-header {
|
|
314
648
|
padding: 10px 14px;
|
|
315
|
-
font-weight:
|
|
316
|
-
font-size:
|
|
649
|
+
font-weight: 700;
|
|
650
|
+
font-size: 12px;
|
|
317
651
|
border-bottom: 1px solid var(--border);
|
|
318
|
-
color: var(--accent);
|
|
652
|
+
color: var(--accent-2);
|
|
653
|
+
font-family: var(--font-mono);
|
|
654
|
+
display: flex;
|
|
655
|
+
align-items: center;
|
|
656
|
+
gap: 6px;
|
|
319
657
|
}
|
|
320
658
|
|
|
321
|
-
.erd-
|
|
659
|
+
.erd-col {
|
|
322
660
|
padding: 6px 14px;
|
|
323
661
|
font-size: 12px;
|
|
324
|
-
font-family: var(--font);
|
|
662
|
+
font-family: var(--font-mono);
|
|
325
663
|
display: flex;
|
|
326
664
|
justify-content: space-between;
|
|
327
|
-
gap:
|
|
665
|
+
gap: 12px;
|
|
666
|
+
border-bottom: 1px solid var(--border);
|
|
667
|
+
transition: background var(--transition);
|
|
328
668
|
}
|
|
329
669
|
|
|
330
|
-
.erd-
|
|
670
|
+
.erd-col:last-child { border-bottom: none; }
|
|
671
|
+
.erd-col:hover { background: var(--bg-3); }
|
|
331
672
|
.erd-col-name { color: var(--text); }
|
|
332
|
-
.erd-col-type { color: var(--text-
|
|
333
|
-
.erd-col-pk { color: var(--
|
|
673
|
+
.erd-col-type { color: var(--text-3); font-size: 11px; }
|
|
674
|
+
.erd-col-pk { color: var(--yellow); }
|
|
334
675
|
|
|
335
|
-
/*
|
|
336
|
-
.
|
|
676
|
+
/* ---- Migrations ---- */
|
|
677
|
+
.migration-list {
|
|
337
678
|
display: flex;
|
|
338
679
|
flex-direction: column;
|
|
339
|
-
|
|
340
|
-
justify-content: center;
|
|
341
|
-
height: 300px;
|
|
342
|
-
color: var(--text-dim);
|
|
680
|
+
gap: 6px;
|
|
343
681
|
}
|
|
344
682
|
|
|
345
|
-
.empty-state-icon { font-size: 48px; margin-bottom: 12px; }
|
|
346
|
-
.empty-state-text { font-size: 14px; }
|
|
347
|
-
|
|
348
|
-
/* Migrations */
|
|
349
683
|
.migration-item {
|
|
350
684
|
display: flex;
|
|
351
685
|
align-items: center;
|
|
352
686
|
gap: 12px;
|
|
353
687
|
padding: 12px 16px;
|
|
354
|
-
background: var(--bg-
|
|
688
|
+
background: var(--bg-2);
|
|
355
689
|
border: 1px solid var(--border);
|
|
356
690
|
border-radius: var(--radius);
|
|
357
|
-
|
|
691
|
+
transition: border-color var(--transition);
|
|
358
692
|
}
|
|
359
693
|
|
|
360
|
-
.migration-
|
|
361
|
-
|
|
694
|
+
.migration-item:hover { border-color: var(--border-strong); }
|
|
695
|
+
|
|
696
|
+
.migration-icon { font-size: 16px; flex-shrink: 0; }
|
|
697
|
+
.migration-name { font-size: 13px; font-family: var(--font-mono); flex: 1; }
|
|
362
698
|
.migration-status {
|
|
363
|
-
font-size:
|
|
364
|
-
|
|
365
|
-
|
|
699
|
+
font-size: 10px;
|
|
700
|
+
font-weight: 600;
|
|
701
|
+
padding: 3px 8px;
|
|
702
|
+
border-radius: 10px;
|
|
703
|
+
text-transform: uppercase;
|
|
704
|
+
letter-spacing: 0.5px;
|
|
366
705
|
}
|
|
367
706
|
|
|
368
|
-
.migration-applied { color: var(--
|
|
369
|
-
.migration-pending { color: var(--
|
|
707
|
+
.migration-applied { color: var(--green); background: var(--green-bg); }
|
|
708
|
+
.migration-pending { color: var(--yellow); background: var(--yellow-bg); }
|
|
370
709
|
|
|
371
|
-
/* Settings */
|
|
372
|
-
.settings-
|
|
373
|
-
|
|
710
|
+
/* ---- Settings ---- */
|
|
711
|
+
.settings-grid {
|
|
712
|
+
display: grid;
|
|
713
|
+
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
714
|
+
gap: 16px;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.settings-card {
|
|
718
|
+
background: var(--bg-2);
|
|
374
719
|
border: 1px solid var(--border);
|
|
375
|
-
border-radius: var(--radius);
|
|
376
|
-
|
|
377
|
-
margin-bottom: 16px;
|
|
720
|
+
border-radius: var(--radius-lg);
|
|
721
|
+
overflow: hidden;
|
|
378
722
|
}
|
|
379
723
|
|
|
380
|
-
.settings-
|
|
724
|
+
.settings-card-header {
|
|
725
|
+
padding: 14px 18px;
|
|
726
|
+
border-bottom: 1px solid var(--border);
|
|
727
|
+
display: flex;
|
|
728
|
+
align-items: center;
|
|
729
|
+
gap: 10px;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.settings-card-icon {
|
|
733
|
+
width: 32px;
|
|
734
|
+
height: 32px;
|
|
735
|
+
border-radius: var(--radius-sm);
|
|
736
|
+
display: flex;
|
|
737
|
+
align-items: center;
|
|
738
|
+
justify-content: center;
|
|
739
|
+
font-size: 16px;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.settings-card-title {
|
|
381
743
|
font-size: 14px;
|
|
382
|
-
|
|
383
|
-
color: var(--text-dim);
|
|
744
|
+
font-weight: 600;
|
|
384
745
|
}
|
|
385
746
|
|
|
747
|
+
.settings-card-desc {
|
|
748
|
+
font-size: 11px;
|
|
749
|
+
color: var(--text-3);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.settings-rows { padding: 4px 0; }
|
|
753
|
+
|
|
386
754
|
.setting-row {
|
|
387
755
|
display: flex;
|
|
388
756
|
align-items: center;
|
|
389
|
-
|
|
757
|
+
padding: 10px 18px;
|
|
758
|
+
border-bottom: 1px solid var(--border);
|
|
759
|
+
transition: background var(--transition);
|
|
390
760
|
}
|
|
391
761
|
|
|
392
|
-
.setting-
|
|
762
|
+
.setting-row:last-child { border-bottom: none; }
|
|
763
|
+
.setting-row:hover { background: var(--bg-3); }
|
|
764
|
+
|
|
765
|
+
.setting-key {
|
|
393
766
|
width: 140px;
|
|
394
|
-
font-size:
|
|
395
|
-
color: var(--text-
|
|
767
|
+
font-size: 12px;
|
|
768
|
+
color: var(--text-3);
|
|
769
|
+
font-weight: 500;
|
|
770
|
+
flex-shrink: 0;
|
|
396
771
|
}
|
|
397
772
|
|
|
398
|
-
.setting-
|
|
399
|
-
font-size:
|
|
773
|
+
.setting-val {
|
|
774
|
+
font-size: 12px;
|
|
775
|
+
font-family: var(--font-mono);
|
|
776
|
+
color: var(--text);
|
|
777
|
+
word-break: break-all;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.setting-val.masked { color: var(--text-3); }
|
|
781
|
+
|
|
782
|
+
/* ---- Empty State ---- */
|
|
783
|
+
.empty-state {
|
|
784
|
+
display: flex;
|
|
785
|
+
flex-direction: column;
|
|
786
|
+
align-items: center;
|
|
787
|
+
justify-content: center;
|
|
788
|
+
padding: 48px 20px;
|
|
789
|
+
color: var(--text-3);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.empty-state-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
|
|
793
|
+
.empty-state-text { font-size: 13px; }
|
|
794
|
+
|
|
795
|
+
/* ---- Search Modal ---- */
|
|
796
|
+
.modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: flex-start; justify-content: center; padding-top: 15vh; }
|
|
797
|
+
.modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); }
|
|
798
|
+
.modal-content { position: relative; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-lg); width: 520px; max-height: 400px; overflow: hidden; box-shadow: var(--shadow-lg); }
|
|
799
|
+
|
|
800
|
+
.search-modal-input {
|
|
801
|
+
display: flex;
|
|
802
|
+
align-items: center;
|
|
803
|
+
gap: 10px;
|
|
804
|
+
padding: 14px 18px;
|
|
805
|
+
border-bottom: 1px solid var(--border);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.search-modal-input input {
|
|
809
|
+
background: none;
|
|
810
|
+
border: none;
|
|
811
|
+
outline: none;
|
|
812
|
+
color: var(--text);
|
|
813
|
+
font-size: 15px;
|
|
814
|
+
width: 100%;
|
|
400
815
|
font-family: var(--font);
|
|
401
816
|
}
|
|
817
|
+
|
|
818
|
+
.search-modal-input input::placeholder { color: var(--text-3); }
|
|
819
|
+
.search-modal-input kbd { font-size: 10px; padding: 2px 5px; border-radius: 4px; background: var(--bg-4); border: 1px solid var(--border); color: var(--text-3); font-family: var(--font); }
|
|
820
|
+
|
|
821
|
+
.search-results { max-height: 320px; overflow-y: auto; padding: 6px; }
|
|
822
|
+
|
|
823
|
+
.search-result-item {
|
|
824
|
+
display: flex;
|
|
825
|
+
align-items: center;
|
|
826
|
+
gap: 10px;
|
|
827
|
+
padding: 8px 12px;
|
|
828
|
+
border-radius: var(--radius-sm);
|
|
829
|
+
cursor: pointer;
|
|
830
|
+
transition: background var(--transition);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.search-result-item:hover { background: var(--bg-3); }
|
|
834
|
+
.search-result-item .sr-icon { font-size: 12px; color: var(--text-3); }
|
|
835
|
+
.search-result-item .sr-name { font-size: 13px; font-family: var(--font-mono); }
|
|
836
|
+
.search-result-item .sr-detail { font-size: 11px; color: var(--text-3); margin-left: auto; }
|
|
837
|
+
|
|
838
|
+
.search-empty {
|
|
839
|
+
padding: 24px;
|
|
840
|
+
text-align: center;
|
|
841
|
+
color: var(--text-3);
|
|
842
|
+
font-size: 13px;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* ---- Scrollbar ---- */
|
|
846
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
847
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
848
|
+
::-webkit-scrollbar-thumb { background: var(--bg-4); border-radius: 3px; }
|
|
849
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
|
|
402
850
|
`;
|
|
403
851
|
}
|
|
404
852
|
function getJS() {
|
|
405
853
|
return `
|
|
854
|
+
let allTables = [];
|
|
855
|
+
let currentTable = null;
|
|
856
|
+
|
|
406
857
|
// ---- Navigation ----
|
|
407
|
-
document.querySelectorAll('.nav-
|
|
858
|
+
document.querySelectorAll('.nav-link').forEach(link => {
|
|
408
859
|
link.addEventListener('click', (e) => {
|
|
409
860
|
e.preventDefault();
|
|
410
861
|
const page = link.dataset.page;
|
|
411
|
-
document.querySelectorAll('.nav-
|
|
862
|
+
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
|
412
863
|
link.classList.add('active');
|
|
413
864
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
414
865
|
document.getElementById('page-' + page)?.classList.add('active');
|
|
@@ -416,11 +867,7 @@ function getJS() {
|
|
|
416
867
|
});
|
|
417
868
|
});
|
|
418
869
|
|
|
419
|
-
// ---- Page loaders ----
|
|
420
|
-
const loaded = {};
|
|
421
870
|
function loadPage(page) {
|
|
422
|
-
if (loaded[page]) return;
|
|
423
|
-
loaded[page] = true;
|
|
424
871
|
switch (page) {
|
|
425
872
|
case 'tables': loadTables(); break;
|
|
426
873
|
case 'query': loadQuery(); break;
|
|
@@ -430,32 +877,117 @@ function getJS() {
|
|
|
430
877
|
}
|
|
431
878
|
}
|
|
432
879
|
|
|
880
|
+
// ---- Global Search (Ctrl+K) ----
|
|
881
|
+
const modal = document.getElementById('search-modal');
|
|
882
|
+
const modalInput = document.getElementById('modal-search-input');
|
|
883
|
+
const searchResults = document.getElementById('search-results');
|
|
884
|
+
|
|
885
|
+
document.getElementById('global-search').addEventListener('focus', openSearch);
|
|
886
|
+
document.addEventListener('keydown', (e) => {
|
|
887
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); }
|
|
888
|
+
if (e.key === 'Escape') closeSearch();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
modal.querySelector('.modal-backdrop').addEventListener('click', closeSearch);
|
|
892
|
+
|
|
893
|
+
function openSearch() {
|
|
894
|
+
modal.style.display = 'flex';
|
|
895
|
+
modalInput.value = '';
|
|
896
|
+
modalInput.focus();
|
|
897
|
+
renderSearchResults('');
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function closeSearch() {
|
|
901
|
+
modal.style.display = 'none';
|
|
902
|
+
document.getElementById('global-search').blur();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
modalInput.addEventListener('input', (e) => renderSearchResults(e.target.value));
|
|
906
|
+
|
|
907
|
+
function renderSearchResults(q) {
|
|
908
|
+
const query = q.toLowerCase().trim();
|
|
909
|
+
if (!query) {
|
|
910
|
+
searchResults.innerHTML = '<div class="search-empty">Type to search tables and columns...</div>';
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const matches = [];
|
|
915
|
+
for (const t of allTables) {
|
|
916
|
+
if (t.name.toLowerCase().includes(query)) {
|
|
917
|
+
matches.push({ type: 'table', name: t.name, detail: t.count + ' rows' });
|
|
918
|
+
}
|
|
919
|
+
if (t.columns) {
|
|
920
|
+
for (const c of t.columns) {
|
|
921
|
+
if (c.toLowerCase().includes(query)) {
|
|
922
|
+
matches.push({ type: 'column', name: c, detail: t.name });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (matches.length === 0) {
|
|
929
|
+
searchResults.innerHTML = '<div class="search-empty">No results found</div>';
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
searchResults.innerHTML = matches.slice(0, 20).map(m => \`
|
|
934
|
+
<div class="search-result-item" onclick="\${m.type === 'table' ? "closeSearch();selectTable('\${m.name}')" : "closeSearch();goToTable('\${m.detail}')"}">
|
|
935
|
+
<span class="sr-icon">\${m.type === 'table' ? '\u{1F4CA}' : '\u{1F4CB}'}</span>
|
|
936
|
+
<span class="sr-name">\${m.name}</span>
|
|
937
|
+
<span class="sr-detail">\${m.detail}</span>
|
|
938
|
+
</div>
|
|
939
|
+
\`).join('');
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function goToTable(tableName) {
|
|
943
|
+
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
|
944
|
+
document.querySelector('[data-page="tables"]').classList.add('active');
|
|
945
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
946
|
+
document.getElementById('page-tables').classList.add('active');
|
|
947
|
+
loadTables().then(() => selectTable(tableName));
|
|
948
|
+
}
|
|
949
|
+
|
|
433
950
|
// ---- Tables ----
|
|
434
951
|
async function loadTables() {
|
|
435
952
|
const el = document.getElementById('page-tables');
|
|
436
953
|
try {
|
|
437
954
|
const res = await fetch('/api/tables');
|
|
438
955
|
const data = await res.json();
|
|
439
|
-
|
|
956
|
+
allTables = (data.tables || []).map(name => ({ name, columns: [], count: 0 }));
|
|
440
957
|
|
|
441
958
|
el.innerHTML = \`
|
|
442
|
-
<
|
|
959
|
+
<div class="page-title">\u{1F4CA} Tables</div>
|
|
960
|
+
<div class="page-subtitle">Browse and inspect your database tables</div>
|
|
443
961
|
<div class="table-grid">
|
|
444
|
-
<div class="table-
|
|
445
|
-
<div class="table-
|
|
446
|
-
|
|
447
|
-
<
|
|
448
|
-
|
|
962
|
+
<div class="table-sidebar">
|
|
963
|
+
<div class="table-search">
|
|
964
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
965
|
+
<input type="text" id="table-search-input" placeholder="Filter tables..." oninput="filterTables(this.value)">
|
|
966
|
+
</div>
|
|
967
|
+
<div class="table-count-wrap">
|
|
968
|
+
<span>\${allTables.length} tables</span>
|
|
969
|
+
</div>
|
|
970
|
+
<div class="table-list" id="table-list">
|
|
971
|
+
\${allTables.map(t => \`
|
|
972
|
+
<div class="table-item" data-table="\${t.name}" onclick="selectTable('\${t.name}')">
|
|
973
|
+
<span class="table-icon">\u25FB</span>
|
|
974
|
+
<span>\${t.name}</span>
|
|
975
|
+
</div>
|
|
976
|
+
\`).join('')}
|
|
977
|
+
</div>
|
|
449
978
|
</div>
|
|
450
979
|
<div class="data-panel">
|
|
451
|
-
<div class="data-
|
|
452
|
-
<
|
|
453
|
-
|
|
980
|
+
<div class="data-toolbar" id="data-toolbar">
|
|
981
|
+
<div class="data-toolbar-title" id="table-title">
|
|
982
|
+
<span>Select a table</span>
|
|
983
|
+
</div>
|
|
454
984
|
</div>
|
|
455
|
-
<div class="data-table-
|
|
456
|
-
<div class="
|
|
457
|
-
<div class="empty-state
|
|
458
|
-
|
|
985
|
+
<div class="data-table-card card">
|
|
986
|
+
<div class="data-table-scroll" id="table-data">
|
|
987
|
+
<div class="empty-state">
|
|
988
|
+
<div class="empty-state-icon">\u{1F4CB}</div>
|
|
989
|
+
<div class="empty-state-text">Select a table to view its data</div>
|
|
990
|
+
</div>
|
|
459
991
|
</div>
|
|
460
992
|
</div>
|
|
461
993
|
</div>
|
|
@@ -466,46 +998,104 @@ function getJS() {
|
|
|
466
998
|
}
|
|
467
999
|
}
|
|
468
1000
|
|
|
469
|
-
|
|
1001
|
+
function filterTables(q) {
|
|
1002
|
+
const query = q.toLowerCase();
|
|
1003
|
+
document.querySelectorAll('#table-list .table-item').forEach(item => {
|
|
1004
|
+
const name = item.dataset.table;
|
|
1005
|
+
item.style.display = name.toLowerCase().includes(query) ? '' : 'none';
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
window.selectTable = async function(name) {
|
|
1010
|
+
currentTable = name;
|
|
470
1011
|
document.querySelectorAll('.table-item').forEach(i => i.classList.remove('active'));
|
|
471
|
-
document.querySelector(\`.table-item[data-table="\${
|
|
472
|
-
document.getElementById('table-title').
|
|
1012
|
+
document.querySelector(\`.table-item[data-table="\${name}"]\`)?.classList.add('active');
|
|
1013
|
+
document.getElementById('table-title').innerHTML = \`\${name}<span class="badge" id="table-count-badge">...</span>\`;
|
|
473
1014
|
|
|
474
1015
|
const el = document.getElementById('table-data');
|
|
475
1016
|
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">Loading...</div></div>';
|
|
476
1017
|
|
|
1018
|
+
// Add filter toolbar
|
|
1019
|
+
document.getElementById('data-toolbar').innerHTML = \`
|
|
1020
|
+
<div class="data-toolbar-title">\${name}<span class="badge" id="table-count-badge">...</span></div>
|
|
1021
|
+
<div class="data-filter">
|
|
1022
|
+
<input type="text" id="row-filter" placeholder="Filter rows..." oninput="filterRows(this.value)">
|
|
1023
|
+
</div>
|
|
1024
|
+
\`;
|
|
1025
|
+
|
|
477
1026
|
try {
|
|
478
|
-
const res = await fetch(\`/api/tables/\${
|
|
1027
|
+
const res = await fetch(\`/api/tables/\${name}\`);
|
|
479
1028
|
const data = await res.json();
|
|
480
|
-
document.getElementById('table-
|
|
1029
|
+
document.getElementById('table-count-badge').textContent = \`\${data.count} rows\`;
|
|
481
1030
|
|
|
482
1031
|
if (data.rows.length === 0) {
|
|
483
|
-
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F4ED}</div><div class="empty-state-text">No rows</div></div>';
|
|
1032
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F4ED}</div><div class="empty-state-text">No rows in this table</div></div>';
|
|
484
1033
|
return;
|
|
485
1034
|
}
|
|
486
1035
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
<tbody>\${data.rows.map(row => \`<tr>\${data.columns.map(c => \`<td title="\${String(row[c] ?? '')}">\${String(row[c] ?? 'NULL')}</td>\`).join('')}</tr>\`).join('')}</tbody>
|
|
491
|
-
</table>
|
|
492
|
-
\`;
|
|
1036
|
+
// Store for filtering
|
|
1037
|
+
window._tableData = data;
|
|
1038
|
+
renderTableData(data);
|
|
493
1039
|
} catch (err) {
|
|
494
1040
|
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u274C</div><div class="empty-state-text">Failed to load data</div></div>';
|
|
495
1041
|
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
function renderTableData(data, filter) {
|
|
1045
|
+
const el = document.getElementById('table-data');
|
|
1046
|
+
let rows = data.rows;
|
|
1047
|
+
if (filter) {
|
|
1048
|
+
const q = filter.toLowerCase();
|
|
1049
|
+
rows = rows.filter(row => Object.values(row).some(v => String(v ?? '').toLowerCase().includes(q)));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
el.innerHTML = \`
|
|
1053
|
+
<table>
|
|
1054
|
+
<thead><tr>\${data.columns.map(c => \`<th>\${c}</th>\`).join('')}</tr></thead>
|
|
1055
|
+
<tbody>\${rows.map(row => \`<tr>\${data.columns.map(c => {
|
|
1056
|
+
const v = row[c];
|
|
1057
|
+
const isNull = v === null || v === undefined;
|
|
1058
|
+
const display = isNull ? 'NULL' : String(v);
|
|
1059
|
+
return \`<td class="\${isNull ? 'cell-null' : ''}" title="\${display}">\${display}</td>\`;
|
|
1060
|
+
}).join('')}</tr>\`).join('')}</tbody>
|
|
1061
|
+
</table>
|
|
1062
|
+
\`;
|
|
496
1063
|
}
|
|
497
1064
|
|
|
498
|
-
|
|
1065
|
+
window.filterRows = function(q) {
|
|
1066
|
+
if (window._tableData) renderTableData(window._tableData, q);
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
// ---- Query Console ----
|
|
499
1070
|
function loadQuery() {
|
|
500
1071
|
document.getElementById('page-query').innerHTML = \`
|
|
501
|
-
<
|
|
502
|
-
<
|
|
503
|
-
<div class="query-
|
|
504
|
-
<
|
|
505
|
-
|
|
1072
|
+
<div class="page-title">\u{1F4BB} SQL Console</div>
|
|
1073
|
+
<div class="page-subtitle">Execute raw SQL queries against your database</div>
|
|
1074
|
+
<div class="query-layout">
|
|
1075
|
+
<div class="query-editor-wrap">
|
|
1076
|
+
<div class="query-editor-header">
|
|
1077
|
+
<span>Editor</span>
|
|
1078
|
+
<div style="display:flex;gap:6px">
|
|
1079
|
+
<button class="btn btn-primary btn-sm" onclick="runQuery()">\u25B6 Run</button>
|
|
1080
|
+
<button class="btn btn-sm" onclick="clearQuery()">Clear</button>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
<textarea class="query-editor" id="sql-input" placeholder="SELECT * FROM users LIMIT 100" spellcheck="false">SELECT * FROM users LIMIT 100</textarea>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div class="query-result-area card">
|
|
1086
|
+
<div class="query-result-table" id="query-result">
|
|
1087
|
+
<div class="empty-state">
|
|
1088
|
+
<div class="empty-state-text">Results will appear here</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
506
1092
|
</div>
|
|
507
|
-
<div id="query-result" class="query-result"></div>
|
|
508
1093
|
\`;
|
|
1094
|
+
|
|
1095
|
+
// Ctrl+Enter to run
|
|
1096
|
+
document.getElementById('sql-input').addEventListener('keydown', (e) => {
|
|
1097
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); runQuery(); }
|
|
1098
|
+
});
|
|
509
1099
|
}
|
|
510
1100
|
|
|
511
1101
|
async function runQuery() {
|
|
@@ -523,42 +1113,68 @@ function getJS() {
|
|
|
523
1113
|
const data = await res.json();
|
|
524
1114
|
|
|
525
1115
|
if (data.error) {
|
|
526
|
-
el.innerHTML =
|
|
1116
|
+
el.innerHTML = \`
|
|
1117
|
+
<div class="query-stats">
|
|
1118
|
+
<span class="stat stat-err">\u2717 Error</span>
|
|
1119
|
+
</div>
|
|
1120
|
+
<div style="padding:16px;color:var(--red);font-family:var(--font-mono);font-size:12px">\${data.error}</div>
|
|
1121
|
+
\`;
|
|
527
1122
|
return;
|
|
528
1123
|
}
|
|
529
1124
|
|
|
530
|
-
let html = \`<div class="query-stats"
|
|
1125
|
+
let html = \`<div class="query-stats">
|
|
1126
|
+
<span class="stat stat-ok">\u2713 \${data.rowCount} row(s)</span>
|
|
1127
|
+
<span class="stat">\${data.duration}ms</span>
|
|
1128
|
+
</div>\`;
|
|
1129
|
+
|
|
531
1130
|
if (data.rows.length > 0) {
|
|
532
|
-
html += \`<table><
|
|
1131
|
+
html += \`<div class="query-result-table"><table>
|
|
1132
|
+
<thead><tr>\${data.columns.map(c => \`<th>\${c}</th>\`).join('')}</tr></thead>
|
|
1133
|
+
<tbody>\${data.rows.map(row => \`<tr>\${data.columns.map(c => {
|
|
1134
|
+
const v = row[c];
|
|
1135
|
+
const isNull = v === null || v === undefined;
|
|
1136
|
+
return \`<td class="\${isNull ? 'cell-null' : ''}">\${isNull ? 'NULL' : String(v)}</td>\`;
|
|
1137
|
+
}).join('')}</tr>\`).join('')}</tbody>
|
|
1138
|
+
</table></div>\`;
|
|
533
1139
|
}
|
|
534
1140
|
el.innerHTML = html;
|
|
535
1141
|
} catch (err) {
|
|
536
|
-
el.innerHTML = \`<div class="query-stats" style="color:var(--
|
|
1142
|
+
el.innerHTML = \`<div class="query-stats"><span class="stat stat-err">\u2717 Error</span></div><div style="padding:16px;color:var(--red);font-size:12px">\${err.message}</div>\`;
|
|
537
1143
|
}
|
|
538
1144
|
}
|
|
539
1145
|
|
|
1146
|
+
window.clearQuery = function() {
|
|
1147
|
+
document.getElementById('sql-input').value = '';
|
|
1148
|
+
document.getElementById('query-result').innerHTML = '<div class="empty-state"><div class="empty-state-text">Results will appear here</div></div>';
|
|
1149
|
+
};
|
|
1150
|
+
|
|
540
1151
|
// ---- ER Diagram ----
|
|
541
1152
|
async function loadERD() {
|
|
542
1153
|
const el = document.getElementById('page-erd');
|
|
1154
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">Loading ER diagram...</div></div>';
|
|
1155
|
+
|
|
543
1156
|
try {
|
|
544
1157
|
const res = await fetch('/api/erd');
|
|
545
1158
|
const data = await res.json();
|
|
546
1159
|
|
|
547
1160
|
if (data.tables.length === 0) {
|
|
548
|
-
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F517}</div><div class="empty-state-text">No tables found</div></div>';
|
|
1161
|
+
el.innerHTML = '<div class="page-title">\u{1F517} ER Diagram</div><div class="page-subtitle">Entity-relationship diagram of your database</div><div class="empty-state"><div class="empty-state-icon">\u{1F517}</div><div class="empty-state-text">No tables found</div></div>';
|
|
549
1162
|
return;
|
|
550
1163
|
}
|
|
551
1164
|
|
|
552
1165
|
el.innerHTML = \`
|
|
553
|
-
<
|
|
554
|
-
<div class="
|
|
1166
|
+
<div class="page-title">\u{1F517} ER Diagram</div>
|
|
1167
|
+
<div class="page-subtitle">\${data.tables.length} tables \xB7 \${data.relations.length} relations</div>
|
|
1168
|
+
<div class="erd-canvas">
|
|
555
1169
|
\${data.tables.map(t => \`
|
|
556
|
-
<div class="erd-
|
|
557
|
-
<div class="erd-
|
|
1170
|
+
<div class="erd-card">
|
|
1171
|
+
<div class="erd-card-header">
|
|
1172
|
+
<span>\u25FB</span> \${t.name}
|
|
1173
|
+
</div>
|
|
558
1174
|
\${t.columns.map(c => \`
|
|
559
|
-
<div class="erd-
|
|
1175
|
+
<div class="erd-col">
|
|
560
1176
|
<span class="erd-col-name \${c.isPrimaryKey ? 'erd-col-pk' : ''}">\${c.isPrimaryKey ? '\u{1F511} ' : ''}\${c.name}</span>
|
|
561
|
-
<span class="erd-col-type">\${c.type}\${c.nullable ? '' : '
|
|
1177
|
+
<span class="erd-col-type">\${c.type}\${c.nullable ? '?' : ''}</span>
|
|
562
1178
|
</div>
|
|
563
1179
|
\`).join('')}
|
|
564
1180
|
</div>
|
|
@@ -581,31 +1197,35 @@ function getJS() {
|
|
|
581
1197
|
|
|
582
1198
|
if (data.applied.length === 0 && data.pending.length === 0) {
|
|
583
1199
|
el.innerHTML = \`
|
|
584
|
-
<
|
|
1200
|
+
<div class="page-title">\u{1F4E6} Migrations</div>
|
|
1201
|
+
<div class="page-subtitle">Database schema version control</div>
|
|
585
1202
|
<div class="empty-state">
|
|
586
1203
|
<div class="empty-state-icon">\u{1F4E6}</div>
|
|
587
|
-
<div class="empty-state-text">No migrations found. Run "mountsqli generate" to create one.</div>
|
|
1204
|
+
<div class="empty-state-text">No migrations found. Run "npx @mountsqli/cli generate" to create one.</div>
|
|
588
1205
|
</div>
|
|
589
1206
|
\`;
|
|
590
1207
|
return;
|
|
591
1208
|
}
|
|
592
1209
|
|
|
593
1210
|
el.innerHTML = \`
|
|
594
|
-
<
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
<
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
<
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1211
|
+
<div class="page-title">\u{1F4E6} Migrations</div>
|
|
1212
|
+
<div class="page-subtitle">\${data.applied.length} applied \xB7 \${data.pending.length} pending</div>
|
|
1213
|
+
<div class="migration-list">
|
|
1214
|
+
\${data.applied.map(m => \`
|
|
1215
|
+
<div class="migration-item">
|
|
1216
|
+
<span class="migration-icon">\u2705</span>
|
|
1217
|
+
<span class="migration-name">\${m.name}</span>
|
|
1218
|
+
<span class="migration-status migration-applied">applied</span>
|
|
1219
|
+
</div>
|
|
1220
|
+
\`).join('')}
|
|
1221
|
+
\${data.pending.map(m => \`
|
|
1222
|
+
<div class="migration-item">
|
|
1223
|
+
<span class="migration-icon">\u23F3</span>
|
|
1224
|
+
<span class="migration-name">\${m}</span>
|
|
1225
|
+
<span class="migration-status migration-pending">pending</span>
|
|
1226
|
+
</div>
|
|
1227
|
+
\`).join('')}
|
|
1228
|
+
</div>
|
|
609
1229
|
\`;
|
|
610
1230
|
} catch (err) {
|
|
611
1231
|
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u274C</div><div class="empty-state-text">Failed to load migrations</div></div>';
|
|
@@ -613,36 +1233,108 @@ function getJS() {
|
|
|
613
1233
|
}
|
|
614
1234
|
|
|
615
1235
|
// ---- Settings ----
|
|
616
|
-
function loadSettings() {
|
|
617
|
-
document.getElementById('page-settings')
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
<div class="
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1236
|
+
async function loadSettings() {
|
|
1237
|
+
const el = document.getElementById('page-settings');
|
|
1238
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">Loading settings...</div></div>';
|
|
1239
|
+
|
|
1240
|
+
try {
|
|
1241
|
+
const res = await fetch('/api/settings');
|
|
1242
|
+
const s = await res.json();
|
|
1243
|
+
|
|
1244
|
+
el.innerHTML = \`
|
|
1245
|
+
<div class="page-title">\u2699\uFE0F Settings</div>
|
|
1246
|
+
<div class="page-subtitle">Configuration and connection details</div>
|
|
1247
|
+
<div class="settings-grid">
|
|
1248
|
+
<!-- Connection -->
|
|
1249
|
+
<div class="settings-card">
|
|
1250
|
+
<div class="settings-card-header">
|
|
1251
|
+
<div class="settings-card-icon" style="background:var(--green-bg);color:var(--green)">\u{1F50C}</div>
|
|
1252
|
+
<div>
|
|
1253
|
+
<div class="settings-card-title">Connection</div>
|
|
1254
|
+
<div class="settings-card-desc">Database connection details</div>
|
|
1255
|
+
</div>
|
|
1256
|
+
</div>
|
|
1257
|
+
<div class="settings-rows">
|
|
1258
|
+
<div class="setting-row"><span class="setting-key">Dialect</span><span class="setting-val">\${s.connection.dialect}</span></div>
|
|
1259
|
+
<div class="setting-row"><span class="setting-key">Host</span><span class="setting-val">\${s.connection.host}</span></div>
|
|
1260
|
+
<div class="setting-row"><span class="setting-key">Port</span><span class="setting-val">\${s.connection.port}</span></div>
|
|
1261
|
+
<div class="setting-row"><span class="setting-key">Database</span><span class="setting-val">\${s.connection.database}</span></div>
|
|
1262
|
+
<div class="setting-row"><span class="setting-key">Username</span><span class="setting-val">\${s.connection.username || '\u2014'}</span></div>
|
|
1263
|
+
<div class="setting-row"><span class="setting-key">Password</span><span class="setting-val masked">\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022</span></div>
|
|
1264
|
+
<div class="setting-row"><span class="setting-key">SSL</span><span class="setting-val">\${s.connection.ssl ? '\u2713 Enabled' : '\u2717 Disabled'}</span></div>
|
|
1265
|
+
<div class="setting-row"><span class="setting-key">Connection URL</span><span class="setting-val masked" style="font-size:11px">\${s.connection.connectionString}</span></div>
|
|
1266
|
+
</div>
|
|
1267
|
+
</div>
|
|
1268
|
+
|
|
1269
|
+
<!-- Schema -->
|
|
1270
|
+
<div class="settings-card">
|
|
1271
|
+
<div class="settings-card-header">
|
|
1272
|
+
<div class="settings-card-icon" style="background:var(--blue-bg);color:var(--blue)">\u{1F4C1}</div>
|
|
1273
|
+
<div>
|
|
1274
|
+
<div class="settings-card-title">Schema</div>
|
|
1275
|
+
<div class="settings-card-desc">Schema definition settings</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
<div class="settings-rows">
|
|
1279
|
+
<div class="setting-row"><span class="setting-key">Directory</span><span class="setting-val">\${s.schema.directory}</span></div>
|
|
1280
|
+
<div class="setting-row"><span class="setting-key">Casing</span><span class="setting-val">\${s.schema.casing}</span></div>
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1283
|
+
|
|
1284
|
+
<!-- Migrations -->
|
|
1285
|
+
<div class="settings-card">
|
|
1286
|
+
<div class="settings-card-header">
|
|
1287
|
+
<div class="settings-card-icon" style="background:var(--yellow-bg);color:var(--yellow)">\u{1F4E6}</div>
|
|
1288
|
+
<div>
|
|
1289
|
+
<div class="settings-card-title">Migrations</div>
|
|
1290
|
+
<div class="settings-card-desc">Migration configuration</div>
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
<div class="settings-rows">
|
|
1294
|
+
<div class="setting-row"><span class="setting-key">Directory</span><span class="setting-val">\${s.migrations.directory}</span></div>
|
|
1295
|
+
<div class="setting-row"><span class="setting-key">History Table</span><span class="setting-val">\${s.migrations.table}</span></div>
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
|
|
1299
|
+
<!-- Studio -->
|
|
1300
|
+
<div class="settings-card">
|
|
1301
|
+
<div class="settings-card-header">
|
|
1302
|
+
<div class="settings-card-icon" style="background:var(--accent-bg);color:var(--accent-2)">\u{1F3A8}</div>
|
|
1303
|
+
<div>
|
|
1304
|
+
<div class="settings-card-title">Studio</div>
|
|
1305
|
+
<div class="settings-card-desc">Dashboard configuration</div>
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
<div class="settings-rows">
|
|
1309
|
+
<div class="setting-row"><span class="setting-key">Version</span><span class="setting-val">\${s.studio.version}</span></div>
|
|
1310
|
+
<div class="setting-row"><span class="setting-key">Host</span><span class="setting-val">\${s.studio.host}</span></div>
|
|
1311
|
+
<div class="setting-row"><span class="setting-key">Port</span><span class="setting-val">\${s.studio.port}</span></div>
|
|
1312
|
+
<div class="setting-row"><span class="setting-key">URL</span><span class="setting-val">\${s.studio.url}</span></div>
|
|
1313
|
+
</div>
|
|
1314
|
+
</div>
|
|
1315
|
+
|
|
1316
|
+
<!-- Logger -->
|
|
1317
|
+
<div class="settings-card">
|
|
1318
|
+
<div class="settings-card-header">
|
|
1319
|
+
<div class="settings-card-icon" style="background:var(--red-bg);color:var(--red)">\u{1F4DD}</div>
|
|
1320
|
+
<div>
|
|
1321
|
+
<div class="settings-card-title">Logging</div>
|
|
1322
|
+
<div class="settings-card-desc">Query logging configuration</div>
|
|
1323
|
+
</div>
|
|
1324
|
+
</div>
|
|
1325
|
+
<div class="settings-rows">
|
|
1326
|
+
<div class="setting-row"><span class="setting-key">Query Logger</span><span class="setting-val">\${s.logger ? '\u2713 Enabled' : '\u2717 Disabled'}</span></div>
|
|
1327
|
+
</div>
|
|
1328
|
+
</div>
|
|
639
1329
|
</div>
|
|
640
|
-
|
|
641
|
-
|
|
1330
|
+
\`;
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u274C</div><div class="empty-state-text">Failed to load settings</div></div>';
|
|
1333
|
+
}
|
|
642
1334
|
}
|
|
643
1335
|
|
|
644
1336
|
// ---- Init ----
|
|
645
|
-
|
|
1337
|
+
loadTables();
|
|
646
1338
|
`;
|
|
647
1339
|
}
|
|
648
1340
|
|
|
@@ -703,6 +1395,9 @@ async function handleRequest(req, res, options) {
|
|
|
703
1395
|
if (path === "/api/health" && req.method === "GET") {
|
|
704
1396
|
return jsonResponse(res, { status: "ok", dialect: options.config.dialect });
|
|
705
1397
|
}
|
|
1398
|
+
if (path === "/api/settings" && req.method === "GET") {
|
|
1399
|
+
return jsonResponse(res, await getSettings(options));
|
|
1400
|
+
}
|
|
706
1401
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
707
1402
|
res.end(getDashboardHTML());
|
|
708
1403
|
}
|
|
@@ -906,6 +1601,69 @@ async function getERD(options) {
|
|
|
906
1601
|
}
|
|
907
1602
|
return { tables: [], relations: [] };
|
|
908
1603
|
}
|
|
1604
|
+
async function getSettings(options) {
|
|
1605
|
+
const config = options.config;
|
|
1606
|
+
const connStr = typeof config.connection === "string" ? config.connection : "";
|
|
1607
|
+
let maskedConn = connStr;
|
|
1608
|
+
try {
|
|
1609
|
+
const url = new URL(connStr);
|
|
1610
|
+
if (url.password) {
|
|
1611
|
+
url.password = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
1612
|
+
maskedConn = url.toString();
|
|
1613
|
+
}
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
return {
|
|
1617
|
+
connection: {
|
|
1618
|
+
dialect: config.dialect,
|
|
1619
|
+
connectionString: maskedConn,
|
|
1620
|
+
host: (() => {
|
|
1621
|
+
try {
|
|
1622
|
+
return new URL(connStr).hostname;
|
|
1623
|
+
} catch {
|
|
1624
|
+
return "\u2014";
|
|
1625
|
+
}
|
|
1626
|
+
})(),
|
|
1627
|
+
port: (() => {
|
|
1628
|
+
try {
|
|
1629
|
+
return new URL(connStr).port || "5432";
|
|
1630
|
+
} catch {
|
|
1631
|
+
return "\u2014";
|
|
1632
|
+
}
|
|
1633
|
+
})(),
|
|
1634
|
+
database: (() => {
|
|
1635
|
+
try {
|
|
1636
|
+
return new URL(connStr).pathname.slice(1);
|
|
1637
|
+
} catch {
|
|
1638
|
+
return "\u2014";
|
|
1639
|
+
}
|
|
1640
|
+
})(),
|
|
1641
|
+
username: (() => {
|
|
1642
|
+
try {
|
|
1643
|
+
return new URL(connStr).username;
|
|
1644
|
+
} catch {
|
|
1645
|
+
return "\u2014";
|
|
1646
|
+
}
|
|
1647
|
+
})(),
|
|
1648
|
+
ssl: connStr.includes("sslmode=")
|
|
1649
|
+
},
|
|
1650
|
+
schema: {
|
|
1651
|
+
directory: config.schema,
|
|
1652
|
+
casing: config.casing ?? "camelCase"
|
|
1653
|
+
},
|
|
1654
|
+
migrations: {
|
|
1655
|
+
directory: config.out,
|
|
1656
|
+
table: config.migrationsTable ?? "_mountsqli_migrations"
|
|
1657
|
+
},
|
|
1658
|
+
studio: {
|
|
1659
|
+
port: options.port,
|
|
1660
|
+
host: options.host,
|
|
1661
|
+
version: "0.1.0",
|
|
1662
|
+
url: `http://${options.host}:${options.port}`
|
|
1663
|
+
},
|
|
1664
|
+
logger: config.logger ?? false
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
909
1667
|
function jsonResponse(res, data) {
|
|
910
1668
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
911
1669
|
res.end(JSON.stringify(data));
|
|
@@ -944,12 +1702,11 @@ async function startStudio(options) {
|
|
|
944
1702
|
\u{1F3A8} MountSQLi Studio running at http://${host}:${port}
|
|
945
1703
|
`);
|
|
946
1704
|
console.log(" Pages:");
|
|
947
|
-
console.log(" / \u2014 Dashboard
|
|
948
|
-
console.log(" /tables
|
|
949
|
-
console.log(" /query
|
|
950
|
-
console.log(" /
|
|
951
|
-
console.log(" /
|
|
952
|
-
console.log(" /settings \u2014 Connection settings");
|
|
1705
|
+
console.log(" / \u2014 Dashboard (SPA)");
|
|
1706
|
+
console.log(" /api/tables \u2014 Table data API");
|
|
1707
|
+
console.log(" /api/query \u2014 SQL query API");
|
|
1708
|
+
console.log(" /api/erd \u2014 ER diagram API");
|
|
1709
|
+
console.log(" /api/settings \u2014 Settings API");
|
|
953
1710
|
console.log("\n Press Ctrl+C to stop\n");
|
|
954
1711
|
});
|
|
955
1712
|
}
|