admindb 1.0.1 → 1.1.0

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.
Files changed (48) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +354 -325
  3. package/dist/db/database.d.ts +61 -39
  4. package/dist/db/database.js +251 -51
  5. package/dist/db/database.js.map +1 -1
  6. package/dist/db/manager.d.ts +2 -2
  7. package/dist/db/manager.js +3 -2
  8. package/dist/db/manager.js.map +1 -1
  9. package/dist/public/css/app.css +1 -1
  10. package/dist/public/css/input.css +232 -179
  11. package/dist/public/js/api.js +47 -47
  12. package/dist/public/js/app.js +120 -120
  13. package/dist/public/js/browse.js +877 -123
  14. package/dist/public/js/databases.js +45 -45
  15. package/dist/public/js/designer.js +205 -205
  16. package/dist/public/js/forms.js +244 -244
  17. package/dist/public/js/query.js +232 -232
  18. package/dist/public/js/schema.js +314 -314
  19. package/dist/routes/api.js +261 -97
  20. package/dist/routes/api.js.map +1 -1
  21. package/dist/routes/pages.js +50 -30
  22. package/dist/routes/pages.js.map +1 -1
  23. package/dist/sql/generator.d.ts +0 -22
  24. package/dist/sql/generator.js +0 -48
  25. package/dist/sql/generator.js.map +1 -1
  26. package/dist/test/database.test.js +241 -63
  27. package/dist/test/database.test.js.map +1 -1
  28. package/dist/test/generator.test.js +0 -32
  29. package/dist/test/generator.test.js.map +1 -1
  30. package/dist/util.d.ts +13 -1
  31. package/dist/util.js +24 -2
  32. package/dist/util.js.map +1 -1
  33. package/dist/views/layouts/main.hbs +49 -49
  34. package/dist/views/pages/databases.hbs +102 -102
  35. package/dist/views/pages/designer.hbs +81 -81
  36. package/dist/views/pages/error.hbs +12 -12
  37. package/dist/views/pages/form.hbs +62 -62
  38. package/dist/views/pages/home.hbs +123 -123
  39. package/dist/views/pages/query.hbs +94 -94
  40. package/dist/views/pages/schema.hbs +290 -290
  41. package/dist/views/pages/table.hbs +256 -203
  42. package/dist/views/partials/navbar.hbs +50 -50
  43. package/dist/views/partials/sidebar.hbs +116 -128
  44. package/package.json +66 -62
  45. package/dist/public/js/triggers.js +0 -155
  46. package/dist/public/js/views.js +0 -176
  47. package/dist/views/pages/triggers.hbs +0 -162
  48. package/dist/views/pages/views.hbs +0 -134
