@adecore/database 0.39.0 → 0.40.1
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/console/QueryConsole.js +2 -10
- package/dist/console/SchemaPicker.js +0 -1
- package/dist/console/StatementResultView.js +1 -1
- package/dist/ddl/diff.js +1 -1
- package/dist/explorer/tree.js +0 -1
- package/dist/export-formats.d.ts +2 -0
- package/dist/export-formats.js +1 -0
- package/dist/grid/CopyAsMenu.js +0 -1
- package/dist/grid/DataGrid.js +10 -10
- package/dist/grid/column-layout.d.ts +0 -1
- package/dist/grid/column-layout.js +1 -2
- package/dist/grid/column-view.js +3 -2
- package/dist/grid/display.js +1 -2
- package/dist/host/host.js +1 -2
- package/dist/host/validate.d.ts +3 -0
- package/dist/host/validate.js +18 -15
- package/dist/same-list.d.ts +1 -0
- package/dist/same-list.js +1 -0
- package/dist/stored-size.d.ts +1 -0
- package/dist/stored-size.js +10 -0
- package/dist/table/TableToolbar.js +1 -1
- package/dist/table/TableView.js +2 -10
- package/package.json +15 -15
- package/src/DatabaseProvider.tsx +2 -0
- package/src/KeyIcon.tsx +1 -0
- package/src/client/client.ts +13 -0
- package/src/client-context.ts +2 -0
- package/src/connections/ConnectionForm.tsx +11 -0
- package/src/connections/ConnectionManager.tsx +9 -0
- package/src/connections/connection-config.ts +21 -0
- package/src/connections/docker.ts +2 -0
- package/src/connections/use-docker-containers.ts +3 -0
- package/src/console/DestructiveDialog.tsx +1 -0
- package/src/console/QueryConsole.tsx +38 -10
- package/src/console/SchemaPicker.tsx +1 -1
- package/src/console/SqlEditor.tsx +10 -0
- package/src/console/StatementResultView.tsx +7 -2
- package/src/console/TransactionControls.tsx +1 -0
- package/src/console/destructive.ts +11 -0
- package/src/console/editing.ts +10 -0
- package/src/console/history.ts +8 -0
- package/src/console/labels.ts +1 -0
- package/src/console/outcome.ts +1 -0
- package/src/console/result-keys.ts +16 -0
- package/src/console/shortcuts.ts +2 -0
- package/src/ddl/alter-mysql.ts +16 -0
- package/src/ddl/alter-sqlite.ts +21 -0
- package/src/ddl/alter.ts +2 -0
- package/src/ddl/column.ts +11 -0
- package/src/ddl/create.ts +15 -0
- package/src/ddl/dialect.ts +4 -0
- package/src/ddl/diff.ts +22 -2
- package/src/ddl/draft.ts +1 -0
- package/src/ddl/parse.ts +31 -0
- package/src/ddl/types.ts +3 -0
- package/src/ddl/validate.ts +12 -0
- package/src/designer/ColumnsEditor.tsx +3 -0
- package/src/designer/DesignerView.tsx +2 -0
- package/src/designer/ForeignKeysEditor.tsx +4 -0
- package/src/designer/IndexesEditor.tsx +1 -0
- package/src/designer/NamePicker.tsx +2 -0
- package/src/designer/OptionsEditor.tsx +4 -0
- package/src/designer/SqlPreview.tsx +2 -0
- package/src/designer/TableDesigner.tsx +14 -0
- package/src/designer/edit.ts +13 -0
- package/src/designer/plan.ts +3 -0
- package/src/designer/recover.ts +1 -0
- package/src/designer/useReferenceColumns.ts +5 -0
- package/src/explorer/DatabaseExplorer.tsx +40 -0
- package/src/explorer/RowMenu.tsx +2 -0
- package/src/explorer/TableDialog.tsx +6 -0
- package/src/explorer/expansion.ts +8 -0
- package/src/explorer/tree.ts +73 -2
- package/src/explorer/use-explorer-loads.ts +15 -0
- package/src/explorer/use-stored-expansion.ts +4 -0
- package/src/export-formats.ts +3 -0
- package/src/grid/CellEditor.tsx +1 -0
- package/src/grid/CopyAsMenu.tsx +1 -1
- package/src/grid/DataGrid.tsx +93 -23
- package/src/grid/EnumPicker.tsx +6 -0
- package/src/grid/GridHeaderCell.tsx +11 -0
- package/src/grid/GridRowView.tsx +3 -0
- package/src/grid/RecordView.tsx +19 -0
- package/src/grid/ValueDock.tsx +2 -0
- package/src/grid/aggregates.ts +12 -0
- package/src/grid/column-layout.ts +12 -4
- package/src/grid/column-selection.ts +9 -0
- package/src/grid/column-view.ts +6 -2
- package/src/grid/copy-formats.ts +13 -0
- package/src/grid/display.ts +17 -3
- package/src/grid/edit-value.ts +11 -0
- package/src/grid/enum-type.ts +15 -0
- package/src/grid/focused-value.ts +4 -0
- package/src/grid/layout.ts +12 -0
- package/src/grid/navigation.ts +5 -0
- package/src/grid/row-selection.ts +7 -0
- package/src/grid/sort.ts +1 -0
- package/src/grid/whole-cells.ts +8 -0
- package/src/host/host.ts +16 -3
- package/src/host/spawn.ts +2 -0
- package/src/host/validate.ts +37 -15
- package/src/same-list.ts +2 -0
- package/src/sql-split.ts +28 -0
- package/src/sql.ts +20 -0
- package/src/stored-size.ts +10 -0
- package/src/structure/StructureView.tsx +18 -0
- package/src/table/CommandField.tsx +11 -0
- package/src/table/ImportDialog.tsx +4 -0
- package/src/table/TableStatusBar.tsx +1 -0
- package/src/table/TableToolbar.tsx +2 -2
- package/src/table/TableView.tsx +50 -10
- package/src/table/command-field.ts +40 -0
- package/src/table/grid-rows.ts +13 -0
- package/src/table/import-mapping.ts +5 -0
- package/src/table/layout-store.ts +7 -0
- package/src/table/paging.ts +1 -0
- package/src/table/pending.ts +18 -0
- package/src/table/references.ts +4 -0
- package/src/table/useLoaded.ts +3 -0
- package/src/table/useStoredLayout.ts +2 -0
- package/src/table/useTableTransfer.ts +17 -1
- package/src/testing/fake.ts +24 -0
- package/src/use-database-resources.ts +1 -0
- package/src/use-popup-press.ts +3 -0
- package/src/value/ValueEditor.tsx +6 -0
- package/src/value/draft.ts +7 -0
- package/src/value/hex.ts +8 -0
- package/src/value/json.ts +16 -0
- package/src/value/views.ts +14 -0
- package/src/workbench/DatabaseWorkbench.tsx +22 -0
- package/src/workbench/tabs.ts +34 -0
package/src/sql-split.ts
CHANGED
|
@@ -22,6 +22,7 @@ const isWordCode = (code: number): boolean =>
|
|
|
22
22
|
|
|
23
23
|
const lineEnd = (text: string, at: number): number => {
|
|
24
24
|
const newline = text.indexOf('\n', at);
|
|
25
|
+
|
|
25
26
|
return newline === -1 ? text.length : newline + 1;
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -31,9 +32,12 @@ const startsLineComment = (text: string, at: number, engine: Engine): boolean =>
|
|
|
31
32
|
if (engine === 'sqlite') {
|
|
32
33
|
return true;
|
|
33
34
|
}
|
|
35
|
+
|
|
34
36
|
const next = text[at + 2];
|
|
37
|
+
|
|
35
38
|
return next === undefined || WHITESPACE.test(next) || next.charCodeAt(0) < 32;
|
|
36
39
|
}
|
|
40
|
+
|
|
37
41
|
return text[at] === '#' && engine === 'mysql';
|
|
38
42
|
};
|
|
39
43
|
|
|
@@ -42,6 +46,7 @@ const quotedEnd = (text: string, at: number, engine: Engine): number => {
|
|
|
42
46
|
const quote = text[at]!;
|
|
43
47
|
const backslashEscapes = engine === 'mysql' && quote !== '`';
|
|
44
48
|
let i = at + 1;
|
|
49
|
+
|
|
45
50
|
while (i < text.length) {
|
|
46
51
|
if (backslashEscapes && text[i] === '\\') {
|
|
47
52
|
i += 2;
|
|
@@ -55,6 +60,7 @@ const quotedEnd = (text: string, at: number, engine: Engine): number => {
|
|
|
55
60
|
i += 1;
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
|
|
58
64
|
return text.length;
|
|
59
65
|
};
|
|
60
66
|
|
|
@@ -62,9 +68,11 @@ const quotedEnd = (text: string, at: number, engine: Engine): number => {
|
|
|
62
68
|
export const tokenize = (text: string, engine: Engine): SqlToken[] => {
|
|
63
69
|
const tokens: SqlToken[] = [];
|
|
64
70
|
let i = 0;
|
|
71
|
+
|
|
65
72
|
while (i < text.length) {
|
|
66
73
|
const char = text[i]!;
|
|
67
74
|
const code = text.charCodeAt(i);
|
|
75
|
+
|
|
68
76
|
if (WHITESPACE.test(char)) {
|
|
69
77
|
i += 1;
|
|
70
78
|
} else if (char === "'" || char === '"' || char === '`') {
|
|
@@ -90,9 +98,11 @@ export const tokenize = (text: string, engine: Engine): SqlToken[] => {
|
|
|
90
98
|
i += 1;
|
|
91
99
|
} else if (isWordCode(code)) {
|
|
92
100
|
let end = i + 1;
|
|
101
|
+
|
|
93
102
|
while (end < text.length && isWordCode(text.charCodeAt(end))) {
|
|
94
103
|
end += 1;
|
|
95
104
|
}
|
|
105
|
+
|
|
96
106
|
tokens.push({ kind: 'word', start: i, end });
|
|
97
107
|
i = end;
|
|
98
108
|
} else {
|
|
@@ -100,6 +110,7 @@ export const tokenize = (text: string, engine: Engine): SqlToken[] => {
|
|
|
100
110
|
i += 1;
|
|
101
111
|
}
|
|
102
112
|
}
|
|
113
|
+
|
|
103
114
|
return tokens;
|
|
104
115
|
};
|
|
105
116
|
|
|
@@ -112,12 +123,15 @@ class TriggerState {
|
|
|
112
123
|
|
|
113
124
|
word(word: string): void {
|
|
114
125
|
const upper = word.toUpperCase();
|
|
126
|
+
|
|
115
127
|
if (this.leading.length < 3) {
|
|
116
128
|
this.leading.push(upper);
|
|
117
129
|
}
|
|
130
|
+
|
|
118
131
|
if (!this.isTrigger()) {
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
134
|
+
|
|
121
135
|
if (upper === 'BEGIN' && !this.inBody) {
|
|
122
136
|
this.inBody = true;
|
|
123
137
|
} else if (upper === 'CASE' && this.inBody) {
|
|
@@ -137,6 +151,7 @@ class TriggerState {
|
|
|
137
151
|
|
|
138
152
|
private isTrigger(): boolean {
|
|
139
153
|
const [first, second, third] = this.leading;
|
|
154
|
+
|
|
140
155
|
return first === 'CREATE' && (second === 'TRIGGER' || ((second === 'TEMP' || second === 'TEMPORARY') && third === 'TRIGGER'));
|
|
141
156
|
}
|
|
142
157
|
}
|
|
@@ -144,12 +159,15 @@ class TriggerState {
|
|
|
144
159
|
const trimmed = (text: string, from: number, to: number): SqlStatement => {
|
|
145
160
|
let start = from;
|
|
146
161
|
let end = to;
|
|
162
|
+
|
|
147
163
|
while (start < end && WHITESPACE.test(text[start]!)) {
|
|
148
164
|
start += 1;
|
|
149
165
|
}
|
|
166
|
+
|
|
150
167
|
while (end > start && WHITESPACE.test(text[end - 1]!)) {
|
|
151
168
|
end -= 1;
|
|
152
169
|
}
|
|
170
|
+
|
|
153
171
|
return { text: text.slice(start, end), start, end };
|
|
154
172
|
};
|
|
155
173
|
|
|
@@ -170,12 +188,14 @@ export const splitStatements = (text: string, engine: Engine): SqlStatement[] =>
|
|
|
170
188
|
if (hasContent) {
|
|
171
189
|
statements.push(trimmed(text, start, token.start));
|
|
172
190
|
}
|
|
191
|
+
|
|
173
192
|
start = token.end;
|
|
174
193
|
hasContent = false;
|
|
175
194
|
trigger = new TriggerState();
|
|
176
195
|
}
|
|
177
196
|
} else if (token.kind !== 'comment') {
|
|
178
197
|
hasContent = true;
|
|
198
|
+
|
|
179
199
|
if (token.kind === 'word' && engine === 'sqlite') {
|
|
180
200
|
trigger.word(text.slice(token.start, token.end));
|
|
181
201
|
}
|
|
@@ -185,6 +205,7 @@ export const splitStatements = (text: string, engine: Engine): SqlStatement[] =>
|
|
|
185
205
|
if (hasContent) {
|
|
186
206
|
statements.push(trimmed(text, start, text.length));
|
|
187
207
|
}
|
|
208
|
+
|
|
188
209
|
return statements;
|
|
189
210
|
};
|
|
190
211
|
|
|
@@ -195,16 +216,21 @@ export const splitStatements = (text: string, engine: Engine): SqlStatement[] =>
|
|
|
195
216
|
*/
|
|
196
217
|
export const statementAt = (text: string, offset: number, engine: Engine): SqlStatement | null => {
|
|
197
218
|
const statements = splitStatements(text, engine);
|
|
219
|
+
|
|
198
220
|
for (const [index, statement] of statements.entries()) {
|
|
199
221
|
if (offset >= statement.start && offset <= statement.end) {
|
|
200
222
|
return statement;
|
|
201
223
|
}
|
|
224
|
+
|
|
202
225
|
const next = statements[index + 1];
|
|
226
|
+
|
|
203
227
|
if (offset > statement.end && (next === undefined || offset < next.start)) {
|
|
204
228
|
const gap = text.slice(statement.end, offset);
|
|
229
|
+
|
|
205
230
|
return gap.includes('\n') && next !== undefined ? next : statement;
|
|
206
231
|
}
|
|
207
232
|
}
|
|
233
|
+
|
|
208
234
|
return statements[0] ?? null;
|
|
209
235
|
};
|
|
210
236
|
|
|
@@ -214,7 +240,9 @@ export const firstKeyword = (text: string, engine: Engine): string => {
|
|
|
214
240
|
if (token.kind === 'comment' || (token.kind === 'punct' && text[token.start] === '(')) {
|
|
215
241
|
continue;
|
|
216
242
|
}
|
|
243
|
+
|
|
217
244
|
return token.kind === 'word' ? (/^[A-Za-z]*/.exec(text.slice(token.start, token.end))?.[0] ?? '').toUpperCase() : '';
|
|
218
245
|
}
|
|
246
|
+
|
|
219
247
|
return '';
|
|
220
248
|
};
|
package/src/sql.ts
CHANGED
|
@@ -24,12 +24,14 @@ export const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
|
24
24
|
|
|
25
25
|
/* Whether a value of this kind is a number, which a grid aligns against the end of its cell. */
|
|
26
26
|
export const isNumericKind = (kind: ValueKind): boolean => NUMERIC_KINDS.has(kind);
|
|
27
|
+
|
|
27
28
|
const ORDER_ITEM = /\s*(`(?:[^`]|``)+`|"(?:[^"]|"")+"|[A-Za-z_][\w$]*)(?:\s+(asc|desc))?\s*(,|$)/iy;
|
|
28
29
|
|
|
29
30
|
const markOf = (engine: Engine): string => (engine === 'mysql' ? '`' : '"');
|
|
30
31
|
|
|
31
32
|
export const quoteIdentifier = (engine: Engine, name: string): string => {
|
|
32
33
|
const mark = markOf(engine);
|
|
34
|
+
|
|
33
35
|
return `${mark}${name.replaceAll(mark, mark + mark)}${mark}`;
|
|
34
36
|
};
|
|
35
37
|
|
|
@@ -42,6 +44,7 @@ export const qualifiedName = (target: SqlTarget): string =>
|
|
|
42
44
|
const stringLiteral = (engine: Engine, text: string): string => {
|
|
43
45
|
// MySQL reads a backslash as an escape unless the server runs with NO_BACKSLASH_ESCAPES.
|
|
44
46
|
const escaped = engine === 'mysql' ? text.replaceAll('\\', '\\\\') : text;
|
|
47
|
+
|
|
45
48
|
return `'${escaped.replaceAll("'", "''")}'`;
|
|
46
49
|
};
|
|
47
50
|
|
|
@@ -52,6 +55,7 @@ export const sqlLiteral = (engine: Engine, value: LiteralValue, kind?: ValueKind
|
|
|
52
55
|
if (value === null) {
|
|
53
56
|
return 'NULL';
|
|
54
57
|
}
|
|
58
|
+
|
|
55
59
|
switch (typeof value) {
|
|
56
60
|
case 'boolean':
|
|
57
61
|
return engine === 'mysql' ? (value ? 'TRUE' : 'FALSE') : value ? '1' : '0';
|
|
@@ -59,9 +63,11 @@ export const sqlLiteral = (engine: Engine, value: LiteralValue, kind?: ValueKind
|
|
|
59
63
|
return Number.isFinite(value) ? String(value) : 'NULL';
|
|
60
64
|
case 'string': {
|
|
61
65
|
const trimmed = value.trim();
|
|
66
|
+
|
|
62
67
|
return kind !== undefined && isNumericKind(kind) && NUMERIC_TEXT.test(trimmed) ? trimmed : stringLiteral(engine, value);
|
|
63
68
|
}
|
|
64
69
|
}
|
|
70
|
+
|
|
65
71
|
switch (value.kind) {
|
|
66
72
|
case 'default':
|
|
67
73
|
return 'DEFAULT';
|
|
@@ -96,43 +102,56 @@ export const orderByClause = (engine: Engine, sorts: readonly ColumnSort[]): str
|
|
|
96
102
|
|
|
97
103
|
const unquote = (engine: Engine, word: string): string | null => {
|
|
98
104
|
const mark = word[0]!;
|
|
105
|
+
|
|
99
106
|
if (mark !== '`' && mark !== '"') {
|
|
100
107
|
return word;
|
|
101
108
|
}
|
|
109
|
+
|
|
102
110
|
// A double quoted word is a string in MySQL, which sorts by a constant and so is not a column.
|
|
103
111
|
if (mark === '"' && engine === 'mysql') {
|
|
104
112
|
return null;
|
|
105
113
|
}
|
|
114
|
+
|
|
106
115
|
return word.slice(1, -1).replaceAll(mark + mark, mark);
|
|
107
116
|
};
|
|
108
117
|
|
|
109
118
|
const readOrderBy = (engine: Engine, text: string, resolve: (word: string) => string | undefined): ColumnSort[] | null => {
|
|
110
119
|
const source = text.trim();
|
|
120
|
+
|
|
111
121
|
if (source === '') {
|
|
112
122
|
return [];
|
|
113
123
|
}
|
|
124
|
+
|
|
114
125
|
const sorts: ColumnSort[] = [];
|
|
115
126
|
let position = 0;
|
|
127
|
+
|
|
116
128
|
while (position < source.length) {
|
|
117
129
|
ORDER_ITEM.lastIndex = position;
|
|
118
130
|
const match = ORDER_ITEM.exec(source);
|
|
131
|
+
|
|
119
132
|
if (match === null) {
|
|
120
133
|
return null;
|
|
121
134
|
}
|
|
135
|
+
|
|
122
136
|
const word = unquote(engine, match[1]!);
|
|
123
137
|
const column = word === null ? undefined : resolve(word);
|
|
138
|
+
|
|
124
139
|
if (column === undefined || sorts.some((sort) => sort.column === column)) {
|
|
125
140
|
return null;
|
|
126
141
|
}
|
|
142
|
+
|
|
127
143
|
sorts.push({ column, direction: match[2]?.toLowerCase() === 'desc' ? 'desc' : 'asc' });
|
|
128
144
|
position = ORDER_ITEM.lastIndex;
|
|
145
|
+
|
|
129
146
|
if (match[3] === '') {
|
|
130
147
|
return sorts;
|
|
131
148
|
}
|
|
149
|
+
|
|
132
150
|
if (position >= source.length) {
|
|
133
151
|
return null;
|
|
134
152
|
}
|
|
135
153
|
}
|
|
154
|
+
|
|
136
155
|
return sorts;
|
|
137
156
|
};
|
|
138
157
|
|
|
@@ -153,5 +172,6 @@ export const parseOrderByLoose = (engine: Engine, text: string): ColumnSort[] |
|
|
|
153
172
|
/* The column a word names, which may be quoted the way the engine quotes, or `undefined` when none of the columns is meant. */
|
|
154
173
|
export const findColumn = (engine: Engine, word: string, columns: readonly string[]): string | undefined => {
|
|
155
174
|
const bare = unquote(engine, word);
|
|
175
|
+
|
|
156
176
|
return bare === null ? undefined : matchColumn(columns, bare);
|
|
157
177
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* A size in pixels a view wrote to the app's storage, or `null` when the text is missing, broken or not at least `minimum`. */
|
|
2
|
+
export const parseStoredSize = (raw: string | null | undefined, minimum: number): number | null => {
|
|
3
|
+
try {
|
|
4
|
+
const value: unknown = JSON.parse(raw ?? 'null');
|
|
5
|
+
|
|
6
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= minimum ? Math.round(value) : null;
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -52,11 +52,13 @@ export function StructureView({ connection, schema, table, toolbarStart, classNa
|
|
|
52
52
|
|
|
53
53
|
useEffect(() => {
|
|
54
54
|
let current = true;
|
|
55
|
+
|
|
55
56
|
const settle = (outcome: Load): void => {
|
|
56
57
|
if (current) {
|
|
57
58
|
setAnswer({ connection, schema, table, attempt, load: outcome });
|
|
58
59
|
}
|
|
59
60
|
};
|
|
61
|
+
|
|
60
62
|
client
|
|
61
63
|
.session(connection)
|
|
62
64
|
.structure(schema, table)
|
|
@@ -64,6 +66,7 @@ export function StructureView({ connection, schema, table, toolbarStart, classNa
|
|
|
64
66
|
(structure) => settle({ status: 'ready', structure }),
|
|
65
67
|
(e: unknown) => settle({ status: 'error', message: messageOf(e, t('structure.failed')) })
|
|
66
68
|
);
|
|
69
|
+
|
|
67
70
|
return () => {
|
|
68
71
|
current = false;
|
|
69
72
|
};
|
|
@@ -92,6 +95,7 @@ export function StructureView({ connection, schema, table, toolbarStart, classNa
|
|
|
92
95
|
|
|
93
96
|
export function StructureTabs({ structure, start }: { structure: TableStructure; start?: ReactNode }) {
|
|
94
97
|
const { t } = useTranslation('database');
|
|
98
|
+
|
|
95
99
|
const head =
|
|
96
100
|
start === undefined || start === null || start === false ? undefined : (
|
|
97
101
|
<span className="flex items-center gap-4">
|
|
@@ -99,6 +103,7 @@ export function StructureTabs({ structure, start }: { structure: TableStructure;
|
|
|
99
103
|
<Separator />
|
|
100
104
|
</span>
|
|
101
105
|
);
|
|
106
|
+
|
|
102
107
|
return (
|
|
103
108
|
<Tabs.Root defaultValue="columns" className="flex min-h-0 grow flex-col">
|
|
104
109
|
<Tabs.List className="shrink-0 px-3" start={head}>
|
|
@@ -151,11 +156,13 @@ function Grid({ heads, children }: { heads: readonly string[]; children: ReactNo
|
|
|
151
156
|
|
|
152
157
|
function YesNo({ value }: { value: boolean }) {
|
|
153
158
|
const { t } = useTranslation('database');
|
|
159
|
+
|
|
154
160
|
return <>{value ? t('structure.yes') : t('structure.no')}</>;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
function ColumnsTable({ structure }: { structure: TableStructure }) {
|
|
158
164
|
const { t } = useTranslation('database');
|
|
165
|
+
|
|
159
166
|
const heads = [
|
|
160
167
|
t('structure.column.name'),
|
|
161
168
|
t('structure.column.type'),
|
|
@@ -164,10 +171,13 @@ function ColumnsTable({ structure }: { structure: TableStructure }) {
|
|
|
164
171
|
t('structure.column.extra'),
|
|
165
172
|
t('structure.column.comment')
|
|
166
173
|
];
|
|
174
|
+
|
|
167
175
|
const foreign = new Set(structure.foreignKeys.flatMap((key) => key.columns));
|
|
176
|
+
|
|
168
177
|
if (structure.columns.length === 0) {
|
|
169
178
|
return <EmptyState>{t('structure.noColumns')}</EmptyState>;
|
|
170
179
|
}
|
|
180
|
+
|
|
171
181
|
return (
|
|
172
182
|
<Grid heads={heads}>
|
|
173
183
|
{structure.columns.map((column) => (
|
|
@@ -179,6 +189,7 @@ function ColumnsTable({ structure }: { structure: TableStructure }) {
|
|
|
179
189
|
|
|
180
190
|
function ColumnRow({ column, primary, foreign }: { column: ColumnInfo; primary: boolean; foreign: boolean }) {
|
|
181
191
|
const { t } = useTranslation('database');
|
|
192
|
+
|
|
182
193
|
return (
|
|
183
194
|
<tr>
|
|
184
195
|
<td className={TD}>
|
|
@@ -211,9 +222,11 @@ function ColumnRow({ column, primary, foreign }: { column: ColumnInfo; primary:
|
|
|
211
222
|
|
|
212
223
|
function IndexesTable({ indexes }: { indexes: readonly IndexInfo[] }) {
|
|
213
224
|
const { t } = useTranslation('database');
|
|
225
|
+
|
|
214
226
|
if (indexes.length === 0) {
|
|
215
227
|
return <EmptyState>{t('structure.noIndexes')}</EmptyState>;
|
|
216
228
|
}
|
|
229
|
+
|
|
217
230
|
return (
|
|
218
231
|
<Grid heads={[t('structure.index.name'), t('structure.index.columns'), t('structure.index.kind')]}>
|
|
219
232
|
{indexes.map((index) => (
|
|
@@ -234,10 +247,13 @@ function IndexesTable({ indexes }: { indexes: readonly IndexInfo[] }) {
|
|
|
234
247
|
|
|
235
248
|
function ForeignKeysTable({ foreignKeys }: { foreignKeys: readonly ForeignKeyInfo[] }) {
|
|
236
249
|
const { t } = useTranslation('database');
|
|
250
|
+
|
|
237
251
|
if (foreignKeys.length === 0) {
|
|
238
252
|
return <EmptyState>{t('structure.noForeignKeys')}</EmptyState>;
|
|
239
253
|
}
|
|
254
|
+
|
|
240
255
|
const action = (value: string | null): string => value ?? t('structure.engineDefault');
|
|
256
|
+
|
|
241
257
|
return (
|
|
242
258
|
<Grid
|
|
243
259
|
heads={[
|
|
@@ -263,9 +279,11 @@ function ForeignKeysTable({ foreignKeys }: { foreignKeys: readonly ForeignKeyInf
|
|
|
263
279
|
|
|
264
280
|
function Ddl({ ddl }: { ddl: string | null }) {
|
|
265
281
|
const { t } = useTranslation('database');
|
|
282
|
+
|
|
266
283
|
if (ddl === null) {
|
|
267
284
|
return <EmptyState>{t('structure.noDdl')}</EmptyState>;
|
|
268
285
|
}
|
|
286
|
+
|
|
269
287
|
return (
|
|
270
288
|
<div className="relative">
|
|
271
289
|
<Button variant="secondary" size="xs" className="absolute top-2 right-3" onClick={() => copyText(ddl)}>
|
|
@@ -59,6 +59,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
|
|
|
59
59
|
const [dismissed, setDismissed] = useState(false);
|
|
60
60
|
const suggestions = useMemo(() => suggest(engine, text, columns), [engine, text, columns]);
|
|
61
61
|
const open = focused && !dismissed && suggestions.length > 0;
|
|
62
|
+
|
|
62
63
|
const optionId = (index: number): string => `${listId}-${index}`;
|
|
63
64
|
|
|
64
65
|
useImperativeHandle(inputRef, () => input.current!, []);
|
|
@@ -86,18 +87,22 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
|
|
|
86
87
|
case 'condition':
|
|
87
88
|
case 'sql':
|
|
88
89
|
commit(suggestion.chip);
|
|
90
|
+
|
|
89
91
|
break;
|
|
90
92
|
case 'filter':
|
|
91
93
|
change(filterStartText(suggestion.column));
|
|
92
94
|
input.current?.focus();
|
|
95
|
+
|
|
93
96
|
break;
|
|
94
97
|
case 'sort':
|
|
95
98
|
onChipsChange(toggleSort(chips, suggestion.column.name));
|
|
96
99
|
clear();
|
|
100
|
+
|
|
97
101
|
break;
|
|
98
102
|
case 'jump':
|
|
99
103
|
clear();
|
|
100
104
|
onJumpToColumn(suggestion.column.name);
|
|
105
|
+
|
|
101
106
|
break;
|
|
102
107
|
}
|
|
103
108
|
};
|
|
@@ -122,6 +127,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
|
|
|
122
127
|
if (event.nativeEvent.isComposing) {
|
|
123
128
|
return;
|
|
124
129
|
}
|
|
130
|
+
|
|
125
131
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
126
132
|
if (suggestions.length > 0) {
|
|
127
133
|
event.preventDefault();
|
|
@@ -131,6 +137,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
|
|
|
131
137
|
} else if (event.key === 'Enter') {
|
|
132
138
|
event.preventDefault();
|
|
133
139
|
const suggestion = suggestions[Math.min(active, suggestions.length - 1)];
|
|
140
|
+
|
|
134
141
|
if (suggestion !== undefined) {
|
|
135
142
|
pick(suggestion);
|
|
136
143
|
} else if (editing !== null) {
|
|
@@ -153,6 +160,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
|
|
|
153
160
|
|
|
154
161
|
const handleBlur = (): void => {
|
|
155
162
|
setFocused(false);
|
|
163
|
+
|
|
156
164
|
if (editing !== null) {
|
|
157
165
|
clear();
|
|
158
166
|
}
|
|
@@ -207,6 +215,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
|
|
|
207
215
|
const column = columnOf(suggestion);
|
|
208
216
|
const previous = suggestions[index - 1];
|
|
209
217
|
const startsGroup = column !== null && (previous === undefined || columnOf(previous)?.name !== column.name);
|
|
218
|
+
|
|
210
219
|
return (
|
|
211
220
|
<div key={`${suggestion.kind}:${column?.name ?? ''}`} role="presentation">
|
|
212
221
|
{startsGroup && (
|
|
@@ -240,6 +249,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
|
|
|
240
249
|
|
|
241
250
|
function SuggestionLabel({ suggestion }: { suggestion: Suggestion }) {
|
|
242
251
|
const { t } = useTranslation('database');
|
|
252
|
+
|
|
243
253
|
switch (suggestion.kind) {
|
|
244
254
|
case 'condition':
|
|
245
255
|
return (
|
|
@@ -280,6 +290,7 @@ interface ChipViewProps {
|
|
|
280
290
|
/* A filter opens its text for editing, a sort flips its direction, and a raw ORDER BY is only shown. */
|
|
281
291
|
function ChipView({ chip, onActivate, onRemove }: ChipViewProps) {
|
|
282
292
|
const { t } = useTranslation('database');
|
|
293
|
+
|
|
283
294
|
return (
|
|
284
295
|
<span className="inline-flex h-5 max-w-72 min-w-0 shrink-0 items-center rounded-md bg-text/10 font-mono text-code text-text">
|
|
285
296
|
{chip.kind === 'filter' && (
|
|
@@ -54,10 +54,13 @@ export function ImportForm({
|
|
|
54
54
|
}: ImportFormProps) {
|
|
55
55
|
const { t } = useTranslation('database');
|
|
56
56
|
const items = [{ value: SKIP, label: t('table.import.skip') }, ...tableColumns.map((column, index) => ({ value: String(index), label: column.name }))];
|
|
57
|
+
|
|
57
58
|
const valueOf = (name: string | null): string => {
|
|
58
59
|
const index = name === null ? -1 : tableColumns.findIndex((column) => column.name === name);
|
|
60
|
+
|
|
59
61
|
return index < 0 ? SKIP : String(index);
|
|
60
62
|
};
|
|
63
|
+
|
|
61
64
|
const canImport = sample !== null && hasMapping(mapping) && !busy;
|
|
62
65
|
|
|
63
66
|
return (
|
|
@@ -156,6 +159,7 @@ export interface ImportDialogProps extends ImportFormProps {
|
|
|
156
159
|
/* The import of a file into the table: pick how the columns map, look at the rows, confirm. */
|
|
157
160
|
export function ImportDialog({ open, onOpenChange, ...form }: ImportDialogProps) {
|
|
158
161
|
const { t } = useTranslation('database');
|
|
162
|
+
|
|
159
163
|
return (
|
|
160
164
|
<Dialog.Root open={open} onOpenChange={(next) => !form.busy && onOpenChange(next)}>
|
|
161
165
|
<Dialog.Popup className="flex max-h-[calc(100vh-96px)] w-[720px] max-w-[calc(100vw-48px)] flex-col p-5">
|
|
@@ -66,6 +66,7 @@ function SelectionSummary({ selection }: { selection: SelectionFigures }) {
|
|
|
66
66
|
const { columns, aggregates } = selection;
|
|
67
67
|
const { count, numeric } = aggregates;
|
|
68
68
|
const notation = useNumberNotation();
|
|
69
|
+
|
|
69
70
|
const figure = (label: string, value: string) => (
|
|
70
71
|
<span className="shrink-0">
|
|
71
72
|
<span className="text-text-faint">{label}</span> <span className="text-text">{value}</span>
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { useTranslation } from 'react-i18next';
|
|
19
19
|
import { Button, ButtonGroup, Icon, IconButton, Menu, Separator } from '@adecore/ui';
|
|
20
20
|
import { formatNumber } from '@adecore/ui/format';
|
|
21
|
+
import { EXPORT_FORMATS } from '../export-formats.ts';
|
|
21
22
|
import type { Engine, FileFormat } from '../protocol/index.ts';
|
|
22
23
|
import { CommandField } from './CommandField.tsx';
|
|
23
24
|
import type { Chip, CommandColumn } from './command-field.ts';
|
|
@@ -32,8 +33,6 @@ export interface TransferMenu {
|
|
|
32
33
|
onImport?(): void;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const EXPORT_FORMATS: readonly FileFormat[] = ['csv', 'tsv', 'json', 'sql'];
|
|
36
|
-
|
|
37
36
|
export interface TableToolbarProps {
|
|
38
37
|
/* The app's own content before the command field, such as where the table is, with a separator after it and after the field. */
|
|
39
38
|
start?: ReactNode;
|
|
@@ -110,6 +109,7 @@ export function TableToolbar({
|
|
|
110
109
|
const { t } = useTranslation('database');
|
|
111
110
|
const readOnly = readOnlyReason !== null;
|
|
112
111
|
const hasStart = start !== undefined && start !== null && start !== false;
|
|
112
|
+
|
|
113
113
|
const tooltipOf = (label: string): string => (readOnly ? t('table.unavailable', { action: label, reason: readOnlyReason }) : label);
|
|
114
114
|
|
|
115
115
|
return (
|