@adecore/database 0.17.0-beta.2 → 0.18.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 (135) hide show
  1. package/README.md +1 -1
  2. package/dist/DatabaseProvider.d.ts +4 -3
  3. package/dist/DatabaseProvider.js +15 -3
  4. package/dist/KeyIcon.d.ts +4 -0
  5. package/dist/KeyIcon.js +14 -0
  6. package/dist/actions.d.ts +12 -0
  7. package/dist/client-context.d.ts +3 -1
  8. package/dist/client-context.js +2 -0
  9. package/dist/code-text.d.ts +1 -0
  10. package/dist/code-text.js +2 -0
  11. package/dist/column-keys.d.ts +5 -0
  12. package/dist/column-keys.js +2 -0
  13. package/dist/connections/ConnectionManager.d.ts +3 -2
  14. package/dist/connections/ConnectionManager.js +27 -16
  15. package/dist/connections/connection-config.js +2 -0
  16. package/dist/console/QueryConsole.d.ts +8 -3
  17. package/dist/console/QueryConsole.js +110 -50
  18. package/dist/console/SqlEditor.d.ts +2 -12
  19. package/dist/console/SqlEditor.js +29 -18
  20. package/dist/console/StatementResultView.d.ts +5 -1
  21. package/dist/console/StatementResultView.js +18 -5
  22. package/dist/console/TransactionControls.js +3 -6
  23. package/dist/console/editor-slot.d.ts +18 -0
  24. package/dist/console/editor-slot.js +1 -0
  25. package/dist/console/result-keys.d.ts +15 -0
  26. package/dist/console/result-keys.js +79 -0
  27. package/dist/designer/ColumnsEditor.js +2 -2
  28. package/dist/designer/DesignerView.d.ts +3 -2
  29. package/dist/designer/DesignerView.js +5 -4
  30. package/dist/designer/SqlPreview.js +2 -1
  31. package/dist/designer/TableDesigner.d.ts +3 -2
  32. package/dist/designer/TableDesigner.js +2 -2
  33. package/dist/explorer/DatabaseExplorer.d.ts +4 -1
  34. package/dist/explorer/DatabaseExplorer.js +79 -23
  35. package/dist/explorer/RowMenu.js +2 -2
  36. package/dist/explorer/expansion.js +2 -2
  37. package/dist/explorer/folders.d.ts +16 -0
  38. package/dist/explorer/folders.js +1 -0
  39. package/dist/explorer/tree.d.ts +55 -4
  40. package/dist/explorer/tree.js +137 -23
  41. package/dist/grid/CellEditor.js +3 -1
  42. package/dist/grid/DataGrid.d.ts +2 -2
  43. package/dist/grid/DataGrid.js +61 -59
  44. package/dist/grid/EnumPicker.js +2 -1
  45. package/dist/grid/GridHeaderCell.d.ts +1 -2
  46. package/dist/grid/GridHeaderCell.js +12 -5
  47. package/dist/grid/GridRowView.d.ts +49 -0
  48. package/dist/grid/GridRowView.js +51 -0
  49. package/dist/grid/RecordView.d.ts +4 -2
  50. package/dist/grid/RecordView.js +71 -22
  51. package/dist/grid/ValueDock.d.ts +3 -1
  52. package/dist/grid/ValueDock.js +9 -3
  53. package/dist/grid/cell-id.d.ts +1 -0
  54. package/dist/grid/cell-id.js +2 -0
  55. package/dist/grid/layout.d.ts +3 -2
  56. package/dist/grid/layout.js +15 -6
  57. package/dist/grid/types.d.ts +8 -1
  58. package/dist/grid/types.js +4 -3
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +2 -0
  61. package/dist/locales/en.json +32 -15
  62. package/dist/locales/nl.json +32 -15
  63. package/dist/protocol/index.d.ts +1 -1
  64. package/dist/protocol/messages.d.ts +1 -1
  65. package/dist/protocol/messages.js +1 -1
  66. package/dist/protocol/schema.d.ts +17 -0
  67. package/dist/structure/StructureView.d.ts +5 -3
  68. package/dist/structure/StructureView.js +8 -6
  69. package/dist/table/CommandField.js +2 -2
  70. package/dist/table/TableStatusBar.js +2 -2
  71. package/dist/table/TableToolbar.d.ts +3 -4
  72. package/dist/table/TableToolbar.js +4 -3
  73. package/dist/table/TableView.d.ts +4 -2
  74. package/dist/table/TableView.js +63 -91
  75. package/dist/table/grid-rows.js +4 -1
  76. package/dist/table/useTableTransfer.d.ts +3 -1
  77. package/dist/table/useTableTransfer.js +19 -5
  78. package/dist/testing/fake.d.ts +3 -1
  79. package/dist/testing/fake.js +7 -1
  80. package/dist/value/ValueEditor.d.ts +9 -0
  81. package/dist/value/ValueEditor.js +102 -0
  82. package/dist/value/ValuePanel.d.ts +1 -1
  83. package/dist/value/ValuePanel.js +3 -94
  84. package/dist/workbench/DatabaseWorkbench.js +4 -7
  85. package/package.json +6 -6
  86. package/src/DatabaseProvider.tsx +24 -5
  87. package/src/KeyIcon.tsx +15 -0
  88. package/src/actions.ts +28 -2
  89. package/src/client-context.ts +5 -1
  90. package/src/code-text.ts +2 -0
  91. package/src/column-keys.ts +8 -0
  92. package/src/connections/ConnectionManager.tsx +98 -68
  93. package/src/connections/connection-config.ts +2 -0
  94. package/src/console/QueryConsole.tsx +251 -150
  95. package/src/console/SqlEditor.tsx +33 -37
  96. package/src/console/StatementResultView.tsx +61 -27
  97. package/src/console/TransactionControls.tsx +8 -11
  98. package/src/console/editor-slot.ts +27 -0
  99. package/src/console/result-keys.ts +109 -0
  100. package/src/designer/ColumnsEditor.tsx +3 -3
  101. package/src/designer/DesignerView.tsx +14 -3
  102. package/src/designer/SqlPreview.tsx +2 -1
  103. package/src/designer/TableDesigner.tsx +5 -2
  104. package/src/explorer/DatabaseExplorer.tsx +170 -26
  105. package/src/explorer/RowMenu.tsx +2 -2
  106. package/src/explorer/expansion.ts +4 -4
  107. package/src/explorer/folders.ts +25 -0
  108. package/src/explorer/tree.ts +219 -28
  109. package/src/grid/CellEditor.tsx +3 -2
  110. package/src/grid/DataGrid.tsx +114 -176
  111. package/src/grid/EnumPicker.tsx +2 -1
  112. package/src/grid/GridHeaderCell.tsx +14 -9
  113. package/src/grid/GridRowView.tsx +184 -0
  114. package/src/grid/RecordView.tsx +188 -75
  115. package/src/grid/ValueDock.tsx +12 -3
  116. package/src/grid/cell-id.ts +2 -0
  117. package/src/grid/layout.ts +16 -6
  118. package/src/grid/types.ts +15 -3
  119. package/src/index.ts +5 -1
  120. package/src/locales/en.json +32 -15
  121. package/src/locales/nl.json +32 -15
  122. package/src/protocol/index.ts +13 -1
  123. package/src/protocol/messages.ts +1 -1
  124. package/src/protocol/schema.ts +28 -0
  125. package/src/structure/StructureView.tsx +16 -6
  126. package/src/table/CommandField.tsx +2 -2
  127. package/src/table/TableStatusBar.tsx +3 -3
  128. package/src/table/TableToolbar.tsx +107 -92
  129. package/src/table/TableView.tsx +95 -132
  130. package/src/table/grid-rows.ts +5 -3
  131. package/src/table/useTableTransfer.ts +21 -6
  132. package/src/testing/fake.ts +18 -1
  133. package/src/value/ValueEditor.tsx +213 -0
  134. package/src/value/ValuePanel.tsx +4 -206
  135. package/src/workbench/DatabaseWorkbench.tsx +9 -10