@@ -1,203 +1,256 @@
1
- <div class="space-y-5">
2
- <!-- Header -->
3
- <div class="flex flex-wrap items-center justify-between gap-3">
4
- <div class="flex items-center gap-3.5">
5
- <span class="grid h-12 w-12 place-items-center rounded-xl bg-primary/10 text-primary">
6
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
7
- </span>
8
- <div>
9
- <h1 class="font-mono text-2xl font-bold tracking-tight">{{table}}</h1>
10
- <p class="text-sm text-base-content/55">
11
- {{count}} row(s) · {{info.columns.length}} column(s)
12
- {{#if pkCols.length}}· PK <span class="font-mono">{{join pkCols ", "}}</span>{{/if}}
13
- </p>
14
- </div>
15
- </div>
16
- <div class="flex flex-wrap gap-2">
17
- <a href="{{basePath}}/tables/{{table}}/schema" class="btn btn-ghost btn-sm gap-1.5">
18
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
19
- Edit schema
20
- </a>
21
- <a href="{{basePath}}/export" class="btn btn-ghost btn-sm gap-1.5">
22
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
23
- SQL dump
24
- </a>
25
- <div class="dropdown dropdown-end">
26
- <div tabindex="0" role="button" class="btn btn-outline btn-sm gap-1.5">
27
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
28
- Export
29
- <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
30
- </div>
31
- <ul tabindex="0" class="dropdown-content menu z-50 w-40 rounded-xl border border-base-300 bg-base-100 p-1.5 shadow-lift">
32
- <li><a href="{{basePath}}/api/tables/{{table}}/export?format=csv">CSV</a></li>
33
- <li><a href="{{basePath}}/api/tables/{{table}}/export?format=json">JSON</a></li>
34
- </ul>
35
- </div>
36
- {{#unless readonly}}
37
- <button id="import-csv" type="button" class="btn btn-outline btn-sm gap-1.5">
38
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
39
- Import CSV
40
- </button>
41
- {{/unless}}
42
- <a href="{{basePath}}/query" class="btn btn-outline btn-sm gap-1.5">
43
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
44
- Query
45
- </a>
46
- {{#unless readonly}}
47
- <a href="{{basePath}}/tables/{{table}}/rows/new" class="btn btn-primary btn-sm gap-1.5">
48
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
49
- New row
50
- </a>
51
- {{/unless}}
52
- </div>
53
- </div>
54
-
55
- <!-- Row filters -->
56
- <form id="filter-form" class="app-card p-4">
57
- <div class="mb-3 flex flex-wrap items-center justify-between gap-2">
58
- <span class="flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-base-content/45">
59
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414A1 1 0 0014 15.414V20a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /></svg>
60
- Filter rows
61
- </span>
62
- <span class="text-xs text-base-content/40">Tip: <code>=exact</code> · <code>&gt;5</code> · <code>&lt;=10</code> · <code>pre*</code> starts with · plain text = contains</span>
63
- </div>
64
- <div class="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4">
65
- {{#each colNames}}
66
- <label class="block">
67
- <span class="field-label !mb-1 font-mono">{{this}}</span>
68
- <input type="text" class="field-input filter-input !py-1.5 !text-[13px]" name="{{this}}" value="{{lookup ../filters this}}" placeholder="filter…" autocomplete="off" spellcheck="false" />
69
- </label>
70
- {{/each}}
71
- </div>
72
- <div class="mt-3 flex flex-wrap items-center gap-2">
73
- <button type="submit" class="btn btn-primary btn-sm gap-1.5">
74
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
75
- Apply filter
76
- </button>
77
- <button type="button" id="clear-filter" class="btn btn-ghost btn-sm">Clear</button>
78
- {{#if filterQS}}
79
- <span class="text-xs text-base-content/50">Filtering <b class="tabular-nums">{{count}}</b> row(s).</span>
80
- {{/if}}
81
- </div>
82
- </form>
83
-
84
- <!-- CSV import -->
85
- <div id="import-panel" class="app-card hidden p-4">
86
- <div class="mb-3 flex items-center justify-between">
87
- <h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/45">Import CSV into <span class="font-mono normal-case">{{table}}</span></h2>
88
- <button id="import-csv-close" type="button" class="btn btn-ghost btn-xs" aria-label="Close">&times;</button>
89
- </div>
90
- <div class="space-y-3">
91
- <textarea id="import-csv-text" rows="6" class="field-input w-full font-mono text-[13px]" placeholder="id,name,email&#10;1,Ada,ada@example.com&#10;2,Bob,bob@example.com" spellcheck="false"></textarea>
92
- <div class="flex flex-wrap items-center gap-3">
93
- <input type="file" id="import-csv-file" class="file-input file-input-sm w-full max-w-xs" accept=".csv,text/csv" />
94
- <button id="import-csv-submit" type="button" class="btn btn-primary btn-sm gap-1.5">
95
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
96
- Import
97
- </button>
98
- </div>
99
- <p class="text-xs text-base-content/45">The first row must be column names matching this table. Empty cells are treated as “not set” so database defaults apply. The import is transactional — a failed row rolls back everything.</p>
100
- </div>
101
- </div>
102
-
103
- {{#if rows.length}}
104
- <div class="app-card overflow-hidden">
105
- <!-- Toolbar -->
106
- <div class="flex flex-wrap items-center justify-between gap-2 border-b border-base-200 px-4 py-3">
107
- <span class="text-sm text-base-content/55">Showing <b class="tabular-nums">{{firstRow}}</b>–<b class="tabular-nums">{{lastRow}}</b> of <b class="tabular-nums">{{count}}</b> rows</span>
108
- <label class="flex items-center gap-2 text-sm text-base-content/55">
109
- Rows per page
110
- <select id="page-size" class="field-select !w-auto !py-1.5 !text-[13px]" data-table="{{table}}" data-orderby="{{orderBy}}" data-orderdir="{{orderDir}}" data-filters="{{filterQS}}">
111
- {{#each sizes}}<option value="{{this}}" {{#if (eq this ../pageSize)}}selected{{/if}}>{{this}}</option>{{/each}}
112
- </select>
113
- </label>
114
- </div>
115
-
116
- <div class="overflow-x-auto">
117
- <table class="table table-sm table-pin-rows">
118
- <thead>
119
- <tr class="bg-base-200/70 text-xs uppercase tracking-wide text-base-content/50">
120
- {{#each colHeaders}}
121
- <th class="whitespace-nowrap font-semibold">
122
- <a href="{{this.href}}" class="sort-link {{#if this.active}}active{{/if}}" title="Sort by {{this.name}}">
123
- <span class="font-mono normal-case">{{this.name}}</span>
124
- {{#if this.pk}}<span class="chip chip-pk">PK</span>{{/if}}
125
- <span class="sort-arrow">{{#if (eq this.dir 'desc')}}↓{{else}}↑{{/if}}</span>
126
- </a>
127
- </th>
128
- {{/each}}
129
- <th class="text-right font-semibold">Actions</th>
130
- </tr>
131
- </thead>
132
- <tbody>
133
- {{#each rows}}
134
- <tr class="group hover:bg-primary/[0.04]">
135
- {{#each this.cells}}
136
- <td class="max-w-xs align-middle">
137
- {{#if this.isNull}}
138
- <span class="chip chip-null">NULL</span>
139
- {{else}}
140
- <span class="block max-w-xs truncate font-mono text-[13px]" title="{{this.display}}">{{truncate this.display 120}}</span>
141
- {{/if}}
142
- </td>
143
- {{/each}}
144
- <td class="whitespace-nowrap text-right align-middle">
145
- {{#unless ../readonly}}
146
- {{#if this.pkEncoded}}
147
- <div class="row-actions">
148
- <a href="{{../basePath}}/tables/{{../table}}/rows/{{this.pkEncoded}}/edit" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
149
- <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
150
- Edit
151
- </a>
152
- <button type="button" class="btn btn-ghost btn-xs gap-1 text-error js-delete-row"
153
- data-table="{{../table}}" data-pk="{{this.pkEncoded}}">
154
- <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
155
- Delete
156
- </button>
157
- </div>
158
- {{else}}
159
- <span class="text-xs text-base-content/35">No PK</span>
160
- {{/if}}
161
- {{/unless}}
162
- </td>
163
- </tr>
164
- {{/each}}
165
- </tbody>
166
- </table>
167
- </div>
168
-
169
- <!-- Pagination -->
170
- {{#if (gt pages 1)}}
171
- <div class="flex flex-wrap items-center justify-between gap-2 border-t border-base-200 px-4 py-3">
172
- <span class="text-sm text-base-content/50">Page {{page}} of {{pages}}</span>
173
- <div class="flex gap-1.5">
174
- <a href="{{basePath}}/tables/{{table}}?page={{sub page 1}}&size={{pageSize}}&orderBy={{orderBy}}&orderDir={{orderDir}}{{#if filterQS}}&{{filterQS}}{{/if}}"
175
- class="btn btn-outline btn-sm {{#unless (gt page 1)}}btn-disabled{{/unless}}" aria-disabled="{{#if (gt page 1)}}false{{else}}true{{/if}}">
176
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg>
177
- Prev
178
- </a>
179
- <a href="{{basePath}}/tables/{{table}}?page={{add page 1}}&size={{pageSize}}&orderBy={{orderBy}}&orderDir={{orderDir}}{{#if filterQS}}&{{filterQS}}{{/if}}"
180
- class="btn btn-outline btn-sm {{#unless (lt page pages)}}btn-disabled{{/unless}}" aria-disabled="{{#if (lt page pages)}}false{{else}}true{{/if}}">
181
- Next
182
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg>
183
- </a>
184
- </div>
185
- </div>
186
- {{/if}}
187
- </div>
188
- {{else}}
189
- <div class="empty-state">
190
- <span class="empty-icon">
191
- <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>
192
- </span>
193
- <p class="text-sm font-medium text-base-content/70">No rows in <span class="font-mono">{{table}}</span></p>
194
- <div class="mt-2 flex gap-2">
195
- {{#unless readonly}}
196
- <a href="{{basePath}}/tables/{{table}}/rows/new" class="btn btn-primary btn-sm">Insert a row</a>
197
- {{/unless}}
198
- <a href="{{basePath}}/query" class="btn btn-outline btn-sm">Run a query</a>
199
- </div>
200
- </div>
201
- {{/if}}
202
- </div>
203
- <script src="{{basePath}}/js/browse.js" defer></script>
1
+ <div class="space-y-5">
2
+ <!-- Header -->
3
+ <div class="flex flex-wrap items-center justify-between gap-3">
4
+ <div class="flex items-center gap-3.5">
5
+ <span class="grid h-12 w-12 place-items-center rounded-xl bg-primary/10 text-primary">
6
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
7
+ </span>
8
+ <div>
9
+ <h1 class="font-mono text-2xl font-bold tracking-tight">{{table}}</h1>
10
+ <p class="text-sm text-base-content/55">
11
+ {{count}} row(s) · {{info.columns.length}} column(s)
12
+ {{#if pkCols.length}}· PK <span class="font-mono">{{join pkCols ", "}}</span>{{/if}}
13
+ </p>
14
+ </div>
15
+ </div>
16
+ <div class="flex flex-wrap gap-2">
17
+ <a href="{{basePath}}/tables/{{table}}/schema" class="btn btn-ghost btn-sm gap-1.5">
18
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
19
+ Edit schema
20
+ </a>
21
+ <a href="{{basePath}}/export" class="btn btn-ghost btn-sm gap-1.5">
22
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
23
+ SQL dump
24
+ </a>
25
+ <div class="dropdown dropdown-end">
26
+ <div tabindex="0" role="button" class="btn btn-outline btn-sm gap-1.5">
27
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
28
+ Export
29
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
30
+ </div>
31
+ <ul tabindex="0" class="dropdown-content menu z-50 w-40 rounded-xl border border-base-300 bg-base-100 p-1.5 shadow-lift">
32
+ <li><a href="{{basePath}}/api/tables/{{table}}/export?format=csv">CSV</a></li>
33
+ <li><a href="{{basePath}}/api/tables/{{table}}/export?format=json">JSON</a></li>
34
+ </ul>
35
+ </div>
36
+ {{#unless readonly}}
37
+ <button id="import-csv" type="button" class="btn btn-outline btn-sm gap-1.5">
38
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
39
+ Import CSV
40
+ </button>
41
+ {{/unless}}
42
+ <a href="{{basePath}}/query" class="btn btn-outline btn-sm gap-1.5">
43
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
44
+ Query
45
+ </a>
46
+ {{#unless readonly}}
47
+ <a href="{{basePath}}/tables/{{table}}/rows/new" class="btn btn-primary btn-sm gap-1.5">
48
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
49
+ New row
50
+ </a>
51
+ {{/unless}}
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Row filters (collapsed by default; auto-expands when filters are active) -->
56
+ <details class="app-card overflow-hidden" {{#if filterQS}}open{{/if}}>
57
+ <summary class="group flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 transition hover:bg-base-200/40 [&::-webkit-details-marker]:hidden">
58
+ <span class="flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-base-content/45">
59
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414A1 1 0 0014 15.414V20a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /></svg>
60
+ Filter rows
61
+ {{#if filterQS}}<span class="chip chip-pk">active</span>{{/if}}
62
+ </span>
63
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 text-base-content/40 transition-transform group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
64
+ </summary>
65
+ <form id="filter-form" class="border-t border-base-200 p-4">
66
+ <p class="mb-3 text-xs text-base-content/40">Filters adapt to each column's type: foreign keys, booleans, dates and numbers get dedicated controls. Text columns keep the operator syntax — <code>=exact</code> · <code>&gt;5</code> · <code>pre*</code> starts with · plain text = contains.</p>
67
+ <div id="filter-fields" class="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4"></div>
68
+ <div class="mt-3 flex flex-wrap items-center gap-2">
69
+ <button type="submit" class="btn btn-primary btn-sm gap-1.5">
70
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
71
+ Apply filter
72
+ </button>
73
+ <button type="button" id="clear-filter" class="btn btn-ghost btn-sm">Clear</button>
74
+ {{#if filterQS}}
75
+ <span class="text-xs text-base-content/50">Filtering <b class="tabular-nums">{{count}}</b> row(s).</span>
76
+ {{/if}}
77
+ </div>
78
+ </form>
79
+ </details>
80
+
81
+ <!-- CSV import -->
82
+ <div id="import-panel" class="app-card hidden p-4">
83
+ <div class="mb-3 flex items-center justify-between">
84
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/45">Import CSV into <span class="font-mono normal-case">{{table}}</span></h2>
85
+ <button id="import-csv-close" type="button" class="btn btn-ghost btn-xs" aria-label="Close">&times;</button>
86
+ </div>
87
+ <div class="space-y-3">
88
+ <textarea id="import-csv-text" rows="6" class="field-input w-full font-mono text-[13px]" placeholder="id,name,email&#10;1,Ada,ada@example.com&#10;2,Bob,bob@example.com" spellcheck="false"></textarea>
89
+ <div class="flex flex-wrap items-center gap-3">
90
+ <input type="file" id="import-csv-file" class="file-input file-input-sm w-full max-w-xs" accept=".csv,text/csv" />
91
+ <button id="import-csv-submit" type="button" class="btn btn-primary btn-sm gap-1.5">
92
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
93
+ Import
94
+ </button>
95
+ </div>
96
+ <p class="text-xs text-base-content/45">The first row must be column names matching this table. Empty cells are treated as “not set” so database defaults apply. The import is transactional — a failed row rolls back everything.</p>
97
+ </div>
98
+ </div>
99
+
100
+ {{#if rows.length}}
101
+ <div class="app-card overflow-hidden">
102
+ <!-- Toolbar -->
103
+ <div class="flex flex-wrap items-center justify-between gap-2 border-b border-base-200 px-4 py-3">
104
+ <span class="text-sm text-base-content/55">Showing <b class="tabular-nums">{{firstRow}}</b>–<b class="tabular-nums">{{lastRow}}</b> of <b class="tabular-nums">{{count}}</b> rows</span>
105
+ <label class="flex items-center gap-2 text-sm text-base-content/55">
106
+ Rows per page
107
+ <select id="page-size" class="field-select !w-auto !py-1.5 !text-[13px]" data-table="{{table}}" data-orderby="{{orderBy}}" data-orderdir="{{orderDir}}" data-filters="{{filterQS}}">
108
+ {{#each sizes}}<option value="{{this}}" {{#if (eq this ../pageSize)}}selected{{/if}}>{{this}}</option>{{/each}}
109
+ </select>
110
+ </label>
111
+ </div>
112
+
113
+ <!-- Bulk selection bar -->
114
+ <div id="bulk-bar" class="hidden items-center justify-between gap-2 border-b border-base-200 bg-primary/[0.04] px-4 py-2.5">
115
+ <span class="text-sm font-medium text-base-content/80">
116
+ <b id="bulk-count" class="tabular-nums text-primary">0</b> row(s) selected
117
+ </span>
118
+ <div class="flex flex-wrap items-center gap-1.5">
119
+ <button id="bulk-export-csv" type="button" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
120
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
121
+ Export CSV
122
+ </button>
123
+ <button id="bulk-export-json" type="button" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
124
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
125
+ Export JSON
126
+ </button>
127
+ <button id="bulk-delete" type="button" class="btn btn-error btn-xs gap-1" {{#if readonly}}disabled{{/if}}>
128
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
129
+ Delete
130
+ </button>
131
+ <button id="bulk-clear" type="button" class="btn btn-ghost btn-xs text-base-content/60">Clear</button>
132
+ </div>
133
+ </div>
134
+
135
+ <div class="overflow-x-auto">
136
+ <table class="table table-sm table-pin-rows">
137
+ <thead>
138
+ <tr class="bg-base-200/70 text-xs uppercase tracking-wide text-base-content/50">
139
+ {{#if hasPk}}
140
+ <th class="w-10 text-center font-semibold">
141
+ <input type="checkbox" id="select-all-rows" class="checkbox checkbox-primary checkbox-xs" title="Select all rows on this page" />
142
+ </th>
143
+ {{/if}}
144
+ {{#each colHeaders}}
145
+ <th class="whitespace-nowrap font-semibold">
146
+ <a href="{{this.href}}" class="sort-link {{#if this.active}}active{{/if}}" title="Sort by {{this.name}}">
147
+ <span class="font-mono normal-case">{{this.name}}</span>
148
+ {{#if this.pk}}<span class="chip chip-pk">PK</span>{{/if}}
149
+ <span class="sort-arrow">{{#if (eq this.dir 'desc')}}↓{{else}}↑{{/if}}</span>
150
+ </a>
151
+ </th>
152
+ {{/each}}
153
+ <th class="text-right font-semibold">Actions</th>
154
+ {{#each refColumns}}
155
+ <th class="whitespace-nowrap text-right font-semibold" title="Rows in {{this.table}} where {{this.from}} references {{this.to}}">
156
+ <span class="font-mono normal-case">{{this.table}}</span>
157
+ </th>
158
+ {{/each}}
159
+ </tr>
160
+ </thead>
161
+ <tbody>
162
+ {{#each rows}}
163
+ <tr class="group hover:bg-primary/[0.04]" data-table="{{../table}}" data-pk="{{this.pkEncoded}}">
164
+ {{#if ../hasPk}}
165
+ <td class="w-10 text-center align-middle">
166
+ {{#if this.pkEncoded}}
167
+ <input type="checkbox" class="row-checkbox checkbox checkbox-primary checkbox-xs" value="{{this.pkEncoded}}" aria-label="Select this row" />
168
+ {{/if}}
169
+ </td>
170
+ {{/if}}
171
+ {{#each this.cells}}
172
+ <td class="max-w-xs align-middle cell-editable {{#unless ../../readonly}}js-inline-cell{{/unless}}"
173
+ data-col="{{this.name}}" data-null="{{this.isNull}}" data-value="{{this.display}}">
174
+ {{#if this.isNull}}
175
+ <span class="chip chip-null js-cell-value">NULL</span>
176
+ {{else}}
177
+ <span class="js-cell-value block max-w-xs truncate font-mono text-[13px]" title="{{this.display}}">{{truncate this.display 120}}</span>
178
+ {{/if}}
179
+ </td>
180
+ {{/each}}
181
+ <td class="whitespace-nowrap text-right align-middle">
182
+ {{#unless ../readonly}}
183
+ {{#if this.pkEncoded}}
184
+ <div class="row-actions">
185
+ <a href="{{../basePath}}/tables/{{../table}}/rows/{{this.pkEncoded}}/edit" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
186
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
187
+ Edit
188
+ </a>
189
+ <button type="button" class="btn btn-ghost btn-xs gap-1 text-error js-delete-row"
190
+ data-table="{{../table}}" data-pk="{{this.pkEncoded}}">
191
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
192
+ Delete
193
+ </button>
194
+ </div>
195
+ {{else}}
196
+ <span class="text-xs text-base-content/35">No PK</span>
197
+ {{/if}}
198
+ {{/unless}}
199
+ </td>
200
+ {{#each this.refs}}
201
+ <td class="whitespace-nowrap text-right align-middle">
202
+ {{#if this.value}}
203
+ <button type="button" class="js-ref-toggle inline-flex items-center gap-1.5 rounded-lg border border-base-300 bg-base-100 px-2 py-1 text-xs font-medium text-base-content/70 shadow-soft transition hover:border-primary/50 hover:text-primary {{#unless this.count}}opacity-50{{/unless}}"
204
+ data-table="{{../../table}}" data-pk="{{../pkEncoded}}" data-ref-table="{{this.table}}"
205
+ aria-expanded="false" title="View {{this.table}} rows where {{this.from}} = {{this.value}}">
206
+ <span class="font-mono normal-case">{{this.table}}</span>
207
+ <span class="rounded-md bg-primary/10 px-1.5 py-0.5 font-semibold tabular-nums text-primary">{{this.count}}</span>
208
+ <svg xmlns="http://www.w3.org/2000/svg" class="js-ref-chevron h-3 w-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
209
+ </button>
210
+ {{else}}
211
+ <span class="text-xs text-base-content/35">&mdash;</span>
212
+ {{/if}}
213
+ </td>
214
+ {{/each}}
215
+ </tr>
216
+ {{/each}}
217
+ </tbody>
218
+ </table>
219
+ </div>
220
+
221
+ <!-- Pagination -->
222
+ {{#if (gt pages 1)}}
223
+ <div class="flex flex-wrap items-center justify-between gap-2 border-t border-base-200 px-4 py-3">
224
+ <span class="text-sm text-base-content/50">Page {{page}} of {{pages}}</span>
225
+ <div class="flex gap-1.5">
226
+ <a href="{{basePath}}/tables/{{table}}?page={{sub page 1}}&size={{pageSize}}&orderBy={{orderBy}}&orderDir={{orderDir}}{{#if filterQS}}&{{filterQS}}{{/if}}"
227
+ class="btn btn-outline btn-sm {{#unless (gt page 1)}}btn-disabled{{/unless}}" aria-disabled="{{#if (gt page 1)}}false{{else}}true{{/if}}">
228
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" /></svg>
229
+ Prev
230
+ </a>
231
+ <a href="{{basePath}}/tables/{{table}}?page={{add page 1}}&size={{pageSize}}&orderBy={{orderBy}}&orderDir={{orderDir}}{{#if filterQS}}&{{filterQS}}{{/if}}"
232
+ class="btn btn-outline btn-sm {{#unless (lt page pages)}}btn-disabled{{/unless}}" aria-disabled="{{#if (lt page pages)}}false{{else}}true{{/if}}">
233
+ Next
234
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /></svg>
235
+ </a>
236
+ </div>
237
+ </div>
238
+ {{/if}}
239
+ </div>
240
+ {{else}}
241
+ <div class="empty-state">
242
+ <span class="empty-icon">
243
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>
244
+ </span>
245
+ <p class="text-sm font-medium text-base-content/70">No rows in <span class="font-mono">{{table}}</span></p>
246
+ <div class="mt-2 flex gap-2">
247
+ {{#unless readonly}}
248
+ <a href="{{basePath}}/tables/{{table}}/rows/new" class="btn btn-primary btn-sm">Insert a row</a>
249
+ {{/unless}}
250
+ <a href="{{basePath}}/query" class="btn btn-outline btn-sm">Run a query</a>
251
+ </div>
252
+ </div>
253
+ {{/if}}
254
+ </div>
255
+ <script type="application/json" id="browse-config">{{{json browseConfig}}}</script>
256
+ <script src="{{basePath}}/js/browse.js" defer></script>