Skip to content

Commit f2f1dc5

Browse files
style: simplify database settings UI and refine rbac success field layout
1 parent a74adaa commit f2f1dc5

6 files changed

Lines changed: 38 additions & 60 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "api-documenter",
3-
"version": "1.0.10",
3+
"version": "1.0.12",
44
"description": "Self-hosted Postman alternative with folder-level RBAC and offline-first documentation",
55
"main": "./out/main/index.js",
66
"author": "Praneeth Kulukuri",

src/main/index.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ ipcMain.handle('send-http-request', async (_event, opts: {
137137
ipcMain.handle('test-db-connection', async (_event, url: string) => {
138138
if (url.startsWith('mysql://')) {
139139
try {
140-
const conn = await mysql.createConnection(url)
140+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
141141
await conn.ping()
142142
await conn.end()
143143
return { success: true }
@@ -146,7 +146,7 @@ ipcMain.handle('test-db-connection', async (_event, url: string) => {
146146
}
147147
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
148148
try {
149-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
149+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
150150
await client.connect()
151151
await client.end()
152152
return { success: true }
@@ -173,7 +173,7 @@ ipcMain.handle('create-remote-tables', async (_event, url: string) => {
173173

174174
if (url.startsWith('mysql://')) {
175175
try {
176-
const conn = await mysql.createConnection({ uri: url, multipleStatements: true })
176+
const conn = await mysql.createConnection({ uri: url, multipleStatements: true, connectTimeout: 10000 })
177177
for (const s of statements) await conn.execute(s)
178178
// Run migrations (ignoring errors if columns exist but ADD COLUMN IF NOT EXISTS isn't supported)
179179
for (const m of migrations) {
@@ -188,7 +188,7 @@ ipcMain.handle('create-remote-tables', async (_event, url: string) => {
188188
}
189189
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
190190
try {
191-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
191+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
192192
await client.connect()
193193
for (const s of statements) await client.query(s)
194194
// Run migrations
@@ -209,7 +209,7 @@ ipcMain.handle('create-remote-tables', async (_event, url: string) => {
209209
ipcMain.handle('create-rbac-user', async (_event, url: string, user: { id: string, email: string, token: string, allowedFolders: string[], projectId: string, role: string }) => {
210210
if (url.startsWith('mysql://')) {
211211
try {
212-
const conn = await mysql.createConnection(url)
212+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
213213
await conn.execute(
214214
'INSERT INTO rbac_users (id, email, token, allowed_folders, project_id, role) VALUES (?, ?, ?, ?, ?, ?)',
215215
[user.id, user.email, user.token, JSON.stringify(user.allowedFolders), user.projectId, user.role]
@@ -221,7 +221,7 @@ ipcMain.handle('create-rbac-user', async (_event, url: string, user: { id: strin
221221
}
222222
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
223223
try {
224-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
224+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
225225
await client.connect()
226226
await client.query(
227227
'INSERT INTO rbac_users (id, email, token, allowed_folders, project_id, role) VALUES ($1, $2, $3, $4, $5, $6)',
@@ -239,7 +239,7 @@ ipcMain.handle('create-rbac-user', async (_event, url: string, user: { id: strin
239239
ipcMain.handle('get-rbac-users', async (_event, url: string, projectId: string) => {
240240
if (url.startsWith('mysql://')) {
241241
try {
242-
const conn = await mysql.createConnection(url)
242+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
243243
const [rows]: any = await conn.execute('SELECT * FROM rbac_users WHERE project_id = ?', [projectId])
244244
await conn.end()
245245
return { success: true, users: rows }
@@ -248,7 +248,7 @@ ipcMain.handle('get-rbac-users', async (_event, url: string, projectId: string)
248248
}
249249
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
250250
try {
251-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
251+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
252252
await client.connect()
253253
const res = await client.query('SELECT * FROM rbac_users WHERE project_id = $1', [projectId])
254254
await client.end()
@@ -263,7 +263,7 @@ ipcMain.handle('get-rbac-users', async (_event, url: string, projectId: string)
263263
ipcMain.handle('update-rbac-user', async (_event, url: string, user: { id: string, email: string, allowedFolders: any, role: string }) => {
264264
if (url.startsWith('mysql://')) {
265265
try {
266-
const conn = await mysql.createConnection(url)
266+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
267267
await conn.execute(
268268
'UPDATE rbac_users SET email = ?, allowed_folders = ?, role = ? WHERE id = ?',
269269
[user.email, JSON.stringify(user.allowedFolders), user.role, user.id]
@@ -275,7 +275,7 @@ ipcMain.handle('update-rbac-user', async (_event, url: string, user: { id: strin
275275
}
276276
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
277277
try {
278-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
278+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
279279
await client.connect()
280280
await client.query(
281281
'UPDATE rbac_users SET email = $1, allowed_folders = $2, role = $3 WHERE id = $4',
@@ -293,7 +293,7 @@ ipcMain.handle('update-rbac-user', async (_event, url: string, user: { id: strin
293293
ipcMain.handle('delete-rbac-user', async (_event, url: string, userId: string) => {
294294
if (url.startsWith('mysql://')) {
295295
try {
296-
const conn = await mysql.createConnection(url)
296+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
297297
await conn.execute('DELETE FROM rbac_users WHERE id = ?', [userId])
298298
await conn.end()
299299
return { success: true }
@@ -302,7 +302,7 @@ ipcMain.handle('delete-rbac-user', async (_event, url: string, userId: string) =
302302
}
303303
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
304304
try {
305-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
305+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
306306
await client.connect()
307307
await client.query('DELETE FROM rbac_users WHERE id = $1', [userId])
308308
await client.end()
@@ -319,7 +319,7 @@ ipcMain.handle('sync-direct', async (_event, url: string, entries: any[]) => {
319319

320320
if (url.startsWith('mysql://')) {
321321
try {
322-
const conn = await mysql.createConnection({ uri: url, multipleStatements: true })
322+
const conn = await mysql.createConnection({ uri: url, multipleStatements: true, connectTimeout: 10000 })
323323
for (const entry of entries) {
324324
const { tableName, operation, data } = entry
325325
const payload = typeof data === 'string' ? JSON.parse(data) : data
@@ -381,7 +381,7 @@ ipcMain.handle('sync-direct', async (_event, url: string, entries: any[]) => {
381381
}
382382
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
383383
try {
384-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
384+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
385385
await client.connect()
386386
for (const entry of entries) {
387387
const { tableName, operation, data } = entry
@@ -441,7 +441,7 @@ ipcMain.handle('sync-direct', async (_event, url: string, entries: any[]) => {
441441
ipcMain.handle('fetch-remote-data', async (_event, url: string, projectId: string) => {
442442
if (url.startsWith('mysql://')) {
443443
try {
444-
const conn = await mysql.createConnection(url)
444+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
445445
const [folders]: any = await conn.execute('SELECT * FROM folders WHERE project_id = ?', [projectId])
446446
const [apis]: any = await conn.execute('SELECT * FROM api_collections WHERE project_id = ?', [projectId])
447447
await conn.end()
@@ -451,7 +451,7 @@ ipcMain.handle('fetch-remote-data', async (_event, url: string, projectId: strin
451451
}
452452
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
453453
try {
454-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
454+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
455455
await client.connect()
456456
const foldersRes = await client.query('SELECT * FROM folders WHERE project_id = $1', [projectId])
457457
const apisRes = await client.query('SELECT * FROM api_collections WHERE project_id = $1', [projectId])
@@ -467,7 +467,7 @@ ipcMain.handle('fetch-remote-data', async (_event, url: string, projectId: strin
467467
ipcMain.handle('get-remote-projects', async (_event, url: string) => {
468468
if (url.startsWith('mysql://')) {
469469
try {
470-
const conn = await mysql.createConnection(url)
470+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
471471
const [rows]: any = await conn.execute('SELECT id, name, created_at FROM projects ORDER BY created_at DESC')
472472
await conn.end()
473473
return { success: true, projects: rows }
@@ -476,7 +476,7 @@ ipcMain.handle('get-remote-projects', async (_event, url: string) => {
476476
}
477477
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
478478
try {
479-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
479+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
480480
await client.connect()
481481
const res = await client.query('SELECT id, name, created_at FROM projects ORDER BY created_at DESC')
482482
await client.end()
@@ -491,7 +491,7 @@ ipcMain.handle('get-remote-projects', async (_event, url: string) => {
491491
ipcMain.handle('delete-remote-project', async (_event, url: string, projectId: string) => {
492492
if (url.startsWith('mysql://')) {
493493
try {
494-
const conn = await mysql.createConnection(url)
494+
const conn = await mysql.createConnection({ uri: url, connectTimeout: 10000 })
495495
await conn.execute('DELETE FROM api_collections WHERE project_id = ?', [projectId])
496496
await conn.execute('DELETE FROM folders WHERE project_id = ?', [projectId])
497497
await conn.execute('DELETE FROM rbac_users WHERE project_id = ?', [projectId])
@@ -503,7 +503,7 @@ ipcMain.handle('delete-remote-project', async (_event, url: string, projectId: s
503503
}
504504
} else if (url.startsWith('postgres://') || url.startsWith('postgresql://')) {
505505
try {
506-
const client = new pg.Client({ connectionString: url, ssl: { rejectUnauthorized: false } })
506+
const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 10000, ssl: { rejectUnauthorized: false } })
507507
await client.connect()
508508
await client.query('DELETE FROM api_collections WHERE project_id = $1', [projectId])
509509
await client.query('DELETE FROM folders WHERE project_id = $1', [projectId])

src/renderer/src/components/DatabaseSettingsDialog.tsx

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -91,54 +91,31 @@ export function DatabaseSettingsDialog() {
9191
</p>
9292
</div>
9393

94-
{/* Action Buttons Grid */}
95-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' }}>
94+
{/* Action Buttons Group */}
95+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
9696
<button
9797
onClick={handleTest}
9898
disabled={testing || !dbUrl}
99-
className="bg-white text-black text-[14px] font-bold rounded-xl hover:bg-neutral-100 disabled:opacity-20 transition-all shadow-lg active:scale-[0.98]"
99+
className={`bg-white text-black text-[14px] font-bold rounded-xl transition-all shadow-lg active:scale-[0.98] ${testing || !dbUrl ? 'opacity-20' : 'hover:bg-neutral-100'}`}
100100
style={{ padding: '16px' }}
101101
>
102102
{testing ? 'Verifying...' : 'Test Connection'}
103103
</button>
104-
<button
105-
onClick={handleCreateTables}
106-
disabled={testing || !dbUrl}
107-
className="border border-white/10 text-white text-[14px] font-bold rounded-xl hover:bg-white/5 disabled:opacity-20 transition-all active:scale-[0.98]"
108-
style={{ padding: '16px' }}
109-
>
110-
Initialize Schema
111-
</button>
112-
</div>
113104

114-
{/* Conditional Sync Button */}
115-
{project?.databaseUrl && project.databaseUrl.trim() !== '' && (
116-
<div style={{ display: 'flex', flexDirection: 'column' }}>
117-
<button
118-
onClick={async () => {
119-
if (!project) return
120-
await triggerFullProjectSync(qc, project)
121-
alert('Sync completed successfully!')
122-
}}
123-
className="bg-blue-600/10 border border-blue-600/20 text-blue-400 text-[14px] font-bold rounded-xl hover:bg-blue-600/20 transition-all active:scale-[0.98]"
124-
style={{ width: '100%', padding: '16px' }}
125-
>
126-
Trigger Full Data Sync
127-
</button>
128-
</div>
129-
)}
130-
131-
{/* Commit Button */}
132-
<div style={{ marginTop: '-8px' }}>
133105
<button
134-
onClick={() => {
135-
updateProject.mutate({ id: currentProjectId!, databaseUrl: dbUrl.trim() })
106+
onClick={async () => {
107+
if (dbUrl !== project?.databaseUrl) {
108+
// If URL changed but not tested/saved yet
109+
await handleTest()
110+
}
111+
await handleCreateTables()
136112
setShowDatabaseSettings(false)
137113
}}
138-
className="bg-emerald-600/10 border border-emerald-600/20 text-emerald-400 text-[15px] font-bold rounded-xl hover:bg-emerald-600/20 transition-all shadow-lg active:scale-[0.98]"
139-
style={{ width: '100%', padding: '16px' }}
114+
disabled={testing || !dbUrl}
115+
className={`bg-blue-600/10 border border-blue-600/20 text-blue-400 text-[14px] font-bold rounded-xl transition-all active:scale-[0.98] ${testing || !dbUrl ? 'opacity-20' : 'hover:bg-blue-600/20'}`}
116+
style={{ padding: '16px' }}
140117
>
141-
Commit Changes & Close
118+
{project?.databaseUrl === dbUrl && dbUrl ? 'Sync Now' : 'Connect & Sync'}
142119
</button>
143120
</div>
144121

src/renderer/src/components/GeneralSettingsDialog.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export function GeneralSettingsDialog() {
3737
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
3838
<div className="rounded-lg bg-white/10 flex items-center justify-center border border-white/5 shrink-0" style={{ width: '32px', height: '32px' }}>
3939
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
40-
<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 .33-1.82 1.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 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-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-.33 1.82V9a1.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" />
40+
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1-1-1.72v-.51a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
41+
<circle cx="12" cy="12" r="3" />
4142
</svg>
4243
</div>
4344
<h2 className="text-xl font-semibold text-white tracking-tight" style={{ margin: 0 }}>Project Settings</h2>

src/renderer/src/components/RbacSettingsDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ export function RbacSettingsDialog() {
520520
</div>
521521
</div>
522522

523-
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
523+
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '16px' }}>
524524
<SuccessField label="User ID" value={successData.id} />
525525
<SuccessField label="Email" value={successData.email} />
526526
</div>

0 commit comments

Comments
 (0)