@@ -0,0 +1,213 @@
1
+ import { useMemo, useState, type KeyboardEvent, type ReactNode } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Copy, Ellipsis, WrapText } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import {
6
+ Button,
7
+ ButtonGroup,
8
+ copyText,
9
+ FormError,
10
+ IconButton,
11
+ isApplePlatform,
12
+ KEY_SHORTCUTS,
13
+ matchesShortcut,
14
+ Menu,
15
+ Segmented,
16
+ TextArea,
17
+ Tooltip
18
+ } from '@adecore/ui';
19
+ import { formatBytes, formatNumber } from '@adecore/ui/format';
20
+ import { CODE_TEXT } from '../code-text.ts';
21
+ import type { GridColumn } from '../grid/types.ts';
22
+ import type { EditValue, Value } from '../protocol/index.ts';
23
+ import { editValueOf, sameDraft, sameSource, sourceOf, type Draft, type DraftProblem } from './draft.ts';
24
+ import { hexDumpLines, parseHex } from './hex.ts';
25
+ import { renderView, sizeOf, viewsOf, type Rendered, type ViewId } from './views.ts';
26
+
27
+ const WRAPPING_VIEWS: ReadonlySet<ViewId> = new Set(['text', 'formatted', 'utf8']);
28
+
29
+ const READING = `min-h-0 flex-1 overflow-auto rounded-lg border border-border bg-surface p-2 ${CODE_TEXT} text-text`;
30
+
31
+ export interface ValueEditorProps {
32
+ column: GridColumn;
33
+ value: Value;
34
+ editable: boolean;
35
+ onCommit?(value: EditValue): void;
36
+ }
37
+
38
+ /*
39
+ * The whole of one value, to read and change what a cell is too small for: Text, Formatted for JSON, or
40
+ * Hex, Text and UUID for binary, with the checks of JSON and hex before a change is taken.
41
+ */
42
+ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorProps) {
43
+ const { t } = useTranslation('database');
44
+ const { name, type, kind } = column;
45
+ const source = useMemo(() => sourceOf({ name, type, kind }, value), [name, type, kind, value]);
46
+ const [seen, setSeen] = useState(source);
47
+ const [draft, setDraft] = useState<Draft>(source.original);
48
+ const [choice, setChoice] = useState<ViewId | null>(null);
49
+ const [problem, setProblem] = useState<DraftProblem | null>(null);
50
+ const [wrap, setWrap] = useState(true);
51
+
52
+ // A new cell, or the same cell with a new value, starts over from that value.
53
+ if (!sameSource(seen, source)) {
54
+ setSeen(source);
55
+ setDraft(source.original);
56
+ setChoice(null);
57
+ setProblem(null);
58
+ }
59
+
60
+ const views = viewsOf(source, draft);
61
+ const view = choice !== null && views.includes(choice) ? choice : views[0]!;
62
+ const dirty = !sameDraft(draft, source.original);
63
+ const rendered = useMemo(() => (view === 'text' ? null : renderView(view, draft)), [view, draft]);
64
+ const hex = source.binary && draft.mode === 'value' ? parseHex(draft.text) : null;
65
+ const shownProblem: DraftProblem | null = problem ?? (hex !== null && !hex.ok ? { kind: 'hex', problem: hex.problem } : null);
66
+ const size = sizeOf(source, draft);
67
+ const placeholder = draft.mode === 'null' ? 'NULL' : draft.mode === 'default' ? 'DEFAULT' : undefined;
68
+
69
+ const change = (next: Draft): void => {
70
+ setDraft(next);
71
+ setProblem(null);
72
+ };
73
+
74
+ const apply = (): void => {
75
+ if (!editable || onCommit === undefined || !dirty) {
76
+ return;
77
+ }
78
+ const conversion = editValueOf(draft, source);
79
+ if (conversion.ok) {
80
+ onCommit(conversion.value);
81
+ } else {
82
+ setProblem(conversion.problem);
83
+ }
84
+ };
85
+
86
+ const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {
87
+ if (!event.nativeEvent.isComposing && matchesShortcut(KEY_SHORTCUTS.modEnter, event.nativeEvent, isApplePlatform())) {
88
+ event.preventDefault();
89
+ apply();
90
+ }
91
+ };
92
+
93
+ const copy = (): void => {
94
+ if (view === 'hex' && hex !== null && hex.ok) {
95
+ copyText(hexDumpLines(hex.hex).join('\n'));
96
+ } else if (rendered !== null && rendered.kind === 'text') {
97
+ copyText(rendered.text);
98
+ } else {
99
+ copyText(draft.mode === 'value' ? draft.text : draft.mode === 'null' ? 'NULL' : 'DEFAULT');
100
+ }
101
+ };
102
+
103
+ const editor = (extra?: string): ReactNode => (
104
+ <TextArea
105
+ aria-label={t(source.binary ? 'value.hexEditor' : 'value.editor', { column: column.name })}
106
+ wrap={wrap || source.binary ? 'soft' : 'off'}
107
+ spellCheck={false}
108
+ readOnly={!editable}
109
+ placeholder={placeholder}
110
+ value={draft.mode === 'value' ? draft.text : ''}
111
+ className={clsx(CODE_TEXT, extra)}
112
+ onChange={(event) => change({ text: event.target.value, mode: 'value' })}
113
+ onKeyDown={handleKeyDown}
114
+ />
115
+ );
116
+
117
+ const problemText = (found: DraftProblem): string =>
118
+ found.kind === 'json'
119
+ ? t('value.invalidJson', { reason: found.reason })
120
+ : t(found.problem === 'odd' ? 'value.invalidHexOdd' : 'value.invalidHexCharacters');
121
+
122
+ const reading = (content: Rendered, wrapped: boolean): ReactNode => {
123
+ if (content.kind === 'empty') {
124
+ return <Faint>{placeholder}</Faint>;
125
+ }
126
+ if (content.kind === 'problem') {
127
+ return <Faint>{problemText(content.problem)}</Faint>;
128
+ }
129
+ return (
130
+ <>
131
+ <pre className={clsx(READING, wrapped ? 'break-words whitespace-pre-wrap' : 'whitespace-pre')}>{content.text}</pre>
132
+ {content.truncated && (
133
+ <p className="shrink-0 text-xs text-text-faint">
134
+ {t('value.dumpTruncated', { shown: formatNumber(content.truncated.shown), total: formatNumber(content.truncated.total) })}
135
+ </p>
136
+ )}
137
+ </>
138
+ );
139
+ };
140
+
141
+ const body = (): ReactNode => {
142
+ if (view === 'text') {
143
+ return editor('min-h-0 flex-1');
144
+ }
145
+ if (view === 'hex' && editable) {
146
+ return (
147
+ <>
148
+ {draft.mode === 'value' && rendered !== null && reading(rendered, false)}
149
+ {editor(draft.mode === 'value' ? 'flex-none' : 'min-h-0 flex-1')}
150
+ </>
151
+ );
152
+ }
153
+ return rendered === null ? null : reading(rendered, wrap && WRAPPING_VIEWS.has(view));
154
+ };
155
+
156
+ return (
157
+ <>
158
+ <div className="flex h-10 shrink-0 items-center gap-2 px-2">
159
+ {views.length > 1 && (
160
+ <Segmented<ViewId>
161
+ value={view}
162
+ onValueChange={(id) => setChoice(id)}
163
+ label={t('value.views')}
164
+ options={views.map((id) => ({ id, label: t(`value.view.${id}`) }))}
165
+ />
166
+ )}
167
+ <ButtonGroup className="ml-auto">
168
+ {WRAPPING_VIEWS.has(view) && (
169
+ <IconButton icon={WrapText} size="sm" label={t('value.wrap')} aria-pressed={wrap} active={wrap} onClick={() => setWrap(!wrap)} />
170
+ )}
171
+ <IconButton icon={Copy} size="sm" label={t('value.copy')} onClick={copy} />
172
+ </ButtonGroup>
173
+ </div>
174
+ <div className="flex min-h-0 flex-1 flex-col gap-2 px-2 pb-2">{body()}</div>
175
+ {shownProblem !== null && <FormError className="shrink-0 px-3 pb-2 text-xs">{problemText(shownProblem)}</FormError>}
176
+ {editable && (
177
+ <div className="flex shrink-0 items-center gap-1.5 border-t border-border p-2">
178
+ <Menu.Root>
179
+ <IconButton icon={Ellipsis} size="sm" label={t('value.more')} render={<Menu.Trigger />} />
180
+ <Menu.Popup>
181
+ {column.nullable !== false && (
182
+ <Menu.Item disabled={draft.mode === 'null'} onClick={() => change({ text: '', mode: 'null' })}>
183
+ {t('value.setNull')}
184
+ </Menu.Item>
185
+ )}
186
+ <Menu.Item disabled={draft.mode === 'default'} onClick={() => change({ text: '', mode: 'default' })}>
187
+ {t('value.setDefault')}
188
+ </Menu.Item>
189
+ </Menu.Popup>
190
+ </Menu.Root>
191
+ <span className="ml-auto flex items-center gap-1.5">
192
+ <Button size="xs" disabled={!dirty} onClick={() => change(source.original)}>
193
+ {t('value.revert')}
194
+ </Button>
195
+ <Tooltip label={t('value.applyHint')} kbd={KEY_SHORTCUTS.modEnter}>
196
+ <Button size="xs" variant="primary" disabled={!dirty || onCommit === undefined} onClick={apply}>
197
+ {t('value.apply')}
198
+ </Button>
199
+ </Tooltip>
200
+ </span>
201
+ </div>
202
+ )}
203
+ <footer className="flex h-8 shrink-0 items-center border-t border-border px-3 text-xs text-text-muted tabular-nums">
204
+ {size !== null &&
205
+ (size.unit === 'bytes' ? formatBytes(size.count) : t('value.characters', { count: size.count, formatted: formatNumber(size.count) }))}
206
+ </footer>
207
+ </>
208
+ );
209
+ }
210
+
211
+ function Faint({ children }: { children?: ReactNode }) {
212
+ return <div className={clsx(CODE_TEXT, 'grid min-h-0 flex-1 place-items-center text-text-faint')}>{children}</div>;
213
+ }
@@ -1,28 +1,11 @@
1
- import { useMemo, useState, type KeyboardEvent, type ReactNode, type Ref } from 'react';
1
+ import type { Ref } from 'react';
2
2
  import clsx from 'clsx';
