@postgres.ai/shared 4.0.2-pr-1149 → 4.0.2-pr-1148.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 (35) hide show
  1. package/package.json +1 -1
  2. package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.d.ts +11 -0
  3. package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.js +24 -0
  4. package/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.d.ts +10 -0
  5. package/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.js +29 -0
  6. package/pages/Instance/Configuration/PhysicalMode/Sync/index.d.ts +9 -0
  7. package/pages/Instance/Configuration/PhysicalMode/Sync/index.js +14 -0
  8. package/pages/Instance/Configuration/PhysicalMode/Walg/index.d.ts +10 -0
  9. package/pages/Instance/Configuration/PhysicalMode/Walg/index.js +21 -0
  10. package/pages/Instance/Configuration/PhysicalMode/index.d.ts +10 -0
  11. package/pages/Instance/Configuration/PhysicalMode/index.js +17 -0
  12. package/pages/Instance/Configuration/SimpleMode/PreviewCard.d.ts +11 -0
  13. package/pages/Instance/Configuration/SimpleMode/PreviewCard.js +14 -0
  14. package/pages/Instance/Configuration/SimpleMode/index.d.ts +14 -0
  15. package/pages/Instance/Configuration/SimpleMode/index.js +107 -0
  16. package/pages/Instance/Configuration/configMode.d.ts +2 -0
  17. package/pages/Instance/Configuration/configMode.js +7 -0
  18. package/pages/Instance/Configuration/configOptions.d.ts +6 -0
  19. package/pages/Instance/Configuration/configOptions.js +48 -0
  20. package/pages/Instance/Configuration/connectionString.d.ts +20 -0
  21. package/pages/Instance/Configuration/connectionString.js +129 -0
  22. package/pages/Instance/Configuration/dockerCatalog.d.ts +2 -0
  23. package/pages/Instance/Configuration/dockerCatalog.js +19 -0
  24. package/pages/Instance/Configuration/index.d.ts +1 -2
  25. package/pages/Instance/Configuration/index.js +115 -86
  26. package/pages/Instance/Configuration/useForm.d.ts +20 -0
  27. package/pages/Instance/Configuration/useForm.js +126 -5
  28. package/pages/Instance/Configuration/utils/index.js +1 -17
  29. package/pages/Instance/index.js +1 -3
  30. package/pages/Instance/stores/Main.d.ts +20 -0
  31. package/pages/Instance/stores/Main.js +9 -0
  32. package/types/api/endpoints/probeSource.d.ts +32 -0
  33. package/types/api/endpoints/probeSource.js +1 -0
  34. package/types/api/entities/config.d.ts +32 -0
  35. package/types/api/entities/config.js +45 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postgres.ai/shared",
