Skip to content

Commit fc78830

Browse files
committed
Add SQL Console and update api-types to 1.4.5
- Add SQL Console component with Table/JSON tabs - Add shortcut buttons for helper functions (list_tables, describe_table) - Add exec() method to database-tokens service - Add bun check script for type checking - Exclude dist from tsconfig - Bump version to 1.4.1
1 parent 54f0b96 commit fc78830

File tree

8 files changed

+524
-10
lines changed

8 files changed

+524
-10
lines changed

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openworkers-dash",
3-
"version": "1.4.0",
3+
"version": "1.4.1",
44
"license": "MIT",
55
"scripts": {
66
"ng": "ng",
@@ -10,7 +10,8 @@
1010
"build": "ng build",
1111
"watch": "ng build --watch --configuration development",
1212
"test": "echo \"Error: no test specified\" && exit 1",
13-
"format": "bun x prettier --write ."
13+
"format": "bun x prettier --write .",
14+
"check": "tsc --noEmit --project tsconfig.app.json"
1415
},
1516
"private": true,
1617
"dependencies": {
@@ -36,7 +37,7 @@
3637
"@angular/build": "^21.1.1",
3738
"@angular/cli": "~21.1.1",
3839
"@angular/compiler-cli": "^21.1.1",
39-
"@openworkers/api-types": "^1.4.0",
40+
"@openworkers/api-types": "^1.4.5",
4041
"@openworkers/workers-types": "^0.1.7",
4142
"@tailwindcss/postcss": "4.1.17",
4243
"@types/jasmine": "6.0.0",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<div>
2+
<!-- Shortcuts -->
3+
<div class="flex flex-wrap gap-2 mb-3">
4+
@for (shortcut of shortcuts; track shortcut.label) {
5+
<button
6+
class="px-2 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
7+
(click)="useShortcut(shortcut.query)"
8+
>
9+
{{ shortcut.label }}
10+
</button>
11+
}
12+
<button
13+
class="px-2 py-1 text-xs rounded text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
14+
(click)="showHelp = !showHelp"
15+
>
16+
{{ showHelp ? 'Hide help' : 'Help' }}
17+
</button>
18+
</div>
19+
20+
<!-- Help -->
21+
@if (showHelp) {
22+
<div class="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded text-sm">
23+
<p class="font-medium text-blue-800 dark:text-blue-200 mb-2">Available helpers</p>
24+
<ul class="text-blue-700 dark:text-blue-300 space-y-1 text-xs font-mono">
25+
<li><code>SELECT * FROM postgate_helpers.list_tables()</code> — List all tables with row counts</li>
26+
<li><code>SELECT * FROM postgate_helpers.describe_table('name')</code> — Describe table columns</li>
27+
</ul>
28+
<p class="mt-2 text-xs text-blue-600 dark:text-blue-400">
29+
Use <code>$1, $2, ...</code> for parameters (not supported in console yet).
30+
</p>
31+
</div>
32+
}
33+
34+
<div class="mb-3">
35+
<textarea
36+
[(ngModel)]="sqlQuery"
37+
placeholder="SELECT * FROM users LIMIT 10"
38+
class="input-outline w-full font-mono text-sm"
39+
rows="4"
40+
(keydown.ctrl.enter)="executeQuery()"
41+
(keydown.meta.enter)="executeQuery()"
42+
></textarea>
43+
<p class="text-xs text-gray-500 mt-1">Ctrl+Enter to execute</p>
44+
</div>
45+
46+
<div class="flex gap-2 mb-4">
47+
<button class="btn-blue" (click)="executeQuery()" [disabled]="!sqlQuery.trim() || (executing$ | async)">
48+
@if (executing$ | async) {
49+
Executing...
50+
} @else {
51+
Execute
52+
}
53+
</button>
54+
@if (sqlResult || sqlError) {
55+
<button class="btn-outline" (click)="clearResult()">Clear</button>
56+
}
57+
</div>
58+
59+
<!-- Error -->
60+
@if (sqlError) {
61+
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3 rounded">
62+
<p class="text-sm text-red-800 dark:text-red-200 font-mono">{{ sqlError }}</p>
63+
</div>
64+
}
65+
66+
<!-- Results -->
67+
@if (sqlResult) {
68+
<div class="border dark:border-gray-700 rounded overflow-hidden">
69+
<!-- Tabs -->
70+
<div class="flex border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
71+
<button
72+
class="px-4 py-2 text-sm font-medium border-b-2 -mb-px"
73+
[class.border-blue-500]="activeTab === 'table'"
74+
[class.text-blue-600]="activeTab === 'table'"
75+
[class.dark:text-blue-400]="activeTab === 'table'"
76+
[class.border-transparent]="activeTab !== 'table'"
77+
[class.text-gray-600]="activeTab !== 'table'"
78+
[class.dark:text-gray-400]="activeTab !== 'table'"
79+
(click)="activeTab = 'table'"
80+
>
81+
Table
82+
</button>
83+
<button
84+
class="px-4 py-2 text-sm font-medium border-b-2 -mb-px"
85+
[class.border-blue-500]="activeTab === 'json'"
86+
[class.text-blue-600]="activeTab === 'json'"
87+
[class.dark:text-blue-400]="activeTab === 'json'"
88+
[class.border-transparent]="activeTab !== 'json'"
89+
[class.text-gray-600]="activeTab !== 'json'"
90+
[class.dark:text-gray-400]="activeTab !== 'json'"
91+
(click)="activeTab = 'json'"
92+
>
93+
JSON
94+
</button>
95+
<div class="flex-1"></div>
96+
<span class="px-4 py-2 text-sm text-gray-500">
97+
{{ sqlResult.rowCount }} row{{ sqlResult.rowCount !== 1 ? 's' : '' }}
98+
</span>
99+
</div>
100+
101+
<!-- Table View -->
102+
@if (activeTab === 'table') {
103+
@if (sqlResult.rows.length > 0) {
104+
<div class="overflow-x-auto">
105+
<table class="w-full text-sm">
106+
<thead>
107+
<tr class="bg-gray-100 dark:bg-gray-800">
108+
@for (col of getColumns(); track col) {
109+
<th class="px-3 py-2 text-left border-b dark:border-gray-700 font-medium">{{ col }}</th>
110+
}
111+
</tr>
112+
</thead>
113+
<tbody>
114+
@for (row of sqlResult.rows; track $index) {
115+
<tr class="border-b dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50">
116+
@for (col of getColumns(); track col) {
117+
<td class="px-3 py-2 font-mono text-xs">
118+
@if (getCellValue(row, col) === null) {
119+
<span class="text-gray-400 italic">null</span>
120+
} @else if (getCellValue(row, col) === true) {
121+
<span class="text-green-600 dark:text-green-400">true</span>
122+
} @else if (getCellValue(row, col) === false) {
123+
<span class="text-red-600 dark:text-red-400">false</span>
124+
} @else {
125+
{{ getCellValue(row, col) }}
126+
}
127+
</td>
128+
}
129+
</tr>
130+
}
131+
</tbody>
132+
</table>
133+
</div>
134+
} @else {
135+
<div class="p-4 text-sm text-gray-500 text-center">No rows returned</div>
136+
}
137+
}
138+
139+
<!-- JSON View -->
140+
@if (activeTab === 'json') {
141+
<pre class="p-4 text-xs font-mono overflow-x-auto bg-gray-50 dark:bg-gray-900 max-h-96">{{ formatJson() }}</pre>
142+
}
143+
</div>
144+
}
145+
</div>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Component, Input } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { FormsModule } from '@angular/forms';
4+
import { BehaviorSubject } from 'rxjs';
5+
import { DatabaseTokensService, IExecResult } from '~/services/database-tokens.service';
6+
7+
type ResultTab = 'table' | 'json';
8+
9+
@Component({
10+
selector: 'app-sql-console',
11+
standalone: true,
12+
imports: [CommonModule, FormsModule],
13+
templateUrl: './sql-console.component.html'
14+
})
15+
export class SqlConsoleComponent {
16+
@Input({ required: true }) databaseId!: string;
17+
18+
public sqlQuery = '';
19+
public sqlResult: IExecResult | null = null;
20+
public sqlError: string | null = null;
21+
public executing$ = new BehaviorSubject<boolean>(false);
22+
public activeTab: ResultTab = 'table';
23+
public showHelp = false;
24+
25+
public readonly shortcuts = [
26+
{ label: 'List tables', query: "SELECT * FROM postgate_helpers.list_tables()" },
27+
{ label: 'Describe table', query: "SELECT * FROM postgate_helpers.describe_table('TABLE_NAME')" }
28+
];
29+
30+
constructor(private tokensService: DatabaseTokensService) {}
31+
32+
executeQuery() {
33+
if (!this.sqlQuery.trim()) return;
34+
35+
this.executing$.next(true);
36+
this.sqlResult = null;
37+
this.sqlError = null;
38+
39+
this.tokensService.exec(this.databaseId, this.sqlQuery).subscribe({
40+
next: (result) => {
41+
this.sqlResult = result;
42+
this.executing$.next(false);
43+
},
44+
error: (err) => {
45+
this.sqlError = err.error?.error || err.message || 'Query failed';
46+
this.executing$.next(false);
47+
}
48+
});
49+
}
50+
51+
clearResult() {
52+
this.sqlResult = null;
53+
this.sqlError = null;
54+
}
55+
56+
getColumns(): string[] {
57+
if (!this.sqlResult?.rows?.length) return [];
58+
return Object.keys(this.sqlResult.rows[0]);
59+
}
60+
61+
getCellValue(row: Record<string, unknown>, key: string): unknown {
62+
return row[key];
63+
}
64+
65+
formatJson(): string {
66+
if (!this.sqlResult) return '';
67+
return JSON.stringify(this.sqlResult, null, 2);
68+
}
69+
70+
useShortcut(query: string) {
71+
this.sqlQuery = query;
72+
}
73+
}

0 commit comments

Comments
 (0)