3
- import { Copy, Ellipsis, MousePointerClick, WrapText, X } from 'lucide-react';
3
+ import { MousePointerClick, X } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
- import {
6
- Button,
7
- copyText,
8
- EmptyState,
9
- FormError,
10
- IconButton,
11
- isApplePlatform,
12
- KEY_SHORTCUTS,
13
- matchesShortcut,
14
- Menu,
15
- PanelHeader,
16
- Segmented,
17
- TextArea,
18
- Tooltip
19
- } from '@adecore/ui';
20
- import { formatBytes, formatNumber } from '@adecore/ui/format';
5
+ import { EmptyState, IconButton, PanelHeader } from '@adecore/ui';
21
6
  import type { GridColumn } from '../grid/types.ts';
22
7
  import type { EditValue, Value } from '../protocol/index.ts';
23
- import { editValueOf, sameDraft, sameSource, sourceOf, type Draft, type DraftProblem } from './draft.ts';
24
- import { hexDumpLines, parseHex } from './hex.ts';
25
- import { renderView, sizeOf, viewsOf, type Rendered, type ViewId } from './views.ts';
8
+ import { ValueEditor } from './ValueEditor.tsx';
26
9
 
27
10
  export interface ValuePanelProps {
28
11
  /* The column of the focused cell; `null` when no cell has focus, which draws an empty state. */
@@ -39,10 +22,6 @@ export interface ValuePanelProps {
39
22
  ref?: Ref<HTMLDivElement>;
40
23
  }
41
24
 
42
- const WRAPPING_VIEWS: ReadonlySet<ViewId> = new Set(['text', 'formatted', 'utf8']);
43
-
44
- const READING = 'min-h-0 flex-1 overflow-auto rounded-lg border border-border bg-surface p-2 font-mono text-code text-text';
45
-
46
25
  /* The whole value of the focused cell beside the grid, to read and edit what a cell is too small for. */
47
26
  export function ValuePanel({ column, value, loading, editable, onCommit, onClose, className, ref }: ValuePanelProps) {
48
27
  const { t } = useTranslation('database');
@@ -71,184 +50,3 @@ export function ValuePanel({ column, value, loading, editable, onCommit, onClose
71
50
  </div>
72
51
  );
73
52
  }
74
-
75
- interface ValueEditorProps {
76
- column: GridColumn;
77
- value: Value;
78
- editable: boolean;
79
- onCommit?(value: EditValue): void;
80
- }
81
-
82
- function ValueEditor({ column, value, editable, onCommit }: ValueEditorProps) {
83
- const { t } = useTranslation('database');
84
- const { name, type, kind } = column;
85
- const source = useMemo(() => sourceOf({ name, type, kind }, value), [name, type, kind, value]);
86
- const [seen, setSeen] = useState(source);
87
- const [draft, setDraft] = useState<Draft>(source.original);
88
- const [choice, setChoice] = useState<ViewId | null>(null);
89
- const [problem, setProblem] = useState<DraftProblem | null>(null);
90
- const [wrap, setWrap] = useState(true);
91
-
92
- // A new cell, or the same cell with a new value, starts over from that value.
93
- if (!sameSource(seen, source)) {
94
- setSeen(source);
95
- setDraft(source.original);
96
- setChoice(null);
97
- setProblem(null);
98
- }
99
-
100
- const views = viewsOf(source, draft);
101
- const view = choice !== null && views.includes(choice) ? choice : views[0]!;
102
- const dirty = !sameDraft(draft, source.original);
103
- const rendered = useMemo(() => (view === 'text' ? null : renderView(view, draft)), [view, draft]);
104
- const hex = source.binary && draft.mode === 'value' ? parseHex(draft.text) : null;
105
- const shownProblem: DraftProblem | null = problem ?? (hex !== null && !hex.ok ? { kind: 'hex', problem: hex.problem } : null);
106
- const size = sizeOf(source, draft);
107
- const placeholder = draft.mode === 'null' ? 'NULL' : draft.mode === 'default' ? 'DEFAULT' : undefined;
108
-
109
- const change = (next: Draft): void => {
110
- setDraft(next);
111
- setProblem(null);
112
- };
113
-
114
- const apply = (): void => {
115
- if (!editable || onCommit === undefined || !dirty) {
116
- return;
117
- }
118
- const conversion = editValueOf(draft, source);
119
- if (conversion.ok) {
120
- onCommit(conversion.value);
121
- } else {
122
- setProblem(conversion.problem);
123
- }
124
- };
125
-
126
- const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {
127
- if (!event.nativeEvent.isComposing && matchesShortcut(KEY_SHORTCUTS.modEnter, event.nativeEvent, isApplePlatform())) {
128
- event.preventDefault();
129
- apply();
130
- }
131
- };
132
-
133
- const copy = (): void => {
134
- if (view === 'hex' && hex !== null && hex.ok) {
135
- copyText(hexDumpLines(hex.hex).join('\n'));
136
- } else if (rendered !== null && rendered.kind === 'text') {
137
- copyText(rendered.text);
138
- } else {
139
- copyText(draft.mode === 'value' ? draft.text : draft.mode === 'null' ? 'NULL' : 'DEFAULT');
140
- }
141
- };
142
-
143
- const editor = (extra?: string): ReactNode => (
144
- <TextArea
145
- mono
146
- aria-label={t(source.binary ? 'value.hexEditor' : 'value.editor', { column: column.name })}
147
- wrap={wrap || source.binary ? 'soft' : 'off'}
148
- spellCheck={false}
149
- readOnly={!editable}
150
- placeholder={placeholder}
151
- value={draft.mode === 'value' ? draft.text : ''}
152
- className={extra}
153
- onChange={(event) => change({ text: event.target.value, mode: 'value' })}
154
- onKeyDown={handleKeyDown}
155
- />
156
- );
157
-
158
- const problemText = (found: DraftProblem): string =>
159
- found.kind === 'json'
160
- ? t('value.invalidJson', { reason: found.reason })
161
- : t(found.problem === 'odd' ? 'value.invalidHexOdd' : 'value.invalidHexCharacters');
162
-
163
- const reading = (content: Rendered, wrapped: boolean): ReactNode => {
164
- if (content.kind === 'empty') {
165
- return <Faint>{placeholder}</Faint>;
166
- }
167
- if (content.kind === 'problem') {
168
- return <Faint>{problemText(content.problem)}</Faint>;
169
- }
170
- return (
171
- <>
172
- <pre className={clsx(READING, wrapped ? 'break-words whitespace-pre-wrap' : 'whitespace-pre')}>{content.text}</pre>
173
- {content.truncated && (
174
- <p className="shrink-0 text-xs text-text-faint">
175
- {t('value.dumpTruncated', { shown: formatNumber(content.truncated.shown), total: formatNumber(content.truncated.total) })}
176
- </p>
177
- )}
178
- </>
179
- );
180
- };
181
-
182
- const body = (): ReactNode => {
183
- if (view === 'text') {
184
- return editor('min-h-0 flex-1');
185
- }
186
- if (view === 'hex' && editable) {
187
- return (
188
- <>
189
- {draft.mode === 'value' && rendered !== null && reading(rendered, false)}
190
- {editor(draft.mode === 'value' ? 'flex-none' : 'min-h-0 flex-1')}
191
- </>
192
- );
193
- }
194
- return rendered === null ? null : reading(rendered, wrap && WRAPPING_VIEWS.has(view));
195
- };
196
-
197
- return (
198
- <>
199
- <div className="flex h-10 shrink-0 items-center gap-2 px-2">
200
- {views.length > 1 && (
201
- <Segmented<ViewId>
202
- value={view}
203
- onValueChange={(id) => setChoice(id)}
204
- label={t('value.views')}
205
- options={views.map((id) => ({ id, label: t(`value.view.${id}`) }))}
206
- />
207
- )}
208
- <div className="ml-auto flex items-center gap-1">
209
- {WRAPPING_VIEWS.has(view) && (
210
- <IconButton icon={WrapText} size="sm" label={t('value.wrap')} aria-pressed={wrap} active={wrap} onClick={() => setWrap(!wrap)} />
211
- )}
212
- <IconButton icon={Copy} size="sm" label={t('value.copy')} onClick={copy} />
213
- </div>
214
- </div>
215
- <div className="flex min-h-0 flex-1 flex-col gap-2 px-2 pb-2">{body()}</div>
216
- {shownProblem !== null && <FormError className="shrink-0 px-3 pb-2 text-xs">{problemText(shownProblem)}</FormError>}
217
- {editable && (
218
- <div className="flex shrink-0 items-center gap-1.5 border-t border-border p-2">
219
- <Menu.Root>
220
- <IconButton icon={Ellipsis} size="sm" label={t('value.more')} render={<Menu.Trigger />} />
221
- <Menu.Popup>
222
- {column.nullable !== false && (
223
- <Menu.Item disabled={draft.mode === 'null'} onClick={() => change({ text: '', mode: 'null' })}>
224
- {t('value.setNull')}
225
- </Menu.Item>
226
- )}
227
- <Menu.Item disabled={draft.mode === 'default'} onClick={() => change({ text: '', mode: 'default' })}>
228
- {t('value.setDefault')}
229
- </Menu.Item>
230
- </Menu.Popup>
231
- </Menu.Root>
232
- <span className="ml-auto flex items-center gap-1.5">
233
- <Button size="xs" disabled={!dirty} onClick={() => change(source.original)}>
234
- {t('value.revert')}
235
- </Button>
236
- <Tooltip label={t('value.applyHint')} kbd={KEY_SHORTCUTS.modEnter}>
237
- <Button size="xs" variant="primary" disabled={!dirty || onCommit === undefined} onClick={apply}>
238
- {t('value.apply')}
239
- </Button>
240
- </Tooltip>
241
- </span>
242
- </div>
243
- )}
244
- <footer className="flex h-8 shrink-0 items-center border-t border-border px-3 text-xs text-text-muted tabular-nums">
245
- {size !== null &&
246
- (size.unit === 'bytes' ? formatBytes(size.count) : t('value.characters', { count: size.count, formatted: formatNumber(size.count) }))}
247
- </footer>
248
- </>
249
- );
250
- }
251
-
252
- function Faint({ children }: { children?: ReactNode }) {
253
- return <div className="grid min-h-0 flex-1 place-items-center font-mono text-code text-text-faint">{children}</div>;
254
- }
@@ -4,6 +4,7 @@ import { PencilRuler, Settings2, SquareTerminal, Table, TableProperties } from '
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import {
6
6
  Button,
7
+ CloseButton,
7
8
  ColumnResizeHandle,
8
9
  Dialog,
9
10
  EmptyState,
@@ -20,7 +21,7 @@ import {
20
21
  import { formatNumber } from '@adecore/ui/format';
21
22
  import { DatabaseProvider } from '../DatabaseProvider.tsx';
22
23
  import type { DatabaseAction, ExplorerSelection } from '../actions.ts';
23
- import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage, useNumberNotation } from '../client-context.ts';
24
+ import { useDatabaseAction, useDatabaseStorage } from '../client-context.ts';
24
25
  import type { Connection } from '../client/types.ts';
25
26
  import { ConnectionManager } from '../connections/ConnectionManager.tsx';
26
27
  import { QueryConsole } from '../console/QueryConsole.tsx';
@@ -63,11 +64,8 @@ const MIN_WIDTH = 200;
63
64
  */
64
65
  export function DatabaseWorkbench({ connections, onConnectionsChange, className, ref }: DatabaseWorkbenchProps) {
65
66
  const { t } = useTranslation('database');
66
- const client = useDatabaseClient();
67
67
  const outer = useDatabaseAction();
68
68
  const storage = useDatabaseStorage();
69
- const files = useDatabaseFiles();
70
- const numberNotation = useNumberNotation();
71
69
  const column = useRef<HTMLElement>(null);
72
70
  const [state, setState] = useState(() => parseState(storage?.get(STORAGE_KEY) ?? null));
73
71
  const [selection, setSelection] = useState<ExplorerSelection | null>(null);
@@ -225,7 +223,7 @@ export function DatabaseWorkbench({ connections, onConnectionsChange, className,
225
223
  const closingTable = closingTab?.kind === 'table' ? closingTab.ref.table : '';
226
224
 
227
225
  return (
228
- <DatabaseProvider client={client} onAction={onAction} storage={storage} files={files} numberNotation={numberNotation}>
226
+ <DatabaseProvider onAction={onAction}>
229
227
  <div ref={ref} className={clsx('flex h-full min-h-0 min-w-0 bg-surface text-text', className)} onKeyDown={onKeyDown}>
230
228
  <aside ref={column} className="relative flex shrink-0 flex-col border-r border-border" style={{ width }}>
231
229
  <header className="flex h-9 shrink-0 items-center justify-between border-b border-border pr-2 pl-3">
@@ -325,9 +323,10 @@ export function DatabaseWorkbench({ connections, onConnectionsChange, className,
325
323
  />
326
324
  {onConnectionsChange !== undefined && (
327
325
  <Dialog.Root open={managing} onOpenChange={setManaging}>
328
- <Dialog.Popup className="flex h-[560px] w-[900px] flex-col overflow-hidden">
329
- <div className="border-b border-border px-4 py-3">
330
- <Dialog.Title>{t('workbench.connections')}</Dialog.Title>
326
+ <Dialog.Popup className="flex h-[640px] w-[800px] flex-col overflow-hidden">
327
+ <div className="flex items-center gap-4 border-b border-border px-5 py-4">
328
+ <Dialog.Title className="grow">{t('workbench.connections')}</Dialog.Title>
329
+ <CloseButton label={t('workbench.close')} dialog />
331
330
  </div>
332
331
  <ConnectionManager
333
332
  value={connections}
@@ -336,9 +335,9 @@ export function DatabaseWorkbench({ connections, onConnectionsChange, className,
336
335
  onSelectedChange={setManaged}
337
336
  className="min-h-0 flex-1"
338
337
  />
339
- <Dialog.Footer>
338
+ <div className="flex shrink-0 justify-end border-t border-border px-5 py-3">
340
339
  <Dialog.Close render={<Button variant="secondary" />}>{t('workbench.close')}</Dialog.Close>
341
- </Dialog.Footer>
340
+ </div>
342
341
  </Dialog.Popup>
343
342
  </Dialog.Root>
344
343
  )}