@postgres.ai/shared 4.1.3 → 4.2.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.
- package/.gitlab-ci.yml +7 -3
- package/components/UpgradeCloneModal/index.tsx +145 -0
- package/dist/components/UpgradeCloneModal/index.d.ts +11 -0
- package/dist/components/UpgradeCloneModal/index.js +75 -0
- package/dist/package.json +2 -2
- package/dist/pages/Branches/Branch/index.js +27 -2
- package/dist/pages/Branches/Branch/stores/Main.d.ts +5 -0
- package/dist/pages/Branches/Branch/stores/Main.js +29 -0
- package/dist/pages/Branches/components/BranchesTable/index.js +2 -2
- package/dist/pages/Branches/index.js +10 -1
- package/dist/pages/Clone/Status/index.js +3 -0
- package/dist/pages/Clone/index.js +12 -4
- package/dist/pages/Clone/stores/Main.d.ts +9 -1
- package/dist/pages/Clone/stores/Main.js +40 -3
- package/dist/pages/CreateBranch/stores/Main.d.ts +3 -0
- package/dist/pages/CreateClone/index.js +2 -0
- package/dist/pages/CreateClone/stores/Main.d.ts +5 -1
- package/dist/pages/CreateClone/stores/Main.js +8 -3
- package/dist/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.d.ts +11 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.js +24 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.d.ts +10 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.js +29 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/Sync/index.d.ts +9 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/Sync/index.js +14 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/Walg/index.d.ts +10 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/Walg/index.js +21 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/index.d.ts +10 -0
- package/dist/pages/Instance/Configuration/PhysicalMode/index.js +17 -0
- package/dist/pages/Instance/Configuration/SimpleMode/PreviewCard.d.ts +13 -0
- package/dist/pages/Instance/Configuration/SimpleMode/PreviewCard.js +14 -0
- package/dist/pages/Instance/Configuration/SimpleMode/index.d.ts +19 -0
- package/dist/pages/Instance/Configuration/SimpleMode/index.js +166 -0
- package/dist/pages/Instance/Configuration/configMode.d.ts +2 -0
- package/dist/pages/Instance/Configuration/configMode.js +7 -0
- package/dist/pages/Instance/Configuration/configOptions.d.ts +16 -0
- package/dist/pages/Instance/Configuration/configOptions.js +71 -0
- package/dist/pages/Instance/Configuration/connectionString.d.ts +20 -0
- package/dist/pages/Instance/Configuration/connectionString.js +129 -0
- package/dist/pages/Instance/Configuration/dockerCatalog.d.ts +2 -0
- package/dist/pages/Instance/Configuration/dockerCatalog.js +19 -0
- package/dist/pages/Instance/Configuration/index.d.ts +1 -2
- package/dist/pages/Instance/Configuration/index.js +115 -86
- package/dist/pages/Instance/Configuration/useForm.d.ts +20 -0
- package/dist/pages/Instance/Configuration/useForm.js +126 -5
- package/dist/pages/Instance/Configuration/utils/index.js +1 -17
- package/dist/pages/Instance/Info/Snapshots/Calendar/Day/index.js +3 -3
- package/dist/pages/Instance/Info/Snapshots/Calendar/utils.d.ts +3 -0
- package/dist/pages/Instance/Info/Snapshots/utils.d.ts +6 -0
- package/dist/pages/Instance/Snapshots/components/SnapshotsList/index.js +11 -1
- package/dist/pages/Instance/Snapshots/index.js +11 -3
- package/dist/pages/Instance/Snapshots/utils/index.d.ts +3 -0
- package/dist/pages/Instance/Tabs/styles.js +6 -6
- package/dist/pages/Instance/index.js +1 -3
- package/dist/pages/Instance/stores/Main.d.ts +22 -2
- package/dist/pages/Instance/stores/Main.js +26 -9
- package/dist/pages/Snapshots/Snapshot/index.js +28 -2
- package/dist/pages/Snapshots/Snapshot/stores/Main.d.ts +5 -0
- package/dist/pages/Snapshots/Snapshot/stores/Main.js +29 -0
- package/dist/stores/Snapshots.d.ts +1 -1
- package/dist/stores/Snapshots.js +12 -6
- package/dist/types/api/endpoints/getBranches.d.ts +6 -0
- package/dist/types/api/endpoints/probeSource.d.ts +32 -0
- package/dist/types/api/endpoints/probeSource.js +1 -0
- package/dist/types/api/endpoints/testDbSource.d.ts +3 -1
- package/dist/types/api/endpoints/testDbSource.js +6 -0
- package/dist/types/api/endpoints/updateBranch.d.ts +11 -0
- package/dist/types/api/endpoints/updateBranch.js +1 -0
- package/dist/types/api/endpoints/updateSnapshot.d.ts +11 -0
- package/dist/types/api/endpoints/updateSnapshot.js +1 -0
- package/dist/types/api/endpoints/upgradeClone.d.ts +8 -0
- package/dist/types/api/endpoints/upgradeClone.js +1 -0
- package/dist/types/api/entities/branchSnapshots.d.ts +3 -0
- package/dist/types/api/entities/clone.d.ts +9 -2
- package/dist/types/api/entities/config.d.ts +32 -0
- package/dist/types/api/entities/config.js +45 -18
- package/dist/types/api/entities/instance.d.ts +11 -1
- package/dist/types/api/entities/instanceState.d.ts +16 -1
- package/dist/types/api/entities/snapshot.d.ts +6 -0
- package/dist/utils/api.d.ts +1 -1
- package/dist/utils/api.js +6 -0
- package/dist/utils/clone.d.ts +3 -1
- package/dist/utils/clone.js +7 -1
- package/dist/utils/clonePoller.d.ts +12 -0
- package/dist/utils/clonePoller.js +41 -0
- package/dist/utils/date.js +12 -12
- package/dist/utils/protection.d.ts +5 -0
- package/dist/utils/protection.js +23 -0
- package/package.json +2 -2
- package/pages/Branches/Branch/index.tsx +70 -0
- package/pages/Branches/Branch/stores/Main.ts +40 -0
- package/pages/Branches/components/BranchesTable/index.tsx +2 -0
- package/pages/Branches/index.tsx +13 -1
- package/pages/Clone/Status/index.tsx +3 -0
- package/pages/Clone/index.tsx +58 -0
- package/pages/Clone/stores/Main.ts +51 -8
- package/pages/CreateClone/index.tsx +3 -0
- package/pages/CreateClone/stores/Main.ts +10 -8
- package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.tsx +129 -0
- package/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.tsx +89 -0
- package/pages/Instance/Configuration/PhysicalMode/Sync/index.tsx +64 -0
- package/pages/Instance/Configuration/PhysicalMode/Walg/index.tsx +63 -0
- package/pages/Instance/Configuration/PhysicalMode/index.tsx +93 -0
- package/pages/Instance/Configuration/SimpleMode/PreviewCard.tsx +176 -0
- package/pages/Instance/Configuration/SimpleMode/index.tsx +303 -0
- package/pages/Instance/Configuration/configMode.ts +12 -0
- package/pages/Instance/Configuration/configOptions.ts +111 -0
- package/pages/Instance/Configuration/connectionString.ts +157 -0
- package/pages/Instance/Configuration/dockerCatalog.ts +21 -0
- package/pages/Instance/Configuration/index.tsx +151 -49
- package/pages/Instance/Configuration/useForm.ts +155 -5
- package/pages/Instance/Configuration/utils/index.ts +1 -17
- package/pages/Instance/Info/Snapshots/Calendar/Day/index.tsx +3 -3
- package/pages/Instance/Snapshots/components/SnapshotsList/index.tsx +21 -1
- package/pages/Instance/Snapshots/index.tsx +15 -5
- package/pages/Instance/Tabs/styles.ts +7 -7
- package/pages/Instance/index.tsx +0 -4
- package/pages/Instance/stores/Main.ts +29 -7
- package/pages/Snapshots/Snapshot/index.tsx +71 -0
- package/pages/Snapshots/Snapshot/stores/Main.ts +40 -0
- package/postgres.ai-shared-4.2.0.tgz +0 -0
- package/stores/Snapshots.ts +16 -7
- package/types/api/endpoints/getBranches.ts +3 -0
- package/types/api/endpoints/probeSource.ts +34 -0
- package/types/api/endpoints/testDbSource.ts +10 -1
- package/types/api/endpoints/updateBranch.ts +11 -0
- package/types/api/endpoints/updateSnapshot.ts +11 -0
- package/types/api/endpoints/upgradeClone.ts +8 -0
- package/types/api/entities/clone.ts +13 -1
- package/types/api/entities/config.ts +46 -0
- package/types/api/entities/instanceState.ts +6 -0
- package/types/api/entities/snapshot.ts +3 -0
- package/utils/api.ts +8 -0
- package/utils/clone.ts +7 -1
- package/utils/clonePoller.ts +47 -0
- package/utils/date.ts +19 -14
- package/utils/protection.ts +25 -0
- package/postgres.ai-shared-4.1.3.tgz +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { observer } from 'mobx-react-lite'
|
|
3
|
+
import { Button, TextField, Typography } from '@material-ui/core'
|
|
4
|
+
import Box from '@mui/material/Box'
|
|
5
|
+
|
|
6
|
+
import { Spinner } from '@postgres.ai/shared/components/Spinner'
|
|
7
|
+
import { useStores } from '@postgres.ai/shared/pages/Instance/context'
|
|
8
|
+
import { SeImages } from '@postgres.ai/shared/types/api/endpoints/getSeImages'
|
|
9
|
+
import { ProposedConfig } from '@postgres.ai/shared/types/api/endpoints/probeSource'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ResolvedImage,
|
|
13
|
+
providerKeyToImage,
|
|
14
|
+
resolveDockerImagePath,
|
|
15
|
+
selectSeImage,
|
|
16
|
+
} from '../configOptions'
|
|
17
|
+
import { FormValues } from '../useForm'
|
|
18
|
+
import { PreviewCard } from './PreviewCard'
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
instanceId: string
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
onApplied?: () => void
|
|
24
|
+
onEdit?: (proposed: ProposedConfig, password: string) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// genericFallbackImage resolves the generic CE image for a probed source. Used
|
|
28
|
+
// when no SE image is available (CE, or a managed provider with no SE image for
|
|
29
|
+
// the detected major version).
|
|
30
|
+
const genericFallbackImage = (proposed: ProposedConfig): ResolvedImage => {
|
|
31
|
+
const mapping = providerKeyToImage(
|
|
32
|
+
proposed.dockerImage,
|
|
33
|
+
proposed.pgMajorVersion,
|
|
34
|
+
)
|
|
35
|
+
const isGeneric = mapping.imageType === 'Generic Postgres'
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
dockerPath: resolveDockerImagePath(
|
|
39
|
+
proposed.dockerImage,
|
|
40
|
+
proposed.pgMajorVersion,
|
|
41
|
+
proposed.dockerTag,
|
|
42
|
+
),
|
|
43
|
+
dockerTag: isGeneric ? proposed.dockerTag || mapping.defaultTag || '' : '',
|
|
44
|
+
sharedPreloadLibraries: proposed.sharedPreloadLibraries,
|
|
45
|
+
isSe: false,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Translates a ProposedConfig from POST /admin/probe-source into the
|
|
50
|
+
// FormValues shape the Expert form uses. Both Apply (→ updateConfig) and
|
|
51
|
+
// Edit (→ formik.setValues) consume it, so the engine receives the same
|
|
52
|
+
// projection regardless of which flow the user picks. The resolved image (SE
|
|
53
|
+
// or generic) is passed in so the preview and the applied config never diverge.
|
|
54
|
+
export const buildProjectionFromProposed = (
|
|
55
|
+
proposed: ProposedConfig,
|
|
56
|
+
password: string,
|
|
57
|
+
resolved?: ResolvedImage,
|
|
58
|
+
): FormValues => {
|
|
59
|
+
const mapping = providerKeyToImage(
|
|
60
|
+
proposed.dockerImage,
|
|
61
|
+
proposed.pgMajorVersion,
|
|
62
|
+
)
|
|
63
|
+
const isGeneric = mapping.imageType === 'Generic Postgres'
|
|
64
|
+
const image = resolved ?? genericFallbackImage(proposed)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
debug: false,
|
|
68
|
+
dockerImage: isGeneric
|
|
69
|
+
? String(proposed.pgMajorVersion)
|
|
70
|
+
: mapping.imageType,
|
|
71
|
+
dockerImageType: mapping.imageType,
|
|
72
|
+
dockerPath: image.dockerPath,
|
|
73
|
+
dockerTag: image.dockerTag,
|
|
74
|
+
sharedBuffers: proposed.sharedBuffers,
|
|
75
|
+
sharedPreloadLibraries:
|
|
76
|
+
image.sharedPreloadLibraries ?? proposed.sharedPreloadLibraries,
|
|
77
|
+
// tuningParams is typed as string on FormValues but updateConfig.ts
|
|
78
|
+
// spreads it as a key-value object; cast matches the Expert form's
|
|
79
|
+
// formatTuningParamsToObj(...) as unknown as string pattern.
|
|
80
|
+
tuningParams: { ...proposed.queryTuning } as unknown as string,
|
|
81
|
+
timetable: '0 0 * * *',
|
|
82
|
+
dbname: proposed.source.dbname,
|
|
83
|
+
host: proposed.source.host,
|
|
84
|
+
port: String(proposed.source.port),
|
|
85
|
+
username: proposed.source.username,
|
|
86
|
+
password,
|
|
87
|
+
databases: proposed.databases.join(' '),
|
|
88
|
+
dumpParallelJobs: '',
|
|
89
|
+
dumpIgnoreErrors: false,
|
|
90
|
+
restoreParallelJobs: '',
|
|
91
|
+
// managed sources reference cloud-only extensions/objects (e.g. supabase_vault)
|
|
92
|
+
// that are absent from the clone image; best-effort restore skips them.
|
|
93
|
+
restoreIgnoreErrors: true,
|
|
94
|
+
restoreConfigs: '',
|
|
95
|
+
pgDumpCustomOptions: '',
|
|
96
|
+
// skip ownership and privileges on restore: managed sources (Supabase, RDS)
|
|
97
|
+
// reference roles that do not exist in the clone, which would abort pg_restore.
|
|
98
|
+
pgRestoreCustomOptions: '--no-owner --no-privileges --no-tablespaces',
|
|
99
|
+
retrievalMode: 'logical',
|
|
100
|
+
physicalTool: '',
|
|
101
|
+
physicalDockerImage: '',
|
|
102
|
+
physicalSyncEnabled: false,
|
|
103
|
+
physicalWalgBackupName: '',
|
|
104
|
+
physicalPgbackrestStanza: '',
|
|
105
|
+
physicalPgbackrestDelta: false,
|
|
106
|
+
physicalEnvs: [],
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// resolveProbeImage selects the platform SE image for a managed provider when
|
|
111
|
+
// the SE catalog is reachable — SE/Enterprise instances expose it (platformUrl
|
|
112
|
+
// set), CE returns undefined. Falls back to the generic image for CE and for
|
|
113
|
+
// managed providers with no SE image at the detected major version.
|
|
114
|
+
export const resolveProbeImage = async (
|
|
115
|
+
proposed: ProposedConfig,
|
|
116
|
+
getSeImages: (args: {
|
|
117
|
+
packageGroup: string
|
|
118
|
+
}) => Promise<SeImages[] | null | undefined>,
|
|
119
|
+
): Promise<ResolvedImage> => {
|
|
120
|
+
const mapping = providerKeyToImage(
|
|
121
|
+
proposed.dockerImage,
|
|
122
|
+
proposed.pgMajorVersion,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if (mapping.imageType !== 'Generic Postgres') {
|
|
126
|
+
const seImages = await getSeImages({ packageGroup: mapping.imageType })
|
|
127
|
+
const se = selectSeImage(seImages, proposed.pgMajorVersion)
|
|
128
|
+
|
|
129
|
+
if (se) {
|
|
130
|
+
return {
|
|
131
|
+
dockerPath: se.location,
|
|
132
|
+
dockerTag: se.tag,
|
|
133
|
+
sharedPreloadLibraries: se.pg_config_presets?.shared_preload_libraries,
|
|
134
|
+
isSe: true,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return genericFallbackImage(proposed)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const SimpleMode = observer(
|
|
143
|
+
({ instanceId, disabled, onApplied, onEdit }: Props) => {
|
|
144
|
+
const stores = useStores()
|
|
145
|
+
const main = stores.main
|
|
146
|
+
|
|
147
|
+
const [url, setUrl] = useState('')
|
|
148
|
+
const [password, setPassword] = useState('')
|
|
149
|
+
const [probing, setProbing] = useState(false)
|
|
150
|
+
const [probeError, setProbeError] = useState<string | null>(null)
|
|
151
|
+
const [proposed, setProposed] = useState<ProposedConfig | null>(null)
|
|
152
|
+
const [resolved, setResolved] = useState<ResolvedImage | null>(null)
|
|
153
|
+
const [applying, setApplying] = useState(false)
|
|
154
|
+
const [applyError, setApplyError] = useState<string | null>(null)
|
|
155
|
+
|
|
156
|
+
const onDetect = async () => {
|
|
157
|
+
setProbing(true)
|
|
158
|
+
setProbeError(null)
|
|
159
|
+
setProposed(null)
|
|
160
|
+
setResolved(null)
|
|
161
|
+
setApplyError(null)
|
|
162
|
+
|
|
163
|
+
const result = await main.probeSource({ url, password })
|
|
164
|
+
|
|
165
|
+
if (!result) {
|
|
166
|
+
setProbing(false)
|
|
167
|
+
setProbeError('Probe is not available on this instance.')
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (result.error) {
|
|
171
|
+
setProbing(false)
|
|
172
|
+
setProbeError(result.error.message)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (result.response) {
|
|
176
|
+
const image = await resolveProbeImage(result.response, main.getSeImages)
|
|
177
|
+
setResolved(image)
|
|
178
|
+
setProposed(result.response)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setProbing(false)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const onApply = async () => {
|
|
185
|
+
if (!proposed || !resolved) return
|
|
186
|
+
|
|
187
|
+
setApplying(true)
|
|
188
|
+
setApplyError(null)
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const projection = buildProjectionFromProposed(proposed, password, resolved)
|
|
192
|
+
const response = await main.updateConfig(projection, instanceId)
|
|
193
|
+
|
|
194
|
+
if (!response) {
|
|
195
|
+
setApplyError(
|
|
196
|
+
main.configError ?? 'Could not apply the proposed configuration.',
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const refresh = await main.fullRefresh(instanceId)
|
|
202
|
+
|
|
203
|
+
if (refresh?.error) {
|
|
204
|
+
setApplyError(
|
|
205
|
+
`Configuration applied, but starting data retrieval failed: ${refresh.error.message}`,
|
|
206
|
+
)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
onApplied?.()
|
|
211
|
+
} catch (err) {
|
|
212
|
+
setApplyError(
|
|
213
|
+
err instanceof Error
|
|
214
|
+
? err.message
|
|
215
|
+
: 'Could not apply the proposed configuration.',
|
|
216
|
+
)
|
|
217
|
+
} finally {
|
|
218
|
+
setApplying(false)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const handleEdit = () => {
|
|
223
|
+
if (proposed) onEdit?.(proposed, password)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const canDetect =
|
|
227
|
+
!probing && url.trim().length > 0 && password.length > 0 && !disabled
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<Box mt={2} mb={2} data-testid="simple-mode">
|
|
231
|
+
{!proposed && (
|
|
232
|
+
<Box>
|
|
233
|
+
<Typography variant="h6">Simple configuration</Typography>
|
|
234
|
+
<Typography variant="body2">
|
|
235
|
+
Paste your source connection string and password. We'll probe
|
|
236
|
+
the source, propose a configuration, and let you review before
|
|
237
|
+
starting retrieval.
|
|
238
|
+
</Typography>
|
|
239
|
+
|
|
240
|
+
<Box mt={2}>
|
|
241
|
+
<TextField
|
|
242
|
+
label="Connection string"
|
|
243
|
+
placeholder="postgres://user@host:5432/dbname"
|
|
244
|
+
value={url}
|
|
245
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
246
|
+
fullWidth
|
|
247
|
+
disabled={probing || disabled}
|
|
248
|
+
inputProps={{ 'data-testid': 'simple-url' }}
|
|
249
|
+
/>
|
|
250
|
+
</Box>
|
|
251
|
+
|
|
252
|
+
<Box mt={2}>
|
|
253
|
+
<TextField
|
|
254
|
+
label="Password"
|
|
255
|
+
type="password"
|
|
256
|
+
value={password}
|
|
257
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
258
|
+
fullWidth
|
|
259
|
+
disabled={probing || disabled}
|
|
260
|
+
inputProps={{ 'data-testid': 'simple-password' }}
|
|
261
|
+
/>
|
|
262
|
+
</Box>
|
|
263
|
+
|
|
264
|
+
<Box mt={2}>
|
|
265
|
+
<Button
|
|
266
|
+
variant="contained"
|
|
267
|
+
color="secondary"
|
|
268
|
+
onClick={onDetect}
|
|
269
|
+
disabled={!canDetect}
|
|
270
|
+
data-testid="simple-detect"
|
|
271
|
+
>
|
|
272
|
+
Detect & preview
|
|
273
|
+
{probing && <Spinner size="sm" />}
|
|
274
|
+
</Button>
|
|
275
|
+
</Box>
|
|
276
|
+
|
|
277
|
+
{probeError && (
|
|
278
|
+
<Box
|
|
279
|
+
mt={1}
|
|
280
|
+
color="#d32f2f"
|
|
281
|
+
fontSize={13}
|
|
282
|
+
data-testid="probe-error"
|
|
283
|
+
>
|
|
284
|
+
{probeError}
|
|
285
|
+
</Box>
|
|
286
|
+
)}
|
|
287
|
+
</Box>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{proposed && resolved && (
|
|
291
|
+
<PreviewCard
|
|
292
|
+
proposed={proposed}
|
|
293
|
+
resolved={resolved}
|
|
294
|
+
applying={applying}
|
|
295
|
+
applyError={applyError}
|
|
296
|
+
onApply={onApply}
|
|
297
|
+
onEdit={handleEdit}
|
|
298
|
+
/>
|
|
299
|
+
)}
|
|
300
|
+
</Box>
|
|
301
|
+
)
|
|
302
|
+
},
|
|
303
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ConfigMode = 'simple' | 'expert'
|
|
2
|
+
|
|
3
|
+
// Picks the default Configuration tab when the page first finishes loading
|
|
4
|
+
// the server config. An unconfigured instance lands on Simple; an instance
|
|
5
|
+
// that already has a source host filled in OR a non-logical retrieval mode
|
|
6
|
+
// configured lands on Expert so the user sees the form they previously
|
|
7
|
+
// interacted with. Physical-mode users do not have a host field but still
|
|
8
|
+
// need to land on Expert because Simple-mode targets logical retrieval.
|
|
9
|
+
export const getInitialConfigMode = (
|
|
10
|
+
host: string | undefined,
|
|
11
|
+
retrievalMode?: string,
|
|
12
|
+
): ConfigMode => (host || retrievalMode === 'physical' ? 'expert' : 'simple')
|
|
@@ -1,3 +1,114 @@
|
|
|
1
|
+
import { SeImages } from '@postgres.ai/shared/types/api/endpoints/getSeImages'
|
|
2
|
+
|
|
3
|
+
import { dockerImagesConfig, genericImagePrefix } from './dockerCatalog'
|
|
4
|
+
|
|
5
|
+
// Mapping from the engine probe's provider key (probe.Provider) to a
|
|
6
|
+
// dockerImageOptions.type value the form already understands. Keys must
|
|
7
|
+
// match the values defined in engine/internal/retrieval/probe/provider.go
|
|
8
|
+
// (ProviderRDS, ProviderAurora, etc.) — see Task 18 in the plan.
|
|
9
|
+
const providerKeyToImageType: Record<string, string> = {
|
|
10
|
+
generic: 'Generic Postgres',
|
|
11
|
+
rds: 'rds',
|
|
12
|
+
aurora: 'aurora',
|
|
13
|
+
cloudsql: 'google-cloud-sql',
|
|
14
|
+
supabase: 'supabase',
|
|
15
|
+
heroku: 'heroku',
|
|
16
|
+
timescale: 'timescale-cloud',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ProviderImageMapping = {
|
|
20
|
+
imageType: string
|
|
21
|
+
defaultTag?: string
|
|
22
|
+
fallback: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Reads the most recent tag for a given PG major from the shared catalog.
|
|
26
|
+
// SE images (rds, aurora, etc.) require the platform-only getSeImages call
|
|
27
|
+
// and have no entry here — they return defaultTag: undefined.
|
|
28
|
+
const genericDefaultTag = (pgMajorVersion: number): string | undefined => {
|
|
29
|
+
if (!pgMajorVersion) return undefined
|
|
30
|
+
const version = String(pgMajorVersion)
|
|
31
|
+
const tags = dockerImagesConfig[version]
|
|
32
|
+
if (!tags || tags.length === 0) return undefined
|
|
33
|
+
return `${version}-${tags[0]}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// genericImagePathForVersion returns a pullable generic extended-postgres
|
|
37
|
+
// reference for a PG major version, or '' when the version is unknown. Simple
|
|
38
|
+
// mode uses it as the CE fallback for managed providers, whose SE images are
|
|
39
|
+
// only resolvable through the platform-only getSeImages call.
|
|
40
|
+
export const genericImagePathForVersion = (pgMajorVersion: number): string => {
|
|
41
|
+
const tag = genericDefaultTag(pgMajorVersion)
|
|
42
|
+
return tag ? `${genericImagePrefix}:${tag}` : ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ResolvedImage is the concrete image Simple mode ships for a probed source:
|
|
46
|
+
// either a platform SE image (carrying its curated preload-library preset) or
|
|
47
|
+
// the generic CE fallback. Apply and the preview both consume it, so they show
|
|
48
|
+
// and post the same thing.
|
|
49
|
+
export type ResolvedImage = {
|
|
50
|
+
dockerPath: string
|
|
51
|
+
dockerTag: string
|
|
52
|
+
sharedPreloadLibraries?: string
|
|
53
|
+
isSe: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// selectSeImage picks the SE catalog entry matching the detected major version.
|
|
57
|
+
// Returns undefined when the catalog is empty (CE) or has no matching version,
|
|
58
|
+
// which signals the caller to fall back to the generic image.
|
|
59
|
+
export const selectSeImage = (
|
|
60
|
+
seImages: SeImages[] | null | undefined,
|
|
61
|
+
pgMajorVersion: number,
|
|
62
|
+
): SeImages | undefined =>
|
|
63
|
+
seImages?.find((image) => image.pg_major_version === String(pgMajorVersion))
|
|
64
|
+
|
|
65
|
+
// resolveDockerImagePath returns the concrete image reference Simple mode ships
|
|
66
|
+
// for a probed source. Managed providers fall back to the generic image in CE,
|
|
67
|
+
// so both the Apply payload and the preview show the same value.
|
|
68
|
+
export const resolveDockerImagePath = (
|
|
69
|
+
providerKey: string,
|
|
70
|
+
pgMajorVersion: number,
|
|
71
|
+
dockerTag?: string,
|
|
72
|
+
): string => {
|
|
73
|
+
const mapping = providerKeyToImage(providerKey, pgMajorVersion)
|
|
74
|
+
const tag = dockerTag || mapping.defaultTag || ''
|
|
75
|
+
|
|
76
|
+
if (mapping.imageType === 'Generic Postgres') {
|
|
77
|
+
return tag ? `${genericImagePrefix}:${tag}` : ''
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return genericImagePathForVersion(pgMajorVersion)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Resolves a probe provider key to a concrete docker image type the
|
|
84
|
+
// Configuration form can write into the projection. Unknown keys (including
|
|
85
|
+
// "azure", which has no matching SE image today) fall back to the generic
|
|
86
|
+
// Postgres image and set fallback=true so the UI can warn.
|
|
87
|
+
export const providerKeyToImage = (
|
|
88
|
+
providerKey: string,
|
|
89
|
+
pgMajorVersion: number,
|
|
90
|
+
): ProviderImageMapping => {
|
|
91
|
+
const known = providerKeyToImageType[providerKey]
|
|
92
|
+
|
|
93
|
+
if (!known) {
|
|
94
|
+
return {
|
|
95
|
+
imageType: 'Generic Postgres',
|
|
96
|
+
defaultTag: genericDefaultTag(pgMajorVersion),
|
|
97
|
+
fallback: true,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (known === 'Generic Postgres') {
|
|
102
|
+
return {
|
|
103
|
+
imageType: 'Generic Postgres',
|
|
104
|
+
defaultTag: genericDefaultTag(pgMajorVersion),
|
|
105
|
+
fallback: false,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { imageType: known, defaultTag: undefined, fallback: false }
|
|
110
|
+
}
|
|
111
|
+
|
|
1
112
|
export const dockerImageOptions = [
|
|
2
113
|
{
|
|
3
114
|
name: 'Generic PostgreSQL (postgresai/extended-postgres)',
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// connectionString.ts — Expert-mode helper that converts between the
|
|
2
|
+
// (host, port, username, dbname) shape stored in `server.yml` and the
|
|
3
|
+
// single URL field the form exposes. The engine remains the source of
|
|
4
|
+
// truth for actual probing (see engine/internal/retrieval/probe/parser.go);
|
|
5
|
+
// this parser only powers the form's load/save round-trip.
|
|
6
|
+
//
|
|
7
|
+
// Accepts both URI form (postgres://user@host:5432/dbname) and DSN form
|
|
8
|
+
// (host=db port=5432 user=app dbname=shop). Rejects any input that carries
|
|
9
|
+
// a password — passwords belong in the masked field, never the URL field.
|
|
10
|
+
|
|
11
|
+
export type ConnectionFields = {
|
|
12
|
+
host: string
|
|
13
|
+
port: string
|
|
14
|
+
username: string
|
|
15
|
+
dbname: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ParseResult = {
|
|
19
|
+
fields: ConnectionFields
|
|
20
|
+
portWasExplicit: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ConnectionStringError extends Error {}
|
|
24
|
+
|
|
25
|
+
export const ErrPasswordInConnectionString = 'Password must be entered in the Password field, not the connection string.'
|
|
26
|
+
export const ErrMultiHost = 'Multi-host connection strings are not supported.'
|
|
27
|
+
export const ErrInvalidConnectionString = 'Could not parse the connection string.'
|
|
28
|
+
|
|
29
|
+
const URI_SCHEMES = ['postgres:', 'postgresql:']
|
|
30
|
+
|
|
31
|
+
const isUri = (s: string) => URI_SCHEMES.some((p) => s.toLowerCase().startsWith(p))
|
|
32
|
+
|
|
33
|
+
const stripBrackets = (h: string) => (h.startsWith('[') && h.endsWith(']') ? h.slice(1, -1) : h)
|
|
34
|
+
|
|
35
|
+
const parseUri = (s: string): ParseResult => {
|
|
36
|
+
let url: URL
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(s)
|
|
39
|
+
} catch {
|
|
40
|
+
throw new ConnectionStringError(ErrInvalidConnectionString)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (url.password) throw new ConnectionStringError(ErrPasswordInConnectionString)
|
|
44
|
+
|
|
45
|
+
if (!url.hostname) throw new ConnectionStringError(ErrInvalidConnectionString)
|
|
46
|
+
if (url.hostname.includes(',')) throw new ConnectionStringError(ErrMultiHost)
|
|
47
|
+
|
|
48
|
+
const portWasExplicit = url.port !== ''
|
|
49
|
+
const path = url.pathname.startsWith('/') ? url.pathname.slice(1) : url.pathname
|
|
50
|
+
const dbname = path.includes(',') ? '' : decodeURIComponent(path)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
fields: {
|
|
54
|
+
host: stripBrackets(url.hostname),
|
|
55
|
+
port: portWasExplicit ? url.port : '5432',
|
|
56
|
+
username: url.username ? decodeURIComponent(url.username) : '',
|
|
57
|
+
dbname,
|
|
58
|
+
},
|
|
59
|
+
portWasExplicit,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Tokenizer for libpq DSN form: key=value pairs, values may be single-quoted
|
|
64
|
+
// when they contain spaces (libpq spec). Throws on malformed input.
|
|
65
|
+
const tokenizeDsn = (s: string): Record<string, string> => {
|
|
66
|
+
const out: Record<string, string> = {}
|
|
67
|
+
let i = 0
|
|
68
|
+
|
|
69
|
+
while (i < s.length) {
|
|
70
|
+
// skip leading whitespace
|
|
71
|
+
while (i < s.length && /\s/.test(s[i])) i++
|
|
72
|
+
if (i >= s.length) break
|
|
73
|
+
|
|
74
|
+
// read key
|
|
75
|
+
const keyStart = i
|
|
76
|
+
while (i < s.length && s[i] !== '=' && !/\s/.test(s[i])) i++
|
|
77
|
+
if (i >= s.length || s[i] !== '=') throw new ConnectionStringError(ErrInvalidConnectionString)
|
|
78
|
+
const key = s.slice(keyStart, i)
|
|
79
|
+
i++ // skip '='
|
|
80
|
+
|
|
81
|
+
// read value (possibly quoted)
|
|
82
|
+
let value = ''
|
|
83
|
+
if (s[i] === "'") {
|
|
84
|
+
i++
|
|
85
|
+
while (i < s.length && s[i] !== "'") {
|
|
86
|
+
if (s[i] === '\\' && i + 1 < s.length) {
|
|
87
|
+
value += s[i + 1]
|
|
88
|
+
i += 2
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
value += s[i]
|
|
92
|
+
i++
|
|
93
|
+
}
|
|
94
|
+
if (i >= s.length) throw new ConnectionStringError(ErrInvalidConnectionString)
|
|
95
|
+
i++ // skip closing quote
|
|
96
|
+
} else {
|
|
97
|
+
while (i < s.length && !/\s/.test(s[i])) {
|
|
98
|
+
value += s[i]
|
|
99
|
+
i++
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
out[key.toLowerCase()] = value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parseDsn = (s: string): ParseResult => {
|
|
110
|
+
const tokens = tokenizeDsn(s)
|
|
111
|
+
if (tokens.password) throw new ConnectionStringError(ErrPasswordInConnectionString)
|
|
112
|
+
|
|
113
|
+
const host = tokens.host ?? tokens.hostaddr ?? ''
|
|
114
|
+
if (host.includes(',')) throw new ConnectionStringError(ErrMultiHost)
|
|
115
|
+
|
|
116
|
+
const portRaw = tokens.port ?? ''
|
|
117
|
+
const portWasExplicit = portRaw !== ''
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
fields: {
|
|
121
|
+
host,
|
|
122
|
+
port: portWasExplicit ? portRaw : '5432',
|
|
123
|
+
username: tokens.user ?? tokens.username ?? '',
|
|
124
|
+
dbname: tokens.dbname ?? tokens.database ?? '',
|
|
125
|
+
},
|
|
126
|
+
portWasExplicit,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const connectionStringToFields = (s: string): ParseResult => {
|
|
131
|
+
const trimmed = s.trim()
|
|
132
|
+
if (!trimmed) throw new ConnectionStringError(ErrInvalidConnectionString)
|
|
133
|
+
|
|
134
|
+
if (isUri(trimmed)) return parseUri(trimmed)
|
|
135
|
+
if (trimmed.includes('=')) return parseDsn(trimmed)
|
|
136
|
+
throw new ConnectionStringError(ErrInvalidConnectionString)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// formatHost wraps IPv6 literals in brackets so the resulting URI parses cleanly.
|
|
140
|
+
const formatHost = (host: string) => (host.includes(':') ? `[${host}]` : host)
|
|
141
|
+
|
|
142
|
+
export type SerializeOptions = {
|
|
143
|
+
// When true, omit `:5432` from the URI even if `fields.port === '5432'`.
|
|
144
|
+
// Used to preserve configs that originally had no `port:` key.
|
|
145
|
+
omitDefaultPort?: boolean
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const connectionStringFromFields = (
|
|
149
|
+
fields: ConnectionFields,
|
|
150
|
+
opts: SerializeOptions = {},
|
|
151
|
+
): string => {
|
|
152
|
+
const userPart = fields.username ? `${encodeURIComponent(fields.username)}@` : ''
|
|
153
|
+
const showPort = fields.port && !(opts.omitDefaultPort && fields.port === '5432')
|
|
154
|
+
const portPart = showPort ? `:${fields.port}` : ''
|
|
155
|
+
const dbPart = fields.dbname ? `/${encodeURIComponent(fields.dbname)}` : ''
|
|
156
|
+
return `postgres://${userPart}${formatHost(fields.host)}${portPart}${dbPart}`
|
|
157
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shared image-catalog constants. Kept in its own module so both
|
|
2
|
+
// `configOptions.ts` and `utils/index.ts` can read them without creating
|
|
3
|
+
// an import cycle (utils → configOptions today).
|
|
4
|
+
|
|
5
|
+
export const genericImagePrefix = 'postgresai/extended-postgres'
|
|
6
|
+
|
|
7
|
+
// Predefined Docker image catalog for the Generic Postgres image. If a
|
|
8
|
+
// user's config references a tag not listed here, createEnhancedDockerImages
|
|
9
|
+
// in utils/index.ts appends it at runtime.
|
|
10
|
+
export const dockerImagesConfig: Record<string, string[]> = {
|
|
11
|
+
'9.6': ['0.5.3', '0.5.2', '0.5.1'],
|
|
12
|
+
'10': ['0.5.3', '0.5.2', '0.5.1'],
|
|
13
|
+
'11': ['0.5.3', '0.5.2', '0.5.1'],
|
|
14
|
+
'12': ['0.5.3', '0.5.2', '0.5.1'],
|
|
15
|
+
'13': ['0.5.3', '0.5.2', '0.5.1'],
|
|
16
|
+
'14': ['0.5.3', '0.5.2', '0.5.1'],
|
|
17
|
+
'15': ['0.5.3', '0.5.2', '0.5.1'],
|
|
18
|
+
'16': ['0.5.3', '0.5.2', '0.5.1'],
|
|
19
|
+
'17': ['0.5.3', '0.5.2', '0.5.1'],
|
|
20
|
+
'18': ['0.6.1'],
|
|
21
|
+
}
|