Skip to content

Commit 0fab037

Browse files
committed
Add database support
1 parent 97b52ce commit 0fab037

15 files changed

Lines changed: 854 additions & 124 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<app-modal [open]="open" (openChange)="close()" variant="large">
2+
<div role="header">
3+
<h4>Create Table</h4>
4+
</div>
5+
6+
<div role="body" class="space-y-4">
7+
@if (error()) {
8+
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-md text-sm">
9+
{{ error() }}
10+
</div>
11+
}
12+
13+
<div>
14+
<label class="block text-sm font-medium mb-1">Table Name</label>
15+
<input
16+
type="text"
17+
class="input-outline w-full font-mono"
18+
[(ngModel)]="tableName"
19+
placeholder="users"
20+
pattern="^[a-zA-Z_][a-zA-Z0-9_]*$"
21+
/>
22+
</div>
23+
24+
<div>
25+
<div class="flex justify-between items-center mb-2">
26+
<label class="block text-sm font-medium">Columns</label>
27+
<button type="button" class="text-sm text-blue-600 hover:text-blue-800" (click)="addColumn()">
28+
+ Add Column
29+
</button>
30+
</div>
31+
32+
<div class="space-y-2 max-h-64 overflow-y-auto">
33+
@for (col of columns(); track $index; let i = $index) {
34+
<div class="flex gap-2 items-start p-2 bg-gray-50 dark:bg-gray-800 rounded-md">
35+
<div class="flex-1">
36+
<input
37+
type="text"
38+
class="input-outline w-full text-sm font-mono"
39+
[ngModel]="col.name"
40+
(ngModelChange)="updateColumn(i, 'name', $event)"
41+
placeholder="column_name"
42+
/>
43+
</div>
44+
45+
<div class="w-32">
46+
<select
47+
class="input-outline w-full text-sm"
48+
[ngModel]="col.type"
49+
(ngModelChange)="updateColumn(i, 'type', $event)"
50+
>
51+
@for (type of commonTypes; track type) {
52+
<option [value]="type">{{ type }}</option>
53+
}
54+
</select>
55+
</div>
56+
57+
<div class="flex items-center gap-2 text-xs">
58+
<label class="flex items-center gap-1 cursor-pointer">
59+
<input
60+
type="checkbox"
61+
[ngModel]="col.primaryKey"
62+
(ngModelChange)="updateColumn(i, 'primaryKey', $event)"
63+
/>
64+
PK
65+
</label>
66+
67+
<label class="flex items-center gap-1 cursor-pointer">
68+
<input
69+
type="checkbox"
70+
[ngModel]="col.notNull"
71+
(ngModelChange)="updateColumn(i, 'notNull', $event)"
72+
/>
73+
NN
74+
</label>
75+
76+
<label class="flex items-center gap-1 cursor-pointer">
77+
<input
78+
type="checkbox"
79+
[ngModel]="col.unique"
80+
(ngModelChange)="updateColumn(i, 'unique', $event)"
81+
/>
82+
UQ
83+
</label>
84+
</div>
85+
86+
<button
87+
type="button"
88+
class="text-red-500 hover:text-red-700 px-2"
89+
(click)="removeColumn(i)"
90+
[disabled]="columns().length <= 1"
91+
>
92+
×
93+
</button>
94+
</div>
95+
}
96+
</div>
97+
98+
<p class="text-xs text-gray-500 mt-2">
99+
PK = Primary Key, NN = Not Null, UQ = Unique
100+
</p>
101+
</div>
102+
</div>
103+
104+
<div role="footer" class="flex gap-2 justify-end">
105+
<button class="btn-outline" (click)="close()" [disabled]="isCreating()">Cancel</button>
106+
<button class="btn-primary" (click)="createTable()" [disabled]="isCreating() || !tableName.trim()">
107+
{{ isCreating() ? 'Creating...' : 'Create Table' }}
108+
</button>
109+
</div>
110+
</app-modal>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { FormsModule } from '@angular/forms';
4+
import { firstValueFrom } from 'rxjs';
5+
import { ModalComponent } from '~/app/shared/modal/modal.component';
6+
import { TablesService } from '~/app/services/tables.service';
7+
import type { IColumnDefinition } from '@openworkers/api-types';
8+
9+
const COMMON_TYPES = ['TEXT', 'INTEGER', 'BIGINT', 'SERIAL', 'BOOLEAN', 'TIMESTAMPTZ', 'UUID', 'JSONB', 'NUMERIC', 'REAL'];
10+
11+
@Component({
12+
standalone: true,
13+
selector: 'app-create-table-modal',
14+
imports: [CommonModule, FormsModule, ModalComponent],
15+
templateUrl: './create-table-modal.component.html'
16+
})
17+
export class CreateTableModalComponent {
18+
@Input() open = false;
19+
@Input({ required: true }) databaseId!: string;
20+
@Output() openChange = new EventEmitter<boolean>();
21+
@Output() created = new EventEmitter<void>();
22+
23+
tableName = '';
24+
columns = signal<IColumnDefinition[]>([{ name: 'id', type: 'SERIAL', primaryKey: true }]);
25+
isCreating = signal(false);
26+
error = signal<string | null>(null);
27+
28+
commonTypes = COMMON_TYPES;
29+
30+
constructor(private tablesService: TablesService) {}
31+
32+
addColumn() {
33+
this.columns.update((cols) => [...cols, { name: '', type: 'TEXT' }]);
34+
}
35+
36+
removeColumn(index: number) {
37+
this.columns.update((cols) => cols.filter((_, i) => i !== index));
38+
}
39+
40+
updateColumn(index: number, field: keyof IColumnDefinition, value: any) {
41+
this.columns.update((cols) => {
42+
const updated = [...cols];
43+
updated[index] = { ...updated[index], [field]: value };
44+
return updated;
45+
});
46+
}
47+
48+
async createTable() {
49+
if (!this.tableName.trim() || this.columns().length === 0) {
50+
this.error.set('Table name and at least one column are required');
51+
return;
52+
}
53+
54+
// Validate column names
55+
const invalidCol = this.columns().find((c) => !c.name.trim());
56+
57+
if (invalidCol) {
58+
this.error.set('All columns must have a name');
59+
return;
60+
}
61+
62+
this.isCreating.set(true);
63+
this.error.set(null);
64+
65+
try {
66+
await firstValueFrom(
67+
this.tablesService.createTable(this.databaseId, {
68+
name: this.tableName.trim(),
69+
columns: this.columns()
70+
})
71+
);
72+
73+
this.reset();
74+
this.created.emit();
75+
} catch (err) {
76+
console.error('Failed to create table:', err);
77+
this.error.set(err instanceof Error ? err.message : 'Failed to create table');
78+
} finally {
79+
this.isCreating.set(false);
80+
}
81+
}
82+
83+
reset() {
84+
this.tableName = '';
85+
this.columns.set([{ name: 'id', type: 'SERIAL', primaryKey: true }]);
86+
this.error.set(null);
87+
}
88+
89+
close() {
90+
this.reset();
91+
this.openChange.emit(false);
92+
}
93+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
@if (provider !== 'platform') {
2+
<div class="text-sm text-gray-500">
3+
Table management is only available for Platform databases.
4+
</div>
5+
} @else {
6+
<div class="flex justify-between items-center mb-4">
7+
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300">Tables</h5>
8+
<button class="btn-outline text-sm" (click)="openCreateModal()">
9+
+ New Table
10+
</button>
11+
</div>
12+
13+
@if (isLoading()) {
14+
<div class="text-sm text-gray-500">Loading...</div>
15+
} @else if (error()) {
16+
<div class="text-sm text-red-500">{{ error() }}</div>
17+
} @else if (tables().length === 0) {
18+
<div class="text-sm text-gray-500">No tables yet. Create your first table to get started.</div>
19+
} @else {
20+
<div class="space-y-2">
21+
@for (table of tables(); track table.name) {
22+
<div
23+
class="border rounded-lg overflow-hidden dark:border-gray-700"
24+
[class.border-blue-500]="selectedTable() === table.name"
25+
>
26+
<div
27+
class="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
28+
(click)="selectTable(table.name)"
29+
>
30+
<div class="flex items-center gap-2">
31+
<span class="font-mono text-sm">{{ table.name }}</span>
32+
<span class="text-xs text-gray-500">({{ table.rowCount }} rows)</span>
33+
</div>
34+
<button
35+
class="text-red-500 hover:text-red-700 text-sm px-2"
36+
(click)="confirmDelete(table.name, $event)"
37+
title="Delete table"
38+
>
39+
×
40+
</button>
41+
</div>
42+
43+
@if (selectedTable() === table.name && selectedColumns().length > 0) {
44+
<div class="border-t dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-3 py-2">
45+
<table class="w-full text-xs">
46+
<thead>
47+
<tr class="text-left text-gray-500">
48+
<th class="pb-1">Column</th>
49+
<th class="pb-1">Type</th>
50+
<th class="pb-1">Nullable</th>
51+
<th class="pb-1">Default</th>
52+
<th class="pb-1 w-8"></th>
53+
</tr>
54+
</thead>
55+
<tbody>
56+
@for (col of selectedColumns(); track col.name) {
57+
<tr class="group">
58+
<td class="py-0.5 font-mono">
59+
{{ col.name }}
60+
@if (col.primaryKey) {
61+
<span class="text-yellow-600 ml-1" title="Primary Key">🔑</span>
62+
}
63+
</td>
64+
<td class="py-0.5 text-gray-600 dark:text-gray-400">{{ col.type }}</td>
65+
<td class="py-0.5">{{ col.nullable ? 'Yes' : 'No' }}</td>
66+
<td class="py-0.5 text-gray-500 font-mono max-w-[120px] truncate" [title]="col.defaultValue || ''">{{ col.defaultValue || '-' }}</td>
67+
<td class="py-0.5 text-right">
68+
@if (!col.primaryKey) {
69+
<button
70+
class="text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
71+
(click)="confirmDeleteColumn(col.name, $event)"
72+
title="Drop column"
73+
>
74+
×
75+
</button>
76+
}
77+
</td>
78+
</tr>
79+
}
80+
</tbody>
81+
</table>
82+
<button class="text-xs text-blue-600 hover:text-blue-800 mt-2" (click)="openAddColumnModal()">
83+
+ Add Column
84+
</button>
85+
</div>
86+
}
87+
</div>
88+
}
89+
</div>
90+
}
91+
}
92+
93+
<!-- Create Table Modal -->
94+
<app-create-table-modal
95+
[open]="showCreateModal()"
96+
[databaseId]="databaseId"
97+
(openChange)="showCreateModal.set($event)"
98+
(created)="onTableCreated()"
99+
/>
100+
101+
<!-- Delete Table Confirmation Modal -->
102+
<app-modal [open]="showDeleteConfirm()" (openChange)="showDeleteConfirm.set($event)" variant="small">
103+
<div role="header">
104+
<h4>Delete Table</h4>
105+
</div>
106+
<div role="body">
107+
<p>Are you sure you want to delete table <strong class="font-mono">{{ tableToDelete() }}</strong>?</p>
108+
<p class="text-sm text-red-600 mt-2">This action cannot be undone. All data will be permanently deleted.</p>
109+
</div>
110+
<div role="footer" class="flex gap-2 justify-end">
111+
<button class="btn-outline" (click)="showDeleteConfirm.set(false)">Cancel</button>
112+
<button class="btn-primary bg-red-600 hover:bg-red-700" (click)="deleteTable()">Delete</button>
113+
</div>
114+
</app-modal>
115+
116+
<!-- Add Column Modal -->
117+
<app-modal [open]="showAddColumnModal()" (openChange)="showAddColumnModal.set($event)" variant="small">
118+
<div role="header">
119+
<h4>Add Column to {{ selectedTable() }}</h4>
120+
</div>
121+
<div role="body" class="space-y-3">
122+
<div>
123+
<label class="block text-sm font-medium mb-1">Column Name</label>
124+
<input
125+
type="text"
126+
class="input-outline w-full font-mono"
127+
[(ngModel)]="newColumn.name"
128+
placeholder="column_name"
129+
pattern="^[a-zA-Z_][a-zA-Z0-9_]*$"
130+
/>
131+
</div>
132+
<div>
133+
<label class="block text-sm font-medium mb-1">Type</label>
134+
<select class="input-outline w-full" [(ngModel)]="newColumn.type">
135+
@for (type of commonTypes; track type) {
136+
<option [value]="type">{{ type }}</option>
137+
}
138+
</select>
139+
</div>
140+
<div class="flex gap-4 text-sm">
141+
<label class="flex items-center gap-1 cursor-pointer">
142+
<input type="checkbox" [(ngModel)]="newColumn.notNull" />
143+
NOT NULL
144+
</label>
145+
<label class="flex items-center gap-1 cursor-pointer">
146+
<input type="checkbox" [(ngModel)]="newColumn.unique" />
147+
UNIQUE
148+
</label>
149+
</div>
150+
</div>
151+
<div role="footer" class="flex gap-2 justify-end">
152+
<button class="btn-outline" (click)="showAddColumnModal.set(false)">Cancel</button>
153+
<button class="btn-primary" [disabled]="!newColumn.name.trim()" (click)="addColumn()">Add Column</button>
154+
</div>
155+
</app-modal>
156+
157+
<!-- Delete Column Confirmation Modal -->
158+
<app-modal [open]="showDeleteColumnConfirm()" (openChange)="showDeleteColumnConfirm.set($event)" variant="small">
159+
<div role="header">
160+
<h4>Drop Column</h4>
161+
</div>
162+
<div role="body">
163+
<p>Are you sure you want to drop column <strong class="font-mono">{{ columnToDelete() }}</strong>?</p>
164+
<p class="text-sm text-red-600 mt-2">This action cannot be undone. All data in this column will be permanently deleted.</p>
165+
</div>
166+
<div role="footer" class="flex gap-2 justify-end">
167+
<button class="btn-outline" (click)="showDeleteColumnConfirm.set(false)">Cancel</button>
168+
<button class="btn-primary bg-red-600 hover:bg-red-700" (click)="deleteColumn()">Drop Column</button>
169+
</div>
170+
</app-modal>

0 commit comments

Comments
 (0)