@better-forge/ui 0.1.0 → 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +13 -13
  3. package/icons.json +170 -170
  4. package/package.json +71 -71
  5. package/src/api/client.ts +139 -139
  6. package/src/components/ConfirmIdentityModal.tsx +82 -82
  7. package/src/components/ConfirmModal.tsx +83 -83
  8. package/src/components/CopyButton.tsx +71 -71
  9. package/src/components/CredentialSection.tsx +270 -270
  10. package/src/components/ErrorBoundary.tsx +47 -47
  11. package/src/components/ModalShell.tsx +160 -160
  12. package/src/components/ResetTwoFactorModal.tsx +74 -74
  13. package/src/components/SidePanel.tsx +96 -96
  14. package/src/components/SingleSignOnModal.tsx +161 -161
  15. package/src/components/SsoConfirmation.tsx +96 -96
  16. package/src/components/VaultTypePicker.tsx +131 -131
  17. package/src/components/WhatsNew.tsx +84 -84
  18. package/src/components/formParts.tsx +59 -59
  19. package/src/components/ui/Alert.tsx +45 -45
  20. package/src/components/ui/DataTable.tsx +225 -225
  21. package/src/components/ui/Field.tsx +63 -63
  22. package/src/components/ui/PageHeader.tsx +36 -36
  23. package/src/components/ui/Pagination.tsx +37 -37
  24. package/src/components/ui/QuickStartPanel.tsx +57 -57
  25. package/src/components/ui/StateViews.tsx +135 -135
  26. package/src/components/ui/Switch.tsx +65 -65
  27. package/src/components/ui/Toast.tsx +104 -104
  28. package/src/components/ui/index.ts +9 -9
  29. package/src/config.ts +46 -46
  30. package/src/hooks/useDarkMode.ts +38 -38
  31. package/src/hooks/useDebouncedValue.ts +22 -22
  32. package/src/index.ts +42 -42
  33. package/src/lib/changelog.ts +93 -93
  34. package/src/lib/dateFormat.ts +90 -90
  35. package/src/navigation.ts +6 -6
  36. package/src/pages/ApiTokens.tsx +361 -361
  37. package/src/pages/AuditLog.tsx +388 -388
  38. package/src/pages/Backups.tsx +170 -170
  39. package/src/pages/ChangePassword.tsx +84 -84
  40. package/src/pages/Login.tsx +275 -275
  41. package/src/pages/Settings.tsx +264 -264
  42. package/src/pages/Setup.tsx +438 -438
  43. package/src/pages/SsoConfirm.tsx +26 -26
  44. package/src/pages/SsoTestResult.tsx +104 -104
  45. package/src/pages/Users.tsx +426 -426
  46. package/src/pages/Vault.tsx +1036 -1036
  47. package/src/shell/AdminShell.tsx +256 -256
  48. package/src/shell/guards.tsx +88 -88
  49. package/styles/base.css +521 -521
  50. package/styles/icons.css +4 -4
  51. package/tailwind-preset.js +79 -79