3
- "version": "4.0.2-pr-1149",
3
+ "version": "4.0.2-pr-1148.1",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "peerDependencies": {
@@ -0,0 +1,11 @@
1
+ /// <reference types="react" />
2
+ import { PhysicalEnv } from '../../useForm';
3
+ declare type Props = {
4
+ envs: PhysicalEnv[];
5
+ onChange: (envs: PhysicalEnv[]) => void;
6
+ suggestions?: string[];
7
+ disabled?: boolean;
8
+ keyErrors?: (string | undefined)[];
9
+ };
10
+ export declare const EnvsEditor: ({ envs, onChange, suggestions, disabled, keyErrors, }: Props) => JSX.Element;
11
+ export {};
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, IconButton, TextField, Typography } from '@material-ui/core';
3
+ import Box from '@mui/material/Box';
4
+ // EnvsEditor renders a rows-of-key/value editor with add/remove and a
5
+ // click-to-add suggestion list. Engine consumes envs as a free-form map
6
+ // (physical.go:76, CopyOptions.Envs map[string]string); the UI is a thin
7
+ // surface over that map.
8
+ export const EnvsEditor = ({ envs, onChange, suggestions = [], disabled, keyErrors = [], }) => {
9
+ const updateRow = (i, patch) => {
10
+ const next = envs.slice();
11
+ next[i] = { ...next[i], ...patch };
12
+ onChange(next);
13
+ };
14
+ const removeRow = (i) => {
15
+ const next = envs.slice();
16
+ next.splice(i, 1);
17
+ onChange(next);
18
+ };
19
+ const addRow = (key = '') => {
20
+ onChange([...envs, { key, value: '' }]);
21
+ };
22
+ const usedKeys = new Set(envs.map((e) => e.key));
23
+ return (_jsxs(Box, { mt: 1, "data-testid": "envs-editor", children: [_jsx(Typography, { variant: "subtitle2", children: "Environment variables" }), envs.length === 0 ? (_jsx(Box, { mt: 1, mb: 1, children: _jsx(Typography, { variant: "caption", color: "textSecondary", children: "No environment variables set. Use suggestions below or click \"Add\"." }) })) : (envs.map((env, i) => (_jsxs(Box, { display: "flex", alignItems: "center", mt: 1, "data-testid": `envs-row-${i}`, children: [_jsx(TextField, { size: "small", label: "Name", value: env.key, disabled: disabled, error: Boolean(keyErrors[i]), helperText: keyErrors[i], onChange: (e) => updateRow(i, { key: e.target.value }), inputProps: { 'data-testid': `envs-key-${i}` } }), _jsx(Box, { mx: 1, children: _jsx(TextField, { size: "small", label: "Value", value: env.value, disabled: disabled, onChange: (e) => updateRow(i, { value: e.target.value }), inputProps: { 'data-testid': `envs-value-${i}` } }) }), _jsx(IconButton, { size: "small", "aria-label": "remove env", disabled: disabled, onClick: () => removeRow(i), "data-testid": `envs-remove-${i}`, children: "\u00D7" })] }, i)))), _jsx(Box, { mt: 1, children: _jsx(Button, { size: "small", variant: "outlined", disabled: disabled, onClick: () => addRow(), "data-testid": "envs-add", children: "+ Add" }) }), suggestions.length > 0 && (_jsxs(Box, { mt: 1, children: [_jsx(Typography, { variant: "caption", color: "textSecondary", children: "Suggestions:" }), _jsx(Box, { mt: 0.5, children: suggestions.map((s) => (_jsx(Button, { size: "small", variant: "text", disabled: disabled || usedKeys.has(s), onClick: () => addRow(s), "data-testid": `envs-suggest-${s}`, children: s }, s))) })] }))] }));
24
+ };
@@ -0,0 +1,10 @@
1
+ /// <reference types="react" />
2
+ import { FormValues } from '../../useForm';
3
+ declare type Props = {
4
+ values: FormValues;
5
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void;
6
+ disabled?: boolean;
7
+ envsKeyErrors?: (string | undefined)[];
8
+ };
9
+ export declare const PgBackRest: ({ values, onChange, disabled, envsKeyErrors, }: Props) => JSX.Element;
10
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Checkbox, FormControlLabel, TextField, Typography, } from '@material-ui/core';
3
+ import Box from '@mui/material/Box';
4
+ import { EnvsEditor } from '../EnvsEditor';
5
+ // pgBackRest exposes two structured fields (stanza, delta); repo paths, S3
6
+ // keys, archive options all live in the envs map. See
7
+ // engine/internal/retrieval/engine/postgres/physical/pgbackrest.go:23-26 and
8
+ // the example envs in config.example.physical_pgbackrest.yml:84-99.
9
+ const PGBACKREST_ENV_SUGGESTIONS = [
10
+ 'PGBACKREST_REPO',
11
+ 'PGBACKREST_REPO1_TYPE',
12
+ 'PGBACKREST_REPO1_PATH',
13
+ 'PGBACKREST_REPO1_HOST',
14
+ 'PGBACKREST_REPO1_HOST_USER',
15
+ 'PGBACKREST_REPO1_S3_BUCKET',
16
+ 'PGBACKREST_REPO1_S3_ENDPOINT',
17
+ 'PGBACKREST_REPO1_S3_KEY',
18
+ 'PGBACKREST_REPO1_S3_KEY_SECRET',
19
+ 'PGBACKREST_REPO1_S3_REGION',
20
+ 'PGBACKREST_LOG_LEVEL_CONSOLE',
21
+ 'PGBACKREST_PROCESS_MAX',
22
+ ];
23
+ export const PgBackRest = ({ values, onChange, disabled, envsKeyErrors, }) => {
24
+ const onEnvsChange = (envs) => onChange('physicalEnvs', envs);
25
+ return (_jsxs(Box, { mt: 2, "data-testid": "pgbackrest-form", children: [_jsx(Typography, { variant: "subtitle1", children: "pgBackRest" }), _jsx(Box, { mt: 1, children: _jsx(TextField, { fullWidth: true, label: "Stanza", placeholder: "my-stanza", value: values.physicalPgbackrestStanza, disabled: disabled, onChange: (e) => onChange('physicalPgbackrestStanza', e.target.value), helperText: "Stanza name (must match the stanza configured in your pgBackRest setup).", inputProps: { 'data-testid': 'pgbackrest-stanza' } }) }), _jsx(Box, { mt: 1, children: _jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: values.physicalPgbackrestDelta, disabled: disabled, onChange: (e) => onChange('physicalPgbackrestDelta', e.target.checked), inputProps: {
26
+ 'aria-label': 'delta',
27
+ 'data-testid': 'pgbackrest-delta',
28
+ } }), label: "Delta restore" }) }), _jsx(EnvsEditor, { envs: values.physicalEnvs, onChange: onEnvsChange, suggestions: PGBACKREST_ENV_SUGGESTIONS, disabled: disabled, keyErrors: envsKeyErrors })] }));
29
+ };
@@ -0,0 +1,9 @@
1
+ /// <reference types="react" />
2
+ import { FormValues } from '../../useForm';
3
+ declare type Props = {
4
+ values: FormValues;
5
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void;
6
+ disabled?: boolean;
7
+ };
8
+ export declare const Sync: ({ values, onChange, disabled }: Props) => JSX.Element;
9
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Checkbox, FormControlLabel, TextField, Typography, } from '@material-ui/core';
3
+ import Box from '@mui/material/Box';
4
+ // Shared section rendered below WAL-G / pgBackRest with the structured fields
5
+ // that apply across all physical sub-tools (sync.enabled and dockerImage). The
6
+ // engine surfaces more knobs (sync.healthCheck, sync.configs, recovery target,
7
+ // custom restore command), but they remain YAML-only for 4.2 to keep the
8
+ // projection footprint flat.
9
+ export const Sync = ({ values, onChange, disabled }) => {
10
+ return (_jsxs(Box, { mt: 2, "data-testid": "physical-sync", children: [_jsx(Typography, { variant: "subtitle1", children: "Sync container & image" }), _jsx(Box, { mt: 1, children: _jsx(TextField, { fullWidth: true, label: "Docker image", placeholder: "postgresai/extended-postgres:18-0.6.2", value: values.physicalDockerImage, disabled: disabled, onChange: (e) => onChange('physicalDockerImage', e.target.value), helperText: "Postgres image for restore/sync containers. Major version must match the source.", inputProps: { 'data-testid': 'physical-docker-image' } }) }), _jsx(Box, { mt: 1, children: _jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: values.physicalSyncEnabled, disabled: disabled, onChange: (e) => onChange('physicalSyncEnabled', e.target.checked), inputProps: {
11
+ 'aria-label': 'sync enabled',
12
+ 'data-testid': 'physical-sync-enabled',
13
+ } }), label: "Run sync container" }) }), _jsx(Box, { mt: 1, children: _jsx(Typography, { variant: "caption", color: "textSecondary", children: "For advanced sync settings (health check, sync postgres configs, recovery target), edit the YAML config directly." }) })] }));
14
+ };
@@ -0,0 +1,10 @@
1
+ /// <reference types="react" />
2
+ import { FormValues } from '../../useForm';
3
+ declare type Props = {
4
+ values: FormValues;
5
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void;
6
+ disabled?: boolean;
7
+ envsKeyErrors?: (string | undefined)[];
8
+ };
9
+ export declare const Walg: ({ values, onChange, disabled, envsKeyErrors }: Props) => JSX.Element;
10
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TextField, Typography } from '@material-ui/core';
3
+ import Box from '@mui/material/Box';
4
+ import { EnvsEditor } from '../EnvsEditor';
5
+ // WAL-G has one structured field (BackupName, defaulting to "LATEST"); storage
6
+ // backend, bucket, prefix, and credentials all live in the envs map. See
7
+ // engine/internal/retrieval/engine/postgres/physical/wal_g.go:36-38 and the
8
+ // example envs in config.example.physical_walg.yml:84-86.
9
+ const WALG_ENV_SUGGESTIONS = [
10
+ 'WALG_GS_PREFIX',
11
+ 'WALG_S3_PREFIX',
12
+ 'WALG_FILE_PREFIX',
13
+ 'GOOGLE_APPLICATION_CREDENTIALS',
14
+ 'AWS_ACCESS_KEY_ID',
15
+ 'AWS_SECRET_ACCESS_KEY',
16
+ 'AWS_REGION',
17
+ ];
18
+ export const Walg = ({ values, onChange, disabled, envsKeyErrors }) => {
19
+ const onEnvsChange = (envs) => onChange('physicalEnvs', envs);
20
+ return (_jsxs(Box, { mt: 2, "data-testid": "walg-form", children: [_jsx(Typography, { variant: "subtitle1", children: "WAL-G" }), _jsx(Box, { mt: 1, children: _jsx(TextField, { fullWidth: true, label: "Backup name", placeholder: "LATEST", value: values.physicalWalgBackupName, disabled: disabled, onChange: (e) => onChange('physicalWalgBackupName', e.target.value), helperText: 'Which backup to restore. "LATEST" picks the most recent.', inputProps: { 'data-testid': 'walg-backup-name' } }) }), _jsx(Box, { mt: 2, children: _jsx(Typography, { variant: "caption", color: "textSecondary", children: "Storage backend, bucket, prefix, and credentials all live in the envs map. Do not paste credentials into the backup name field or any URL." }) }), _jsx(EnvsEditor, { envs: values.physicalEnvs, onChange: onEnvsChange, suggestions: WALG_ENV_SUGGESTIONS, disabled: disabled, keyErrors: envsKeyErrors })] }));
21
+ };
@@ -0,0 +1,10 @@
1
+ /// <reference types="react" />
2
+ import { FormValues } from '../useForm';
3
+ declare type Props = {
4
+ values: FormValues;
5
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void;
6
+ disabled?: boolean;
7
+ envsKeyErrors?: (string | undefined)[];
8
+ };
9
+ export declare const PhysicalMode: ({ values, onChange, disabled, envsKeyErrors, }: Props) => JSX.Element;
10
+ export {};
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FormControl, FormControlLabel, Radio, RadioGroup, Typography, } from '@material-ui/core';
3
+ import Box from '@mui/material/Box';
4
+ import { PgBackRest } from './PgBackRest';
5
+ import { Sync } from './Sync';
6
+ import { Walg } from './Walg';
7
+ // Physical-mode restore tool selector + sub-form. Two values surface in the
8
+ // UI: WAL-G and pgBackRest, the two tool values the engine accepts outside
9
+ // customTool. pg_basebackup is invoked via the customTool path and remains
10
+ // YAML-only; when the loaded projection has `tool: customTool` we render a
11
+ // banner instead of the sub-form, keeping the user from accidentally wiping a
12
+ // hand-edited customTool config.
13
+ export const PhysicalMode = ({ values, onChange, disabled, envsKeyErrors, }) => {
14
+ const tool = values.physicalTool;
15
+ const isCustomTool = tool === 'customTool';
16
+ return (_jsxs(Box, { mt: 1, "data-testid": "physical-mode", children: [_jsx(Typography, { variant: "subtitle2", children: "Restore tool" }), isCustomTool ? (_jsx(Box, { mt: 1, "data-testid": "physical-custom-tool-banner", children: _jsx(Typography, { variant: "body2", color: "textSecondary", children: "This config uses a custom restore tool (e.g. pg_basebackup). The UI cannot edit customTool configurations \u2014 edit the YAML directly." }) })) : (_jsx(FormControl, { children: _jsxs(RadioGroup, { row: true, value: tool, onChange: (_, value) => onChange('physicalTool', value), "aria-label": "physical restore tool", children: [_jsx(FormControlLabel, { value: "walg", control: _jsx(Radio, { disabled: disabled }), label: "WAL-G" }), _jsx(FormControlLabel, { value: "pgbackrest", control: _jsx(Radio, { disabled: disabled }), label: "pgBackRest" })] }) })), !isCustomTool && tool === 'walg' && (_jsx(Walg, { values: values, onChange: onChange, disabled: disabled, envsKeyErrors: envsKeyErrors })), !isCustomTool && tool === 'pgbackrest' && (_jsx(PgBackRest, { values: values, onChange: onChange, disabled: disabled, envsKeyErrors: envsKeyErrors })), !isCustomTool && tool && (_jsx(Sync, { values: values, onChange: onChange, disabled: disabled }))] }));
17
+ };
@@ -0,0 +1,11 @@
1
+ /// <reference types="react" />
2
+ import { ProposedConfig } from '@postgres.ai/shared/types/api/endpoints/probeSource';
3
+ declare type Props = {
4
+ proposed: ProposedConfig;
5
+ applying: boolean;
6
+ applyError: string | null;
7
+ onApply: () => void;
8
+ onEdit: () => void;
9
+ };
10
+ export declare const PreviewCard: ({ proposed, applying, applyError, onApply, onEdit, }: Props) => JSX.Element;
11
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Link, Typography } from '@material-ui/core';
3
+ import Box from '@mui/material/Box';
4
+ import { Spinner } from '@postgres.ai/shared/components/Spinner';
5
+ import { providerKeyToImage } from '../configOptions';
6
+ const Field = ({ label, value }) => (_jsxs(Box, { display: "flex", mb: 0.5, children: [_jsx(Box, { minWidth: 220, fontWeight: 600, children: label }), _jsx(Box, { style: { wordBreak: 'break-all' }, children: value })] }));
7
+ const Callout = ({ children }) => (_jsx(Box, { mt: 1, p: 1, bgcolor: "#fff8e1", borderLeft: "4px solid #f5a623", fontSize: 13, children: children }));
8
+ export const PreviewCard = ({ proposed, applying, applyError, onApply, onEdit, }) => {
9
+ var _a, _b;
10
+ const mapping = providerKeyToImage(proposed.dockerImage, proposed.pgMajorVersion);
11
+ const resolvedTag = proposed.dockerTag || mapping.defaultTag || '(latest)';
12
+ const tuningEntries = Object.entries((_a = proposed.queryTuning) !== null && _a !== void 0 ? _a : {});
13
+ return (_jsxs(Box, { mt: 2, p: 2, border: "1px solid #e0e0e0", borderRadius: 4, "data-testid": "preview-card", children: [_jsx(Typography, { variant: "h6", children: "Proposed configuration" }), _jsxs(Box, { mt: 2, children: [_jsx(Field, { label: "Detected provider", value: proposed.detectedProvider || 'unknown' }), _jsx(Field, { label: "Docker image", value: mapping.imageType }), _jsx(Field, { label: "Docker tag", value: resolvedTag }), _jsx(Field, { label: "Postgres major version", value: String(proposed.pgMajorVersion || 'unknown') }), _jsx(Field, { label: "Databases", value: ((_b = proposed.databases) === null || _b === void 0 ? void 0 : _b.join(', ')) || '(none)' }), _jsx(Field, { label: "shared_buffers", value: proposed.sharedBuffers || '' }), _jsx(Field, { label: "shared_preload_libraries", value: proposed.sharedPreloadLibraries || '' })] }), tuningEntries.length > 0 && (_jsxs(Box, { mt: 2, children: [_jsx(Typography, { variant: "subtitle2", children: "Query tuning" }), _jsx(Box, { component: "table", mt: 1, style: { borderCollapse: 'collapse' }, children: _jsx("tbody", { children: tuningEntries.map(([k, v]) => (_jsxs("tr", { children: [_jsx("td", { style: { padding: '2px 16px 2px 0', fontWeight: 600 }, children: k }), _jsx("td", { style: { padding: '2px 0' }, children: v })] }, k))) }) })] })), _jsxs(Box, { mt: 2, children: [(mapping.fallback || proposed.detectedProvider === 'generic') && (_jsx(Callout, { children: "Could not detect a managed cloud provider; using the generic Postgres image. Switch to Expert mode if your source runs on a managed service and we missed it." })), !proposed.memoryProbed && (_jsxs(Callout, { children: ["Could not detect host memory; ", _jsx("code", { children: "shared_buffers" }), " is set to a 1\u00A0GB safe default. Adjust in Expert mode if your host has more RAM."] })), _jsx(Callout, { children: "Query tuning is copied from your source. If you use the RDS refresh tool, these values may not match production \u2014 review in Expert mode after the first retrieval run." }), _jsxs(Callout, { children: ["We'll ship ", _jsx("code", { children: proposed.sharedPreloadLibraries }), ". If the chosen image does not bundle one of these libraries, the clone container will fail to start with a \"could not load library\" error \u2014 check ", _jsx("code", { children: "docker logs dblab_server" }), " after Apply."] })] }), _jsxs(Box, { mt: 2, display: "flex", alignItems: "center", children: [_jsxs(Button, { variant: "contained", color: "secondary", onClick: onApply, disabled: applying, "data-testid": "preview-apply", children: ["Apply & start retrieval", applying && _jsx(Spinner, { size: "sm" })] }), _jsx(Box, { ml: 2, children: _jsx(Link, { component: "button", type: "button", onClick: onEdit, "data-testid": "preview-edit", children: "Edit before applying" }) })] }), applyError && (_jsx(Box, { mt: 1, color: "#d32f2f", fontSize: 13, "data-testid": "apply-error", children: applyError }))] }));
14
+ };
@@ -0,0 +1,14 @@
1
+ /// <reference types="react" />
2
+ import { ProposedConfig } from '@postgres.ai/shared/types/api/endpoints/probeSource';
3
+ import { FormValues } from '../useForm';
4
+ declare type Props = {
5
+ instanceId: string;
6
+ disabled?: boolean;
7
+ onApplied?: () => void;
8
+ onEdit?: (proposed: ProposedConfig, password: string) => void;
9
+ };
10
+ export declare const buildProjectionFromProposed: (proposed: ProposedConfig, password: string) => FormValues;
11
+ export declare const SimpleMode: (({ instanceId, disabled, onApplied, onEdit }: Props) => JSX.Element) & {
12
+ displayName: string;
13
+ };
14
+ export {};
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { observer } from 'mobx-react-lite';
4
+ import { Button, TextField, Typography } from '@material-ui/core';
5
+ import Box from '@mui/material/Box';
6
+ import { Spinner } from '@postgres.ai/shared/components/Spinner';
7
+ import { useStores } from '@postgres.ai/shared/pages/Instance/context';
8
+ import { providerKeyToImage } from '../configOptions';
9
+ import { genericImagePrefix } from '../dockerCatalog';
10
+ import { PreviewCard } from './PreviewCard';
11
+ // Translates a ProposedConfig from POST /admin/probe-source into the
12
+ // FormValues shape the Expert form uses. Both Apply (→ updateConfig) and
13
+ // Edit (→ formik.setValues) consume it, so the engine receives the same
14
+ // projection regardless of which flow the user picks.
15
+ export const buildProjectionFromProposed = (proposed, password) => {
16
+ const mapping = providerKeyToImage(proposed.dockerImage, proposed.pgMajorVersion);
17
+ const tag = proposed.dockerTag || mapping.defaultTag || '';
18
+ const isGeneric = mapping.imageType === 'Generic Postgres';
19
+ const dockerPath = isGeneric ? `${genericImagePrefix}:${tag}` : '';
20
+ return {
21
+ debug: false,
22
+ dockerImage: isGeneric
23
+ ? String(proposed.pgMajorVersion)
24
+ : mapping.imageType,
25
+ dockerImageType: mapping.imageType,
26
+ dockerPath,
27
+ dockerTag: tag,
28
+ sharedBuffers: proposed.sharedBuffers,
29
+ sharedPreloadLibraries: proposed.sharedPreloadLibraries,
30
+ // tuningParams is typed as string on FormValues but updateConfig.ts
31
+ // spreads it as a key-value object; cast matches the Expert form's
32
+ // formatTuningParamsToObj(...) as unknown as string pattern.
33
+ tuningParams: { ...proposed.queryTuning },
34
+ timetable: '0 0 * * *',
35
+ dbname: proposed.source.dbname,
36
+ host: proposed.source.host,
37
+ port: String(proposed.source.port),
38
+ username: proposed.source.username,
39
+ password,
40
+ databases: proposed.databases.join(' '),
41
+ dumpParallelJobs: '',
42
+ dumpIgnoreErrors: false,
43
+ restoreParallelJobs: '',
44
+ restoreIgnoreErrors: false,
45
+ restoreConfigs: '',
46
+ pgDumpCustomOptions: '',
47
+ pgRestoreCustomOptions: '',
48
+ retrievalMode: 'logical',
49
+ physicalTool: '',
50
+ physicalDockerImage: '',
51
+ physicalSyncEnabled: false,
52
+ physicalWalgBackupName: '',
53
+ physicalPgbackrestStanza: '',
54
+ physicalPgbackrestDelta: false,
55
+ physicalEnvs: [],
56
+ };
57
+ };
58
+ export const SimpleMode = observer(({ instanceId, disabled, onApplied, onEdit }) => {
59
+ const stores = useStores();
60
+ const main = stores.main;
61
+ const [url, setUrl] = useState('');
62
+ const [password, setPassword] = useState('');
63
+ const [probing, setProbing] = useState(false);
64
+ const [probeError, setProbeError] = useState(null);
65
+ const [proposed, setProposed] = useState(null);
66
+ const [applying, setApplying] = useState(false);
67
+ const [applyError, setApplyError] = useState(null);
68
+ const onDetect = async () => {
69
+ setProbing(true);
70
+ setProbeError(null);
71
+ setProposed(null);
72
+ setApplyError(null);
73
+ const result = await main.probeSource({ url, password });
74
+ setProbing(false);
75
+ if (!result) {
76
+ setProbeError('Probe is not available on this instance.');
77
+ return;
78
+ }
79
+ if (result.error) {
80
+ setProbeError(result.error.message);
81
+ return;
82
+ }
83
+ if (result.response)
84
+ setProposed(result.response);
85
+ };
86
+ const onApply = async () => {
87
+ var _a;
88
+ if (!proposed)
89
+ return;
90
+ setApplying(true);
91
+ setApplyError(null);
92
+ const projection = buildProjectionFromProposed(proposed, password);
93
+ const response = await main.updateConfig(projection, instanceId);
94
+ setApplying(false);
95
+ if (!response) {
96
+ setApplyError((_a = main.configError) !== null && _a !== void 0 ? _a : 'Could not apply the proposed configuration.');
97
+ return;
98
+ }
99
+ onApplied === null || onApplied === void 0 ? void 0 : onApplied();
100
+ };
101
+ const handleEdit = () => {
102
+ if (proposed)
103
+ onEdit === null || onEdit === void 0 ? void 0 : onEdit(proposed, password);
104
+ };
105
+ const canDetect = !probing && url.trim().length > 0 && password.length > 0 && !disabled;
106
+ return (_jsxs(Box, { mt: 2, mb: 2, "data-testid": "simple-mode", children: [!proposed && (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", children: "Simple configuration" }), _jsx(Typography, { variant: "body2", children: "Paste your source connection string and password. We'll probe the source, propose a configuration, and let you review before starting retrieval." }), _jsx(Box, { mt: 2, children: _jsx(TextField, { label: "Connection string", placeholder: "postgres://user@host:5432/dbname", value: url, onChange: (e) => setUrl(e.target.value), fullWidth: true, disabled: probing || disabled, inputProps: { 'data-testid': 'simple-url' } }) }), _jsx(Box, { mt: 2, children: _jsx(TextField, { label: "Password", type: "password", value: password, onChange: (e) => setPassword(e.target.value), fullWidth: true, disabled: probing || disabled, inputProps: { 'data-testid': 'simple-password' } }) }), _jsx(Box, { mt: 2, children: _jsxs(Button, { variant: "contained", color: "secondary", onClick: onDetect, disabled: !canDetect, "data-testid": "simple-detect", children: ["Detect & preview", probing && _jsx(Spinner, { size: "sm" })] }) }), probeError && (_jsx(Box, { mt: 1, color: "#d32f2f", fontSize: 13, "data-testid": "probe-error", children: probeError }))] })), proposed && (_jsx(PreviewCard, { proposed: proposed, applying: applying, applyError: applyError, onApply: onApply, onEdit: handleEdit }))] }));
107
+ });
@@ -0,0 +1,2 @@
1
+ export declare type ConfigMode = 'simple' | 'expert';
2
+ export declare const getInitialConfigMode: (host: string | undefined, retrievalMode?: string) => ConfigMode;
@@ -0,0 +1,7 @@
1
+ // Picks the default Configuration tab when the page first finishes loading
2
+ // the server config. An unconfigured instance lands on Simple; an instance
3
+ // that already has a source host filled in OR a non-logical retrieval mode
4
+ // configured lands on Expert so the user sees the form they previously
5
+ // interacted with. Physical-mode users do not have a host field but still
6
+ // need to land on Expert because Simple-mode targets logical retrieval.
7
+ export const getInitialConfigMode = (host, retrievalMode) => (host || retrievalMode === 'physical' ? 'expert' : 'simple');
@@ -1,3 +1,9 @@
1
+ export declare type ProviderImageMapping = {
2
+ imageType: string;
3
+ defaultTag?: string;
4
+ fallback: boolean;
5
+ };
6
+ export declare const providerKeyToImage: (providerKey: string, pgMajorVersion: number) => ProviderImageMapping;
1
7
  export declare const dockerImageOptions: {
2
8
  name: string;
3
9
  type: string;
@@ -1,3 +1,51 @@
1
+ import { dockerImagesConfig } from './dockerCatalog';
2
+ // Mapping from the engine probe's provider key (probe.Provider) to a
3
+ // dockerImageOptions.type value the form already understands. Keys must
4
+ // match the values defined in engine/internal/retrieval/probe/provider.go
5
+ // (ProviderRDS, ProviderAurora, etc.) — see Task 18 in the plan.
6
+ const providerKeyToImageType = {
7
+ generic: 'Generic Postgres',
8
+ rds: 'rds',
9
+ aurora: 'aurora',
10
+ cloudsql: 'google-cloud-sql',
11
+ supabase: 'supabase',
12
+ heroku: 'heroku',
13
+ timescale: 'timescale-cloud',
14
+ };
15
+ // Reads the most recent tag for a given PG major from the shared catalog.
16
+ // SE images (rds, aurora, etc.) require the platform-only getSeImages call
17
+ // and have no entry here — they return defaultTag: undefined.
18
+ const genericDefaultTag = (pgMajorVersion) => {
19
+ if (!pgMajorVersion)
20
+ return undefined;
21
+ const version = String(pgMajorVersion);
22
+ const tags = dockerImagesConfig[version];
23
+ if (!tags || tags.length === 0)
24
+ return undefined;
25
+ return `${version}-${tags[0]}`;
26
+ };
27
+ // Resolves a probe provider key to a concrete docker image type the
28
+ // Configuration form can write into the projection. Unknown keys (including
29
+ // "azure", which has no matching SE image today) fall back to the generic
30
+ // Postgres image and set fallback=true so the UI can warn.
31
+ export const providerKeyToImage = (providerKey, pgMajorVersion) => {
32
+ const known = providerKeyToImageType[providerKey];
33
+ if (!known) {
34
+ return {
35
+ imageType: 'Generic Postgres',
36
+ defaultTag: genericDefaultTag(pgMajorVersion),
37
+ fallback: true,
38
+ };
39
+ }
40
+ if (known === 'Generic Postgres') {
41
+ return {
42
+ imageType: 'Generic Postgres',
43
+ defaultTag: genericDefaultTag(pgMajorVersion),
44
+ fallback: false,
45
+ };
46
+ }
47
+ return { imageType: known, defaultTag: undefined, fallback: false };
48
+ };
1
49
  export const dockerImageOptions = [
2
50
  {
3
51
  name: 'Generic PostgreSQL (postgresai/extended-postgres)',
@@ -0,0 +1,20 @@
1
+ export declare type ConnectionFields = {
2
+ host: string;
3
+ port: string;
4
+ username: string;
5
+ dbname: string;
6
+ };
7
+ export declare type ParseResult = {
8
+ fields: ConnectionFields;
9
+ portWasExplicit: boolean;
10
+ };
11
+ export declare class ConnectionStringError extends Error {
12
+ }
13
+ export declare const ErrPasswordInConnectionString = "Password must be entered in the Password field, not the connection string.";
14
+ export declare const ErrMultiHost = "Multi-host connection strings are not supported.";
15
+ export declare const ErrInvalidConnectionString = "Could not parse the connection string.";
16
+ export declare const connectionStringToFields: (s: string) => ParseResult;
17
+ export declare type SerializeOptions = {
18
+ omitDefaultPort?: boolean;
19
+ };
20
+ export declare const connectionStringFromFields: (fields: ConnectionFields, opts?: SerializeOptions) => string;
@@ -0,0 +1,129 @@
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
+ export class ConnectionStringError extends Error {
11
+ }
12
+ export const ErrPasswordInConnectionString = 'Password must be entered in the Password field, not the connection string.';
13
+ export const ErrMultiHost = 'Multi-host connection strings are not supported.';
14
+ export const ErrInvalidConnectionString = 'Could not parse the connection string.';
15
+ const URI_SCHEMES = ['postgres:', 'postgresql:'];
16
+ const isUri = (s) => URI_SCHEMES.some((p) => s.toLowerCase().startsWith(p));
17
+ const stripBrackets = (h) => (h.startsWith('[') && h.endsWith(']') ? h.slice(1, -1) : h);
18
+ const parseUri = (s) => {
19
+ let url;
20
+ try {
21
+ url = new URL(s);
22
+ }
23
+ catch {
24
+ throw new ConnectionStringError(ErrInvalidConnectionString);
25
+ }
26
+ if (url.password)
27
+ throw new ConnectionStringError(ErrPasswordInConnectionString);
28
+ if (!url.hostname)
29
+ throw new ConnectionStringError(ErrInvalidConnectionString);
30
+ if (url.hostname.includes(','))
31
+ throw new ConnectionStringError(ErrMultiHost);
32
+ const portWasExplicit = url.port !== '';
33
+ const path = url.pathname.startsWith('/') ? url.pathname.slice(1) : url.pathname;
34
+ const dbname = path.includes(',') ? '' : decodeURIComponent(path);
35
+ return {
36
+ fields: {
37
+ host: stripBrackets(url.hostname),
38
+ port: portWasExplicit ? url.port : '5432',
39
+ username: url.username ? decodeURIComponent(url.username) : '',
40
+ dbname,
41
+ },
42
+ portWasExplicit,
43
+ };
44
+ };
45
+ // Tokenizer for libpq DSN form: key=value pairs, values may be single-quoted
46
+ // when they contain spaces (libpq spec). Throws on malformed input.
47
+ const tokenizeDsn = (s) => {
48
+ const out = {};
49
+ let i = 0;
50
+ while (i < s.length) {
51
+ // skip leading whitespace
52
+ while (i < s.length && /\s/.test(s[i]))
53
+ i++;
54
+ if (i >= s.length)
55
+ break;
56
+ // read key
57
+ const keyStart = i;
58
+ while (i < s.length && s[i] !== '=' && !/\s/.test(s[i]))
59
+ i++;
60
+ if (i >= s.length || s[i] !== '=')
61
+ throw new ConnectionStringError(ErrInvalidConnectionString);
62
+ const key = s.slice(keyStart, i);
63
+ i++; // skip '='
64
+ // read value (possibly quoted)
65
+ let value = '';
66
+ if (s[i] === "'") {
67
+ i++;
68
+ while (i < s.length && s[i] !== "'") {
69
+ if (s[i] === '\\' && i + 1 < s.length) {
70
+ value += s[i + 1];
71
+ i += 2;
72
+ continue;
73
+ }
74
+ value += s[i];
75
+ i++;
76
+ }
77
+ if (i >= s.length)
78
+ throw new ConnectionStringError(ErrInvalidConnectionString);
79
+ i++; // skip closing quote
80
+ }
81
+ else {
82
+ while (i < s.length && !/\s/.test(s[i])) {
83
+ value += s[i];
84
+ i++;
85
+ }
86
+ }
87
+ out[key.toLowerCase()] = value;
88
+ }
89
+ return out;
90
+ };
91
+ const parseDsn = (s) => {
92
+ var _a, _b, _c, _d, _e, _f, _g;
93
+ const tokens = tokenizeDsn(s);
94
+ if (tokens.password)
95
+ throw new ConnectionStringError(ErrPasswordInConnectionString);
96
+ const host = (_b = (_a = tokens.host) !== null && _a !== void 0 ? _a : tokens.hostaddr) !== null && _b !== void 0 ? _b : '';
97
+ if (host.includes(','))
98
+ throw new ConnectionStringError(ErrMultiHost);
99
+ const portRaw = (_c = tokens.port) !== null && _c !== void 0 ? _c : '';
100
+ const portWasExplicit = portRaw !== '';
101
+ return {
102
+ fields: {
103
+ host,
104
+ port: portWasExplicit ? portRaw : '5432',
105
+ username: (_e = (_d = tokens.user) !== null && _d !== void 0 ? _d : tokens.username) !== null && _e !== void 0 ? _e : '',
106
+ dbname: (_g = (_f = tokens.dbname) !== null && _f !== void 0 ? _f : tokens.database) !== null && _g !== void 0 ? _g : '',
107
+ },
108
+ portWasExplicit,
109
+ };
110
+ };
111
+ export const connectionStringToFields = (s) => {
112
+ const trimmed = s.trim();
113
+ if (!trimmed)
114
+ throw new ConnectionStringError(ErrInvalidConnectionString);
115
+ if (isUri(trimmed))
116
+ return parseUri(trimmed);
117
+ if (trimmed.includes('='))
118
+ return parseDsn(trimmed);
119
+ throw new ConnectionStringError(ErrInvalidConnectionString);
120
+ };
121
+ // formatHost wraps IPv6 literals in brackets so the resulting URI parses cleanly.
122
+ const formatHost = (host) => (host.includes(':') ? `[${host}]` : host);
123
+ export const connectionStringFromFields = (fields, opts = {}) => {
124
+ const userPart = fields.username ? `${encodeURIComponent(fields.username)}@` : '';
125
+ const showPort = fields.port && !(opts.omitDefaultPort && fields.port === '5432');
126
+ const portPart = showPort ? `:${fields.port}` : '';
127
+ const dbPart = fields.dbname ? `/${encodeURIComponent(fields.dbname)}` : '';
128
+ return `postgres://${userPart}${formatHost(fields.host)}${portPart}${dbPart}`;
129
+ };
@@ -0,0 +1,2 @@
1
+ export declare const genericImagePrefix = "postgresai/extended-postgres";
2
+ export declare const dockerImagesConfig: Record<string, string[]>;
@@ -0,0 +1,19 @@
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
+ export const genericImagePrefix = 'postgresai/extended-postgres';
5
+ // Predefined Docker image catalog for the Generic Postgres image. If a
6
+ // user's config references a tag not listed here, createEnhancedDockerImages
7
+ // in utils/index.ts appends it at runtime.
8
+ export const dockerImagesConfig = {
9
+ '9.6': ['0.5.3', '0.5.2', '0.5.1'],
10
+ '10': ['0.5.3', '0.5.2', '0.5.1'],
11
+ '11': ['0.5.3', '0.5.2', '0.5.1'],
12
+ '12': ['0.5.3', '0.5.2', '0.5.1'],
13
+ '13': ['0.5.3', '0.5.2', '0.5.1'],
14
+ '14': ['0.5.3', '0.5.2', '0.5.1'],
15
+ '15': ['0.5.3', '0.5.2', '0.5.1'],
16
+ '16': ['0.5.3', '0.5.2', '0.5.1'],
17
+ '17': ['0.5.3', '0.5.2', '0.5.1'],
18
+ '18': ['0.6.1'],
19
+ };
@@ -1,9 +1,8 @@
1
1
  /// <reference types="react" />
2
- export declare const Configuration: (({ instanceId, switchActiveTab, reload, isConfigurationActive, disableConfigModification, }: {
2
+ export declare const Configuration: (({ instanceId, switchActiveTab, reload, disableConfigModification, }: {
3
3
  instanceId: string;
4
4
  switchActiveTab: (_: null, activeTab: number) => void;
5
5
  reload: () => void;
6
- isConfigurationActive: boolean;
7
6
  disableConfigModification?: boolean | undefined;
8
7
  }) => JSX.Element) & {
9
8
  displayName: string;