@@ -1,270 +1,270 @@
1
- import type { ReactNode } from 'react'
2
- import type { VaultRef } from '@better-forge/contracts'
3
- import { Field, Note, type VaultPickerProps } from './formParts'
4
-
5
- interface CredentialSectionProps extends VaultPickerProps {
6
- vault: VaultRef | undefined
7
- onVaultChange: (v: VaultRef | undefined) => void
8
- mappingFields: { key: string; label: string }[]
9
- /** Optional note shown at the top of the vault/direct section */
10
- note?: string
11
- /** What a `value` secret is used as; defaults to the password / client secret. */
12
- valueLabel?: string
13
- /** Replaces the note shown for a User / Password secret when its username and password are not a username and password. */
14
- userpassNote?: string
15
- /** Restricts the secret dropdown to these types; every type is offered when omitted. */
16
- secretTypes?: string[]
17
- /** Direct-input fields — rendered only when not using vault */
18
- children: ReactNode
19
- }
20
-
21
- /**
22
- * Wraps a set of credential fields with a Direct / Vault source toggle.
23
- * In direct mode: shows a warning banner + the credential inputs (children).
24
- * In vault mode: shows vault → secret dropdowns + JSON field mapping if needed.
25
- */
26
- export function CredentialSection({
27
- vault, onVaultChange,
28
- vaults, secretsByVault, onLoadSecrets,
29
- mappingFields, note, valueLabel = 'password / client secret', userpassNote, secretTypes, children,
30
- }: CredentialSectionProps) {
31
- const isVault = !!vault
32
-
33
- const selectedVaultId = vault?.vaultId
34
- const selectedSecretId = vault?.secretId
35
- const fieldMapping = vault?.fieldMapping ?? {}
36
-
37
- const secrets = selectedVaultId ? (secretsByVault[selectedVaultId]?.filter((s) => !secretTypes || secretTypes.includes(s.type)) ?? null) : null
38
- const selectedSecret = secrets?.find((s) => s.id === selectedSecretId)
39
-
40
- return (
41
- <div className="space-y-3">
42
- {note && (
43
- <p className="text-xs" style={{ color: 'var(--m3-secondary)' }}>{note}</p>
44
- )}
45
-
46
- {/* Source toggle */}
47
- <div className="flex rounded-lg overflow-hidden" style={{ border: '1px solid var(--m3-outline-variant)', width: 'fit-content' }}>
48
- {(['direct', 'vault'] as const).map((src) => {
49
- const active = src === 'vault' ? isVault : !isVault
50
- return (
51
- <button
52
- key={src}
53
- type="button"
54
- onClick={() => {
55
- if (src === 'direct') {
56
- onVaultChange(undefined)
57
- } else {
58
- const first = vaults[0]
59
- if (first) onLoadSecrets(first.id)
60
- onVaultChange({ vaultId: first?.id ?? 0, secretId: 0, fieldMapping: {} })
61
- }
62
- }}
63
- className={`px-4 py-1.5 text-xs font-medium transition-all ${active ? 'selection-active' : ''}`}
64
- style={{
65
- background: active ? 'var(--m3-primary-fixed)' : 'transparent',
66
- color: active ? 'var(--m3-primary)' : 'var(--m3-secondary)',
67
- }}
68
- >
69
- {src === 'direct' ? 'Direct input' : 'From Vault'}
70
- </button>
71
- )
72
- })}
73
- </div>
74
-
75
- {/* Direct input */}
76
- {!isVault && (
77
- <>
78
- <Note tone="warning">For security, store credentials in Vault rather than entering them directly here.</Note>
79
- {children}
80
- </>
81
- )}
82
-
83
- {/* Vault picker */}
84
- {isVault && (
85
- <div className="space-y-3">
86
- <div className="grid grid-cols-2 gap-3">
87
- <Field label="Vault">
88
- <select
89
- className="input-sig"
90
- value={selectedVaultId || ''}
91
- onChange={(e) => {
92
- const id = Number(e.target.value)
93
- onLoadSecrets(id)
94
- onVaultChange({ vaultId: id, secretId: 0, fieldMapping: {} })
95
- }}
96
- >
97
- <option value="">— select vault —</option>
98
- {vaults.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
99
- </select>
100
- </Field>
101
- <Field label="Secret">
102
- <select
103
- className="input-sig"
104
- value={selectedSecretId || ''}
105
- disabled={!selectedVaultId}
106
- onChange={(e) => {
107
- onVaultChange({ vaultId: selectedVaultId!, secretId: Number(e.target.value), fieldMapping: {} })
108
- }}
109
- >
110
- <option value="">— select secret —</option>
111
- {(secrets ?? []).map((s) => (
112
- <option key={s.id} value={s.id}>{s.name} ({s.type})</option>
113
- ))}
114
- </select>
115
- </Field>
116
- </div>
117
-
118
- {/* userpass — auto-mapped, no input needed */}
119
- {selectedSecret?.type === 'userpass' && (
120
- <p className="text-xs rounded-lg px-3 py-2" style={{ background: 'var(--m3-surface-container)', color: 'var(--m3-secondary)' }}>
121
- {userpassNote ?? <>Auto-mapped: <strong>username</strong> → username field, <strong>password</strong> → password field.</>}
122
- </p>
123
- )}
124
-
125
- {/* value — single value used as password / client secret */}
126
- {selectedSecret?.type === 'value' && (
127
- <p className="text-xs rounded-lg px-3 py-2" style={{ background: 'var(--m3-surface-container)', color: 'var(--m3-secondary)' }}>
128
- The secret value will be used as the <strong>{valueLabel}</strong>.
129
- </p>
130
- )}
131
-
132
- {/* json — field mapping UI */}
133
- {selectedSecret?.type === 'json' && selectedSecretId && (
134
- <div>
135
- <p className="font-mono text-xs uppercase tracking-wider mb-1" style={{ color: 'var(--m3-secondary)' }}>
136
- JSON Field Mapping
137
- </p>
138
- <p className="text-xs mb-3" style={{ color: 'var(--m3-secondary)' }}>
139
- For each credential, enter the key name from the JSON secret.
140
- </p>
141
- <div className="space-y-2">
142
- {mappingFields.map(({ key, label }) => (
143
- <div key={key} className="grid items-center gap-3" style={{ gridTemplateColumns: '110px 1fr' }}>
144
- <span className="text-xs font-medium truncate" style={{ color: 'var(--m3-on-surface-variant)' }}>{label}</span>
145
- <input
146
- className="input-sig text-xs"
147
- placeholder={`JSON key (e.g. "${key}")`}
148
- value={fieldMapping[key] ?? ''}
149
- onChange={(e) => {
150
- onVaultChange({
151
- vaultId: selectedVaultId!,
152
- secretId: selectedSecretId,
153
- fieldMapping: { ...fieldMapping, [key]: e.target.value },
154
- })
155
- }}
156
- />
157
- </div>
158
- ))}
159
- </div>
160
- </div>
161
- )}
162
-
163
- {vaults.length === 0 && (
164
- <p className="text-xs" style={{ color: 'var(--m3-secondary)' }}>
165
- No vaults found. Create one in the Vault section first.
166
- </p>
167
- )}
168
- </div>
169
- )}
170
- </div>
171
- )
172
- }
173
-
174
- interface ConnectionStringSectionProps extends VaultPickerProps {
175
- vault: VaultRef | undefined
176
- onVaultChange: (v: VaultRef | undefined) => void
177
- }
178
-
179
- /**
180
- * Vault-only picker for SQL database connection strings.
181
- * Direct input is intentionally not available — a connection string contains credentials.
182
- */
183
- export function ConnectionStringSection({
184
- vault, onVaultChange, vaults, secretsByVault, onLoadSecrets,
185
- }: ConnectionStringSectionProps) {
186
- const selectedVaultId = vault?.vaultId
187
- const selectedSecretId = vault?.secretId
188
- const fieldMapping = vault?.fieldMapping ?? {}
189
-
190
- const secrets = selectedVaultId ? (secretsByVault[selectedVaultId] ?? null) : null
191
- const selectedSecret = secrets?.find((s) => s.id === selectedSecretId)
192
-
193
- return (
194
- <div className="space-y-3">
195
- <Note tone="info">Connection strings contain credentials and must always be stored in Vault.</Note>
196
-
197
- <div className="grid grid-cols-2 gap-3">
198
- <Field label="Vault">
199
- <select
200
- className="input-sig"
201
- value={selectedVaultId || ''}
202
- onChange={(e) => {
203
- const id = Number(e.target.value)
204
- onLoadSecrets(id)
205
- onVaultChange({ vaultId: id, secretId: 0, fieldMapping: {} })
206
- }}
207
- >
208
- <option value="">— select vault —</option>
209
- {vaults.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
210
- </select>
211
- </Field>
212
- <Field label="Secret">
213
- <select
214
- className="input-sig"
215
- value={selectedSecretId || ''}
216
- disabled={!selectedVaultId}
217
- onChange={(e) => {
218
- onVaultChange({ vaultId: selectedVaultId!, secretId: Number(e.target.value), fieldMapping: {} })
219
- }}
220
- >
221
- <option value="">— select secret —</option>
222
- {(secrets ?? []).map((s) => (
223
- <option key={s.id} value={s.id}>{s.name} ({s.type})</option>
224
- ))}
225
- </select>
226
- </Field>
227
- </div>
228
-
229
- {selectedSecret?.type === 'value' && (
230
- <p className="text-xs rounded-lg px-3 py-2" style={{ background: 'var(--m3-surface-container)', color: 'var(--m3-secondary)' }}>
231
- The secret value will be used as the full connection string.
232
- </p>
233
- )}
234
-
235
- {selectedSecret?.type === 'userpass' && (
236
- <Note tone="warning">
237
- A <strong>userpass</strong> secret cannot be used as a connection string. Use a <strong>value</strong> or <strong>json</strong> secret instead.
238
- </Note>
239
- )}
240
-
241
- {selectedSecret?.type === 'json' && selectedSecretId && (
242
- <div>
243
- <p className="font-mono text-xs uppercase tracking-wider mb-1" style={{ color: 'var(--m3-secondary)' }}>JSON Field Mapping</p>
244
- <p className="text-xs mb-3" style={{ color: 'var(--m3-secondary)' }}>Enter the JSON key that holds the connection string.</p>
245
- <div className="grid items-center gap-3" style={{ gridTemplateColumns: '130px 1fr' }}>
246
- <span className="text-xs font-medium" style={{ color: 'var(--m3-on-surface-variant)' }}>Connection String</span>
247
- <input
248
- className="input-sig text-xs"
249
- placeholder='JSON key (e.g. "connectionString")'
250
- value={fieldMapping['connectionString'] ?? ''}
251
- onChange={(e) => {
252
- onVaultChange({
253
- vaultId: selectedVaultId!,
254
- secretId: selectedSecretId,
255
- fieldMapping: { connectionString: e.target.value },
256
- })
257
- }}
258
- />
259
- </div>
260
- </div>
261
- )}
262
-
263
- {vaults.length === 0 && (
264
- <p className="text-xs" style={{ color: 'var(--m3-secondary)' }}>
265
- No vaults found. Create one in the Vault section first.
266
- </p>
267
- )}
268
- </div>
269
- )
270
- }
1
+ import type { ReactNode } from 'react'
2
+ import type { VaultRef } from '@better-forge/contracts'
3
+ import { Field, Note, type VaultPickerProps } from './formParts'
4
+
5
+ interface CredentialSectionProps extends VaultPickerProps {
6
+ vault: VaultRef | undefined
7
+ onVaultChange: (v: VaultRef | undefined) => void
8
+ mappingFields: { key: string; label: string }[]
9
+ /** Optional note shown at the top of the vault/direct section */
10
+ note?: string
11
+ /** What a `value` secret is used as; defaults to the password / client secret. */
12
+ valueLabel?: string
13
+ /** Replaces the note shown for a User / Password secret when its username and password are not a username and password. */
14
+ userpassNote?: string
15
+ /** Restricts the secret dropdown to these types; every type is offered when omitted. */
16
+ secretTypes?: string[]
17
+ /** Direct-input fields — rendered only when not using vault */
18
+ children: ReactNode
19
+ }
20
+
21
+ /**
22
+ * Wraps a set of credential fields with a Direct / Vault source toggle.
23
+ * In direct mode: shows a warning banner + the credential inputs (children).
24
+ * In vault mode: shows vault → secret dropdowns + JSON field mapping if needed.
25
+ */
26
+ export function CredentialSection({
27
+ vault, onVaultChange,
28
+ vaults, secretsByVault, onLoadSecrets,
29
+ mappingFields, note, valueLabel = 'password / client secret', userpassNote, secretTypes, children,
30
+ }: CredentialSectionProps) {
31
+ const isVault = !!vault
32
+
33
+ const selectedVaultId = vault?.vaultId
34
+ const selectedSecretId = vault?.secretId
35
+ const fieldMapping = vault?.fieldMapping ?? {}
36
+
37
+ const secrets = selectedVaultId ? (secretsByVault[selectedVaultId]?.filter((s) => !secretTypes || secretTypes.includes(s.type)) ?? null) : null
38
+ const selectedSecret = secrets?.find((s) => s.id === selectedSecretId)
39
+
40
+ return (
41
+ <div className="space-y-3">
42
+ {note && (
43
+ <p className="text-xs" style={{ color: 'var(--m3-secondary)' }}>{note}</p>
44
+ )}
45
+
46
+ {/* Source toggle */}
47
+ <div className="flex rounded-lg overflow-hidden" style={{ border: '1px solid var(--m3-outline-variant)', width: 'fit-content' }}>
48
+ {(['direct', 'vault'] as const).map((src) => {
49
+ const active = src === 'vault' ? isVault : !isVault
50
+ return (
51
+ <button
52
+ key={src}
53
+ type="button"
54
+ onClick={() => {
55
+ if (src === 'direct') {
56
+ onVaultChange(undefined)
57
+ } else {
58
+ const first = vaults[0]
59
+ if (first) onLoadSecrets(first.id)
60
+ onVaultChange({ vaultId: first?.id ?? 0, secretId: 0, fieldMapping: {} })
61
+ }
62
+ }}
63
+ className={`px-4 py-1.5 text-xs font-medium transition-all ${active ? 'selection-active' : ''}`}
64
+ style={{
65
+ background: active ? 'var(--m3-primary-fixed)' : 'transparent',
66
+ color: active ? 'var(--m3-primary)' : 'var(--m3-secondary)',
67
+ }}
68
+ >
69
+ {src === 'direct' ? 'Direct input' : 'From Vault'}
70
+ </button>
71
+ )
72
+ })}
73
+ </div>
74
+
75
+ {/* Direct input */}
76
+ {!isVault && (
77
+ <>
78
+ <Note tone="warning">For security, store credentials in Vault rather than entering them directly here.</Note>
79
+ {children}
80
+ </>
81
+ )}
82
+
83
+ {/* Vault picker */}
84
+ {isVault && (
85
+ <div className="space-y-3">
86
+ <div className="grid grid-cols-2 gap-3">
87
+ <Field label="Vault">
88
+ <select
89
+ className="input-sig"
90
+ value={selectedVaultId || ''}
91
+ onChange={(e) => {
92
+ const id = Number(e.target.value)
93
+ onLoadSecrets(id)
94
+ onVaultChange({ vaultId: id, secretId: 0, fieldMapping: {} })
95
+ }}
96
+ >
97
+ <option value="">— select vault —</option>
98
+ {vaults.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
99
+ </select>
100
+ </Field>
101
+ <Field label="Secret">
102
+ <select
103
+ className="input-sig"
104
+ value={selectedSecretId || ''}
105
+ disabled={!selectedVaultId}
106
+ onChange={(e) => {
107
+ onVaultChange({ vaultId: selectedVaultId!, secretId: Number(e.target.value), fieldMapping: {} })
108
+ }}
109
+ >
110
+ <option value="">— select secret —</option>
111
+ {(secrets ?? []).map((s) => (
112
+ <option key={s.id} value={s.id}>{s.name} ({s.type})</option>
113
+ ))}
114
+ </select>
115
+ </Field>
116
+ </div>
117
+
118
+ {/* userpass — auto-mapped, no input needed */}
119
+ {selectedSecret?.type === 'userpass' && (
120
+ <p className="text-xs rounded-lg px-3 py-2" style={{ background: 'var(--m3-surface-container)', color: 'var(--m3-secondary)' }}>
121
+ {userpassNote ?? <>Auto-mapped: <strong>username</strong> → username field, <strong>password</strong> → password field.</>}
122
+ </p>
123
+ )}
124
+
125
+ {/* value — single value used as password / client secret */}
126
+ {selectedSecret?.type === 'value' && (
127
+ <p className="text-xs rounded-lg px-3 py-2" style={{ background: 'var(--m3-surface-container)', color: 'var(--m3-secondary)' }}>
128
+ The secret value will be used as the <strong>{valueLabel}</strong>.
129
+ </p>
130
+ )}
131
+
132
+ {/* json — field mapping UI */}
133
+ {selectedSecret?.type === 'json' && selectedSecretId && (
134
+ <div>
135
+ <p className="font-mono text-xs uppercase tracking-wider mb-1" style={{ color: 'var(--m3-secondary)' }}>
136
+ JSON Field Mapping
137
+ </p>
138
+ <p className="text-xs mb-3" style={{ color: 'var(--m3-secondary)' }}>
139
+ For each credential, enter the key name from the JSON secret.
140
+ </p>
141
+ <div className="space-y-2">
142
+ {mappingFields.map(({ key, label }) => (
143
+ <div key={key} className="grid items-center gap-3" style={{ gridTemplateColumns: '110px 1fr' }}>
144
+ <span className="text-xs font-medium truncate" style={{ color: 'var(--m3-on-surface-variant)' }}>{label}</span>
145
+ <input
146
+ className="input-sig text-xs"
147
+ placeholder={`JSON key (e.g. "${key}")`}
148
+ value={fieldMapping[key] ?? ''}
149
+ onChange={(e) => {
150
+ onVaultChange({
151
+ vaultId: selectedVaultId!,
152
+ secretId: selectedSecretId,
153
+ fieldMapping: { ...fieldMapping, [key]: e.target.value },
154
+ })
155
+ }}
156
+ />
157
+ </div>
158
+ ))}
159
+ </div>
160
+ </div>
161
+ )}
162
+
163
+ {vaults.length === 0 && (
164
+ <p className="text-xs" style={{ color: 'var(--m3-secondary)' }}>
165
+ No vaults found. Create one in the Vault section first.
166
+ </p>
167
+ )}
168
+ </div>
169
+ )}
170
+ </div>
171
+ )
172
+ }
173
+
174
+ interface ConnectionStringSectionProps extends VaultPickerProps {
175
+ vault: VaultRef | undefined
176
+ onVaultChange: (v: VaultRef | undefined) => void
177
+ }
178
+
179
+ /**
180
+ * Vault-only picker for SQL database connection strings.
181
+ * Direct input is intentionally not available — a connection string contains credentials.
182
+ */
183
+ export function ConnectionStringSection({
184
+ vault, onVaultChange, vaults, secretsByVault, onLoadSecrets,
185
+ }: ConnectionStringSectionProps) {
186
+ const selectedVaultId = vault?.vaultId
187
+ const selectedSecretId = vault?.secretId
188
+ const fieldMapping = vault?.fieldMapping ?? {}
189
+
190
+ const secrets = selectedVaultId ? (secretsByVault[selectedVaultId] ?? null) : null
191
+ const selectedSecret = secrets?.find((s) => s.id === selectedSecretId)
192
+
193
+ return (
194
+ <div className="space-y-3">
195
+ <Note tone="info">Connection strings contain credentials and must always be stored in Vault.</Note>
196
+
197
+ <div className="grid grid-cols-2 gap-3">
198
+ <Field label="Vault">
199
+ <select
200
+ className="input-sig"
201
+ value={selectedVaultId || ''}
202
+ onChange={(e) => {
203
+ const id = Number(e.target.value)
204
+ onLoadSecrets(id)
205
+ onVaultChange({ vaultId: id, secretId: 0, fieldMapping: {} })
206
+ }}
207
+ >
208
+ <option value="">— select vault —</option>
209
+ {vaults.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
210
+ </select>
211
+ </Field>
212
+ <Field label="Secret">
213
+ <select
214
+ className="input-sig"
215
+ value={selectedSecretId || ''}
216
+ disabled={!selectedVaultId}
217
+ onChange={(e) => {
218
+ onVaultChange({ vaultId: selectedVaultId!, secretId: Number(e.target.value), fieldMapping: {} })
219
+ }}
220
+ >
221
+ <option value="">— select secret —</option>
222
+ {(secrets ?? []).map((s) => (
223
+ <option key={s.id} value={s.id}>{s.name} ({s.type})</option>
224
+ ))}
225
+ </select>
226
+ </Field>
227
+ </div>
228
+
229
+ {selectedSecret?.type === 'value' && (
230
+ <p className="text-xs rounded-lg px-3 py-2" style={{ background: 'var(--m3-surface-container)', color: 'var(--m3-secondary)' }}>
231
+ The secret value will be used as the full connection string.
232
+ </p>
233
+ )}
234
+
235
+ {selectedSecret?.type === 'userpass' && (
236
+ <Note tone="warning">
237
+ A <strong>userpass</strong> secret cannot be used as a connection string. Use a <strong>value</strong> or <strong>json</strong> secret instead.
238
+ </Note>
239
+ )}
240
+
241
+ {selectedSecret?.type === 'json' && selectedSecretId && (
242
+ <div>
243
+ <p className="font-mono text-xs uppercase tracking-wider mb-1" style={{ color: 'var(--m3-secondary)' }}>JSON Field Mapping</p>
244
+ <p className="text-xs mb-3" style={{ color: 'var(--m3-secondary)' }}>Enter the JSON key that holds the connection string.</p>
245
+ <div className="grid items-center gap-3" style={{ gridTemplateColumns: '130px 1fr' }}>
246
+ <span className="text-xs font-medium" style={{ color: 'var(--m3-on-surface-variant)' }}>Connection String</span>
247
+ <input
248
+ className="input-sig text-xs"
249
+ placeholder='JSON key (e.g. "connectionString")'
250
+ value={fieldMapping['connectionString'] ?? ''}
251
+ onChange={(e) => {
252
+ onVaultChange({
253
+ vaultId: selectedVaultId!,
254
+ secretId: selectedSecretId,
255
+ fieldMapping: { connectionString: e.target.value },
256
+ })
257
+ }}
258
+ />
259
+ </div>
260
+ </div>
261
+ )}
262
+
263
+ {vaults.length === 0 && (
264
+ <p className="text-xs" style={{ color: 'var(--m3-secondary)' }}>
265
+ No vaults found. Create one in the Vault section first.
266
+ </p>
267
+ )}
268
+ </div>
269
+ )
270
+ }
@@ -1,47 +1,47 @@
1
- import { Component, type ErrorInfo, type ReactNode } from 'react'
2
-
3
- /** A lazy chunk that failed to load: after a deploy the old file names are gone, so the page needs a reload. */
4
- export function isChunkLoadError(error: unknown): boolean {
5
- const message = error instanceof Error ? `${error.name} ${error.message}` : String(error)
6
- return /ChunkLoadError|Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/i.test(message)
7
- }
8
-
9
- interface Props {
10
- children: ReactNode
11
- }
12
-
13
- interface State {
14
- error: unknown
15
- }
16
-
17
- /** Catches errors thrown while rendering (and by lazy imports) so the screen is not left blank; offers a reload. */
18
- export class ErrorBoundary extends Component<Props, State> {
19
- override state: State = { error: null }
20
-
21
- static getDerivedStateFromError(error: unknown): State {
22
- return { error: error ?? new Error('Unknown error') }
23
- }
24
-
25
- override componentDidCatch(error: unknown, info: ErrorInfo): void {
26
- console.error(error, info.componentStack)
27
- }
28
-
29
- override render(): ReactNode {
30
- const { error } = this.state
31
- if (!error) return this.props.children
32
- const stale = isChunkLoadError(error)
33
- return (
34
- <div role="alert" className="min-h-screen flex flex-col items-center justify-center gap-3 px-6 text-center" style={{ background: 'var(--m3-surface)', color: 'var(--m3-on-surface)' }}>
35
- <span className="material-symbols-outlined" aria-hidden="true" style={{ fontSize: '32px', color: 'var(--m3-down)' }}>error</span>
36
- <h1 className="font-headline text-lg font-bold">{stale ? 'A new version is available' : 'Something went wrong'}</h1>
37
- <p className="text-sm max-w-md" style={{ color: 'var(--m3-secondary)' }}>
38
- {stale ? 'This page was updated while it was open. Reload to continue.' : 'The page could not be displayed. Reloading usually fixes it.'}
39
- </p>
40
- <button type="button" onClick={() => window.location.reload()} className="btn btn-secondary btn-sm">
41
- <span className="material-symbols-outlined" aria-hidden="true">refresh</span>
42
- Reload
43
- </button>
44
- </div>
45
- )
46
- }
47
- }
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react'
2
+
3
+ /** A lazy chunk that failed to load: after a deploy the old file names are gone, so the page needs a reload. */
4
+ export function isChunkLoadError(error: unknown): boolean {
5
+ const message = error instanceof Error ? `${error.name} ${error.message}` : String(error)
6
+ return /ChunkLoadError|Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/i.test(message)
7
+ }
8
+
9
+ interface Props {
10
+ children: ReactNode
11
+ }
12
+
13
+ interface State {
14
+ error: unknown
15
+ }
16
+
17
+ /** Catches errors thrown while rendering (and by lazy imports) so the screen is not left blank; offers a reload. */
18
+ export class ErrorBoundary extends Component<Props, State> {
19
+ override state: State = { error: null }
20
+
21
+ static getDerivedStateFromError(error: unknown): State {
22
+ return { error: error ?? new Error('Unknown error') }
23
+ }
24
+
25
+ override componentDidCatch(error: unknown, info: ErrorInfo): void {
26
+ console.error(error, info.componentStack)
27
+ }
28
+
29
+ override render(): ReactNode {
30
+ const { error } = this.state
31
+ if (!error) return this.props.children
32
+ const stale = isChunkLoadError(error)
33
+ return (
34
+ <div role="alert" className="min-h-screen flex flex-col items-center justify-center gap-3 px-6 text-center" style={{ background: 'var(--m3-surface)', color: 'var(--m3-on-surface)' }}>
35
+ <span className="material-symbols-outlined" aria-hidden="true" style={{ fontSize: '32px', color: 'var(--m3-down)' }}>error</span>
36
+ <h1 className="font-headline text-lg font-bold">{stale ? 'A new version is available' : 'Something went wrong'}</h1>
37
+ <p className="text-sm max-w-md" style={{ color: 'var(--m3-secondary)' }}>
38
+ {stale ? 'This page was updated while it was open. Reload to continue.' : 'The page could not be displayed. Reloading usually fixes it.'}
39
+ </p>
40
+ <button type="button" onClick={() => window.location.reload()} className="btn btn-secondary btn-sm">
41
+ <span className="material-symbols-outlined" aria-hidden="true">refresh</span>
42
+ Reload
43
+ </button>
44
+ </div>
45
+ )
46
+ }
47
+ }