@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.
Files changed (137) hide show
  1. package/.gitlab-ci.yml +7 -3
  2. package/components/UpgradeCloneModal/index.tsx +145 -0
  3. package/dist/components/UpgradeCloneModal/index.d.ts +11 -0
  4. package/dist/components/UpgradeCloneModal/index.js +75 -0
  5. package/dist/package.json +2 -2
  6. package/dist/pages/Branches/Branch/index.js +27 -2
  7. package/dist/pages/Branches/Branch/stores/Main.d.ts +5 -0
  8. package/dist/pages/Branches/Branch/stores/Main.js +29 -0
  9. package/dist/pages/Branches/components/BranchesTable/index.js +2 -2
  10. package/dist/pages/Branches/index.js +10 -1
  11. package/dist/pages/Clone/Status/index.js +3 -0
  12. package/dist/pages/Clone/index.js +12 -4
  13. package/dist/pages/Clone/stores/Main.d.ts +9 -1
  14. package/dist/pages/Clone/stores/Main.js +40 -3
  15. package/dist/pages/CreateBranch/stores/Main.d.ts +3 -0
  16. package/dist/pages/CreateClone/index.js +2 -0
  17. package/dist/pages/CreateClone/stores/Main.d.ts +5 -1
  18. package/dist/pages/CreateClone/stores/Main.js +8 -3
  19. package/dist/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.d.ts +11 -0
  20. package/dist/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.js +24 -0
  21. package/dist/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.d.ts +10 -0
  22. package/dist/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.js +29 -0
  23. package/dist/pages/Instance/Configuration/PhysicalMode/Sync/index.d.ts +9 -0
  24. package/dist/pages/Instance/Configuration/PhysicalMode/Sync/index.js +14 -0
  25. package/dist/pages/Instance/Configuration/PhysicalMode/Walg/index.d.ts +10 -0
  26. package/dist/pages/Instance/Configuration/PhysicalMode/Walg/index.js +21 -0
  27. package/dist/pages/Instance/Configuration/PhysicalMode/index.d.ts +10 -0
  28. package/dist/pages/Instance/Configuration/PhysicalMode/index.js +17 -0
  29. package/dist/pages/Instance/Configuration/SimpleMode/PreviewCard.d.ts +13 -0
  30. package/dist/pages/Instance/Configuration/SimpleMode/PreviewCard.js +14 -0
  31. package/dist/pages/Instance/Configuration/SimpleMode/index.d.ts +19 -0
  32. package/dist/pages/Instance/Configuration/SimpleMode/index.js +166 -0
  33. package/dist/pages/Instance/Configuration/configMode.d.ts +2 -0
  34. package/dist/pages/Instance/Configuration/configMode.js +7 -0
  35. package/dist/pages/Instance/Configuration/configOptions.d.ts +16 -0
  36. package/dist/pages/Instance/Configuration/configOptions.js +71 -0
  37. package/dist/pages/Instance/Configuration/connectionString.d.ts +20 -0
  38. package/dist/pages/Instance/Configuration/connectionString.js +129 -0
  39. package/dist/pages/Instance/Configuration/dockerCatalog.d.ts +2 -0
  40. package/dist/pages/Instance/Configuration/dockerCatalog.js +19 -0
  41. package/dist/pages/Instance/Configuration/index.d.ts +1 -2
  42. package/dist/pages/Instance/Configuration/index.js +115 -86
  43. package/dist/pages/Instance/Configuration/useForm.d.ts +20 -0
  44. package/dist/pages/Instance/Configuration/useForm.js +126 -5
  45. package/dist/pages/Instance/Configuration/utils/index.js +1 -17
  46. package/dist/pages/Instance/Info/Snapshots/Calendar/Day/index.js +3 -3
  47. package/dist/pages/Instance/Info/Snapshots/Calendar/utils.d.ts +3 -0
  48. package/dist/pages/Instance/Info/Snapshots/utils.d.ts +6 -0
  49. package/dist/pages/Instance/Snapshots/components/SnapshotsList/index.js +11 -1
  50. package/dist/pages/Instance/Snapshots/index.js +11 -3
  51. package/dist/pages/Instance/Snapshots/utils/index.d.ts +3 -0
  52. package/dist/pages/Instance/Tabs/styles.js +6 -6
  53. package/dist/pages/Instance/index.js +1 -3
  54. package/dist/pages/Instance/stores/Main.d.ts +22 -2
  55. package/dist/pages/Instance/stores/Main.js +26 -9
  56. package/dist/pages/Snapshots/Snapshot/index.js +28 -2
  57. package/dist/pages/Snapshots/Snapshot/stores/Main.d.ts +5 -0
  58. package/dist/pages/Snapshots/Snapshot/stores/Main.js +29 -0
  59. package/dist/stores/Snapshots.d.ts +1 -1
  60. package/dist/stores/Snapshots.js +12 -6
  61. package/dist/types/api/endpoints/getBranches.d.ts +6 -0
  62. package/dist/types/api/endpoints/probeSource.d.ts +32 -0
  63. package/dist/types/api/endpoints/probeSource.js +1 -0
  64. package/dist/types/api/endpoints/testDbSource.d.ts +3 -1
  65. package/dist/types/api/endpoints/testDbSource.js +6 -0
  66. package/dist/types/api/endpoints/updateBranch.d.ts +11 -0
  67. package/dist/types/api/endpoints/updateBranch.js +1 -0
  68. package/dist/types/api/endpoints/updateSnapshot.d.ts +11 -0
  69. package/dist/types/api/endpoints/updateSnapshot.js +1 -0
  70. package/dist/types/api/endpoints/upgradeClone.d.ts +8 -0
  71. package/dist/types/api/endpoints/upgradeClone.js +1 -0
  72. package/dist/types/api/entities/branchSnapshots.d.ts +3 -0
  73. package/dist/types/api/entities/clone.d.ts +9 -2
  74. package/dist/types/api/entities/config.d.ts +32 -0
  75. package/dist/types/api/entities/config.js +45 -18
  76. package/dist/types/api/entities/instance.d.ts +11 -1
  77. package/dist/types/api/entities/instanceState.d.ts +16 -1
  78. package/dist/types/api/entities/snapshot.d.ts +6 -0
  79. package/dist/utils/api.d.ts +1 -1
  80. package/dist/utils/api.js +6 -0
  81. package/dist/utils/clone.d.ts +3 -1
  82. package/dist/utils/clone.js +7 -1
  83. package/dist/utils/clonePoller.d.ts +12 -0
  84. package/dist/utils/clonePoller.js +41 -0
  85. package/dist/utils/date.js +12 -12
  86. package/dist/utils/protection.d.ts +5 -0
  87. package/dist/utils/protection.js +23 -0
  88. package/package.json +2 -2
  89. package/pages/Branches/Branch/index.tsx +70 -0
  90. package/pages/Branches/Branch/stores/Main.ts +40 -0
  91. package/pages/Branches/components/BranchesTable/index.tsx +2 -0
  92. package/pages/Branches/index.tsx +13 -1
  93. package/pages/Clone/Status/index.tsx +3 -0
  94. package/pages/Clone/index.tsx +58 -0
  95. package/pages/Clone/stores/Main.ts +51 -8
  96. package/pages/CreateClone/index.tsx +3 -0
  97. package/pages/CreateClone/stores/Main.ts +10 -8
  98. package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.tsx +129 -0
  99. package/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.tsx +89 -0
  100. package/pages/Instance/Configuration/PhysicalMode/Sync/index.tsx +64 -0
  101. package/pages/Instance/Configuration/PhysicalMode/Walg/index.tsx +63 -0
  102. package/pages/Instance/Configuration/PhysicalMode/index.tsx +93 -0
  103. package/pages/Instance/Configuration/SimpleMode/PreviewCard.tsx +176 -0
  104. package/pages/Instance/Configuration/SimpleMode/index.tsx +303 -0
  105. package/pages/Instance/Configuration/configMode.ts +12 -0
  106. package/pages/Instance/Configuration/configOptions.ts +111 -0
  107. package/pages/Instance/Configuration/connectionString.ts +157 -0
  108. package/pages/Instance/Configuration/dockerCatalog.ts +21 -0
  109. package/pages/Instance/Configuration/index.tsx +151 -49
  110. package/pages/Instance/Configuration/useForm.ts +155 -5
  111. package/pages/Instance/Configuration/utils/index.ts +1 -17
  112. package/pages/Instance/Info/Snapshots/Calendar/Day/index.tsx +3 -3
  113. package/pages/Instance/Snapshots/components/SnapshotsList/index.tsx +21 -1
  114. package/pages/Instance/Snapshots/index.tsx +15 -5
  115. package/pages/Instance/Tabs/styles.ts +7 -7
  116. package/pages/Instance/index.tsx +0 -4
  117. package/pages/Instance/stores/Main.ts +29 -7
  118. package/pages/Snapshots/Snapshot/index.tsx +71 -0
  119. package/pages/Snapshots/Snapshot/stores/Main.ts +40 -0
  120. package/postgres.ai-shared-4.2.0.tgz +0 -0
  121. package/stores/Snapshots.ts +16 -7
  122. package/types/api/endpoints/getBranches.ts +3 -0
  123. package/types/api/endpoints/probeSource.ts +34 -0
  124. package/types/api/endpoints/testDbSource.ts +10 -1
  125. package/types/api/endpoints/updateBranch.ts +11 -0
  126. package/types/api/endpoints/updateSnapshot.ts +11 -0
  127. package/types/api/endpoints/upgradeClone.ts +8 -0
  128. package/types/api/entities/clone.ts +13 -1
  129. package/types/api/entities/config.ts +46 -0
  130. package/types/api/entities/instanceState.ts +6 -0
  131. package/types/api/entities/snapshot.ts +3 -0
  132. package/utils/api.ts +8 -0
  133. package/utils/clone.ts +7 -1
  134. package/utils/clonePoller.ts +47 -0
  135. package/utils/date.ts +19 -14
  136. package/utils/protection.ts +25 -0
  137. package/postgres.ai-shared-4.1.3.tgz +0 -0
@@ -0,0 +1,129 @@
1
+ import { Button, IconButton, TextField, Typography } from '@material-ui/core'
2
+ import Box from '@mui/material/Box'
3
+
4
+ import { PhysicalEnv } from '../../useForm'
5
+
6
+ type Props = {
7
+ envs: PhysicalEnv[]
8
+ onChange: (envs: PhysicalEnv[]) => void
9
+ suggestions?: string[]
10
+ disabled?: boolean
11
+ keyErrors?: (string | undefined)[]
12
+ }
13
+
14
+ // EnvsEditor renders a rows-of-key/value editor with add/remove and a
15
+ // click-to-add suggestion list. Engine consumes envs as a free-form map
16
+ // (physical.go:76, CopyOptions.Envs map[string]string); the UI is a thin
17
+ // surface over that map.
18
+ export const EnvsEditor = ({
19
+ envs,
20
+ onChange,
21
+ suggestions = [],
22
+ disabled,
23
+ keyErrors = [],
24
+ }: Props) => {
25
+ const updateRow = (i: number, patch: Partial<PhysicalEnv>) => {
26
+ const next = envs.slice()
27
+ next[i] = { ...next[i], ...patch }
28
+ onChange(next)
29
+ }
30
+
31
+ const removeRow = (i: number) => {
32
+ const next = envs.slice()
33
+ next.splice(i, 1)
34
+ onChange(next)
35
+ }
36
+
37
+ const addRow = (key = '') => {
38
+ onChange([...envs, { key, value: '' }])
39
+ }
40
+
41
+ const usedKeys = new Set(envs.map((e) => e.key))
42
+
43
+ return (
44
+ <Box mt={1} data-testid="envs-editor">
45
+ <Typography variant="subtitle2">Environment variables</Typography>
46
+ {envs.length === 0 ? (
47
+ <Box mt={1} mb={1}>
48
+ <Typography variant="caption" color="textSecondary">
49
+ No environment variables set. Use suggestions below or click "Add".
50
+ </Typography>
51
+ </Box>
52
+ ) : (
53
+ envs.map((env, i) => (
54
+ <Box
55
+ key={i}
56
+ display="flex"
57
+ alignItems="center"
58
+ mt={1}
59
+ data-testid={`envs-row-${i}`}
60
+ >
61
+ <TextField
62
+ size="small"
63
+ label="Name"
64
+ value={env.key}
65
+ disabled={disabled}
66
+ error={Boolean(keyErrors[i])}
67
+ helperText={keyErrors[i]}
68
+ onChange={(e) => updateRow(i, { key: e.target.value })}
69
+ inputProps={{ 'data-testid': `envs-key-${i}` }}
70
+ />
71
+ <Box mx={1}>
72
+ <TextField
73
+ size="small"
74
+ label="Value"
75
+ value={env.value}
76
+ disabled={disabled}
77
+ onChange={(e) => updateRow(i, { value: e.target.value })}
78
+ inputProps={{ 'data-testid': `envs-value-${i}` }}
79
+ />
80
+ </Box>
81
+ <IconButton
82
+ size="small"
83
+ aria-label="remove env"
84
+ disabled={disabled}
85
+ onClick={() => removeRow(i)}
86
+ data-testid={`envs-remove-${i}`}
87
+ >
88
+ ×
89
+ </IconButton>
90
+ </Box>
91
+ ))
92
+ )}
93
+
94
+ <Box mt={1}>
95
+ <Button
96
+ size="small"
97
+ variant="outlined"
98
+ disabled={disabled}
99
+ onClick={() => addRow()}
100
+ data-testid="envs-add"
101
+ >
102
+ + Add
103
+ </Button>
104
+ </Box>
105
+
106
+ {suggestions.length > 0 && (
107
+ <Box mt={1}>
108
+ <Typography variant="caption" color="textSecondary">
109
+ Suggestions:
110
+ </Typography>
111
+ <Box mt={0.5}>
112
+ {suggestions.map((s) => (
113
+ <Button
114
+ key={s}
115
+ size="small"
116
+ variant="text"
117
+ disabled={disabled || usedKeys.has(s)}
118
+ onClick={() => addRow(s)}
119
+ data-testid={`envs-suggest-${s}`}
120
+ >
121
+ {s}
122
+ </Button>
123
+ ))}
124
+ </Box>
125
+ </Box>
126
+ )}
127
+ </Box>
128
+ )
129
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ Checkbox,
3
+ FormControlLabel,
4
+ TextField,
5
+ Typography,
6
+ } from '@material-ui/core'
7
+ import Box from '@mui/material/Box'
8
+
9
+ import { FormValues, PhysicalEnv } from '../../useForm'
10
+ import { EnvsEditor } from '../EnvsEditor'
11
+
12
+ type Props = {
13
+ values: FormValues
14
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void
15
+ disabled?: boolean
16
+ envsKeyErrors?: (string | undefined)[]
17
+ }
18
+
19
+ // pgBackRest exposes two structured fields (stanza, delta); repo paths, S3
20
+ // keys, archive options all live in the envs map. See
21
+ // engine/internal/retrieval/engine/postgres/physical/pgbackrest.go:23-26 and
22
+ // the example envs in config.example.physical_pgbackrest.yml:84-99.
23
+ const PGBACKREST_ENV_SUGGESTIONS = [
24
+ 'PGBACKREST_REPO',
25
+ 'PGBACKREST_REPO1_TYPE',
26
+ 'PGBACKREST_REPO1_PATH',
27
+ 'PGBACKREST_REPO1_HOST',
28
+ 'PGBACKREST_REPO1_HOST_USER',
29
+ 'PGBACKREST_REPO1_S3_BUCKET',
30
+ 'PGBACKREST_REPO1_S3_ENDPOINT',
31
+ 'PGBACKREST_REPO1_S3_KEY',
32
+ 'PGBACKREST_REPO1_S3_KEY_SECRET',
33
+ 'PGBACKREST_REPO1_S3_REGION',
34
+ 'PGBACKREST_LOG_LEVEL_CONSOLE',
35
+ 'PGBACKREST_PROCESS_MAX',
36
+ ]
37
+
38
+ export const PgBackRest = ({
39
+ values,
40
+ onChange,
41
+ disabled,
42
+ envsKeyErrors,
43
+ }: Props) => {
44
+ const onEnvsChange = (envs: PhysicalEnv[]) => onChange('physicalEnvs', envs)
45
+
46
+ return (
47
+ <Box mt={2} data-testid="pgbackrest-form">
48
+ <Typography variant="subtitle1">pgBackRest</Typography>
49
+ <Box mt={1}>
50
+ <TextField
51
+ fullWidth
52
+ label="Stanza"
53
+ placeholder="my-stanza"
54
+ value={values.physicalPgbackrestStanza}
55
+ disabled={disabled}
56
+ onChange={(e) => onChange('physicalPgbackrestStanza', e.target.value)}
57
+ helperText="Stanza name (must match the stanza configured in your pgBackRest setup)."
58
+ inputProps={{ 'data-testid': 'pgbackrest-stanza' }}
59
+ />
60
+ </Box>
61
+ <Box mt={1}>
62
+ <FormControlLabel
63
+ control={
64
+ <Checkbox
65
+ checked={values.physicalPgbackrestDelta}
66
+ disabled={disabled}
67
+ onChange={(e) =>
68
+ onChange('physicalPgbackrestDelta', e.target.checked)
69
+ }
70
+ inputProps={{
71
+ 'aria-label': 'delta',
72
+ 'data-testid': 'pgbackrest-delta',
73
+ } as React.InputHTMLAttributes<HTMLInputElement>}
74
+ />
75
+ }
76
+ label="Delta restore"
77
+ />
78
+ </Box>
79
+
80
+ <EnvsEditor
81
+ envs={values.physicalEnvs}
82
+ onChange={onEnvsChange}
83
+ suggestions={PGBACKREST_ENV_SUGGESTIONS}
84
+ disabled={disabled}
85
+ keyErrors={envsKeyErrors}
86
+ />
87
+ </Box>
88
+ )
89
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ Checkbox,
3
+ FormControlLabel,
4
+ TextField,
5
+ Typography,
6
+ } from '@material-ui/core'
7
+ import Box from '@mui/material/Box'
8
+
9
+ import { FormValues } from '../../useForm'
10
+
11
+ type Props = {
12
+ values: FormValues
13
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void
14
+ disabled?: boolean
15
+ }
16
+
17
+ // Shared section rendered below WAL-G / pgBackRest with the structured fields
18
+ // that apply across all physical sub-tools (sync.enabled and dockerImage). The
19
+ // engine surfaces more knobs (sync.healthCheck, sync.configs, recovery target,
20
+ // custom restore command), but they remain YAML-only for 4.2 to keep the
21
+ // projection footprint flat.
22
+ export const Sync = ({ values, onChange, disabled }: Props) => {
23
+ return (
24
+ <Box mt={2} data-testid="physical-sync">
25
+ <Typography variant="subtitle1">Sync container & image</Typography>
26
+ <Box mt={1}>
27
+ <TextField
28
+ fullWidth
29
+ label="Docker image"
30
+ placeholder="postgresai/extended-postgres:18-0.6.2"
31
+ value={values.physicalDockerImage}
32
+ disabled={disabled}
33
+ onChange={(e) => onChange('physicalDockerImage', e.target.value)}
34
+ helperText="Postgres image for restore/sync containers. Major version must match the source."
35
+ inputProps={{ 'data-testid': 'physical-docker-image' }}
36
+ />
37
+ </Box>
38
+ <Box mt={1}>
39
+ <FormControlLabel
40
+ control={
41
+ <Checkbox
42
+ checked={values.physicalSyncEnabled}
43
+ disabled={disabled}
44
+ onChange={(e) =>
45
+ onChange('physicalSyncEnabled', e.target.checked)
46
+ }
47
+ inputProps={{
48
+ 'aria-label': 'sync enabled',
49
+ 'data-testid': 'physical-sync-enabled',
50
+ } as React.InputHTMLAttributes<HTMLInputElement>}
51
+ />
52
+ }
53
+ label="Run sync container"
54
+ />
55
+ </Box>
56
+ <Box mt={1}>
57
+ <Typography variant="caption" color="textSecondary">
58
+ For advanced sync settings (health check, sync postgres configs,
59
+ recovery target), edit the YAML config directly.
60
+ </Typography>
61
+ </Box>
62
+ </Box>
63
+ )
64
+ }
@@ -0,0 +1,63 @@
1
+ import { TextField, Typography } from '@material-ui/core'
2
+ import Box from '@mui/material/Box'
3
+
4
+ import { FormValues, PhysicalEnv } from '../../useForm'
5
+ import { EnvsEditor } from '../EnvsEditor'
6
+
7
+ type Props = {
8
+ values: FormValues
9
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void
10
+ disabled?: boolean
11
+ envsKeyErrors?: (string | undefined)[]
12
+ }
13
+
14
+ // WAL-G has one structured field (BackupName, defaulting to "LATEST"); storage
15
+ // backend, bucket, prefix, and credentials all live in the envs map. See
16
+ // engine/internal/retrieval/engine/postgres/physical/wal_g.go:36-38 and the
17
+ // example envs in config.example.physical_walg.yml:84-86.
18
+ const WALG_ENV_SUGGESTIONS = [
19
+ 'WALG_GS_PREFIX',
20
+ 'WALG_S3_PREFIX',
21
+ 'WALG_FILE_PREFIX',
22
+ 'GOOGLE_APPLICATION_CREDENTIALS',
23
+ 'AWS_ACCESS_KEY_ID',
24
+ 'AWS_SECRET_ACCESS_KEY',
25
+ 'AWS_REGION',
26
+ ]
27
+
28
+ export const Walg = ({ values, onChange, disabled, envsKeyErrors }: Props) => {
29
+ const onEnvsChange = (envs: PhysicalEnv[]) => onChange('physicalEnvs', envs)
30
+
31
+ return (
32
+ <Box mt={2} data-testid="walg-form">
33
+ <Typography variant="subtitle1">WAL-G</Typography>
34
+ <Box mt={1}>
35
+ <TextField
36
+ fullWidth
37
+ label="Backup name"
38
+ placeholder="LATEST"
39
+ value={values.physicalWalgBackupName}
40
+ disabled={disabled}
41
+ onChange={(e) => onChange('physicalWalgBackupName', e.target.value)}
42
+ helperText='Which backup to restore. "LATEST" picks the most recent.'
43
+ inputProps={{ 'data-testid': 'walg-backup-name' }}
44
+ />
45
+ </Box>
46
+
47
+ <Box mt={2}>
48
+ <Typography variant="caption" color="textSecondary">
49
+ Storage backend, bucket, prefix, and credentials all live in the envs
50
+ map. Do not paste credentials into the backup name field or any URL.
51
+ </Typography>
52
+ </Box>
53
+
54
+ <EnvsEditor
55
+ envs={values.physicalEnvs}
56
+ onChange={onEnvsChange}
57
+ suggestions={WALG_ENV_SUGGESTIONS}
58
+ disabled={disabled}
59
+ keyErrors={envsKeyErrors}
60
+ />
61
+ </Box>
62
+ )
63
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ FormControl,
3
+ FormControlLabel,
4
+ Radio,
5
+ RadioGroup,
6
+ Typography,
7
+ } from '@material-ui/core'
8
+ import Box from '@mui/material/Box'
9
+
10
+ import { FormValues, PhysicalTool } from '../useForm'
11
+ import { PgBackRest } from './PgBackRest'
12
+ import { Sync } from './Sync'
13
+ import { Walg } from './Walg'
14
+
15
+ type Props = {
16
+ values: FormValues
17
+ onChange: <K extends keyof FormValues>(key: K, value: FormValues[K]) => void
18
+ disabled?: boolean
19
+ envsKeyErrors?: (string | undefined)[]
20
+ }
21
+
22
+ // Physical-mode restore tool selector + sub-form. Two values surface in the
23
+ // UI: WAL-G and pgBackRest, the two tool values the engine accepts outside
24
+ // customTool. pg_basebackup is invoked via the customTool path and remains
25
+ // YAML-only; when the loaded projection has `tool: customTool` we render a
26
+ // banner instead of the sub-form, keeping the user from accidentally wiping a
27
+ // hand-edited customTool config.
28
+ export const PhysicalMode = ({
29
+ values,
30
+ onChange,
31
+ disabled,
32
+ envsKeyErrors,
33
+ }: Props) => {
34
+ const tool = values.physicalTool
35
+ const isCustomTool = tool === 'customTool'
36
+
37
+ return (
38
+ <Box mt={1} data-testid="physical-mode">
39
+ <Typography variant="subtitle2">Restore tool</Typography>
40
+ {isCustomTool ? (
41
+ <Box mt={1} data-testid="physical-custom-tool-banner">
42
+ <Typography variant="body2" color="textSecondary">
43
+ This config uses a custom restore tool (e.g. pg_basebackup). The UI
44
+ cannot edit customTool configurations — edit the YAML directly.
45
+ </Typography>
46
+ </Box>
47
+ ) : (
48
+ <FormControl>
49
+ <RadioGroup
50
+ row
51
+ value={tool}
52
+ onChange={(_, value) =>
53
+ onChange('physicalTool', value as PhysicalTool)
54
+ }
55
+ aria-label="physical restore tool"
56
+ >
57
+ <FormControlLabel
58
+ value="walg"
59
+ control={<Radio disabled={disabled} />}
60
+ label="WAL-G"
61
+ />
62
+ <FormControlLabel
63
+ value="pgbackrest"
64
+ control={<Radio disabled={disabled} />}
65
+ label="pgBackRest"
66
+ />
67
+ </RadioGroup>
68
+ </FormControl>
69
+ )}
70
+
71
+ {!isCustomTool && tool === 'walg' && (
72
+ <Walg
73
+ values={values}
74
+ onChange={onChange}
75
+ disabled={disabled}
76
+ envsKeyErrors={envsKeyErrors}
77
+ />
78
+ )}
79
+ {!isCustomTool && tool === 'pgbackrest' && (
80
+ <PgBackRest
81
+ values={values}
82
+ onChange={onChange}
83
+ disabled={disabled}
84
+ envsKeyErrors={envsKeyErrors}
85
+ />
86
+ )}
87
+
88
+ {!isCustomTool && tool && (
89
+ <Sync values={values} onChange={onChange} disabled={disabled} />
90
+ )}
91
+ </Box>
92
+ )
93
+ }
@@ -0,0 +1,176 @@
1
+ import { Button, Link, Typography } from '@material-ui/core'
2
+ import Box from '@mui/material/Box'
3
+
4
+ import { Spinner } from '@postgres.ai/shared/components/Spinner'
5
+ import { ProposedConfig } from '@postgres.ai/shared/types/api/endpoints/probeSource'
6
+
7
+ import { ResolvedImage, providerKeyToImage } from '../configOptions'
8
+
9
+ type Props = {
10
+ proposed: ProposedConfig
11
+ resolved: ResolvedImage
12
+ applying: boolean
13
+ applyError: string | null
14
+ onApply: () => void
15
+ onEdit: () => void
16
+ }
17
+
18
+ const Field = ({ label, value }: { label: string; value: string }) => (
19
+ <Box display="flex" mb={0.5}>
20
+ <Box minWidth={220} fontWeight={600}>
21
+ {label}
22
+ </Box>
23
+ <Box minWidth={0} style={{ overflowWrap: 'anywhere' }}>
24
+ {value}
25
+ </Box>
26
+ </Box>
27
+ )
28
+
29
+ const Callout = ({ children }: { children: React.ReactNode }) => (
30
+ <Box
31
+ mt={1}
32
+ p={1}
33
+ bgcolor="#fff8e1"
34
+ borderLeft="4px solid #f5a623"
35
+ fontSize={13}
36
+ style={{ overflowWrap: 'anywhere' }}
37
+ >
38
+ {children}
39
+ </Box>
40
+ )
41
+
42
+ export const PreviewCard = ({
43
+ proposed,
44
+ resolved,
45
+ applying,
46
+ applyError,
47
+ onApply,
48
+ onEdit,
49
+ }: Props) => {
50
+ const mapping = providerKeyToImage(
51
+ proposed.dockerImage,
52
+ proposed.pgMajorVersion,
53
+ )
54
+ const preloadLibraries =
55
+ resolved.sharedPreloadLibraries ?? proposed.sharedPreloadLibraries
56
+ const tuningEntries = Object.entries(proposed.queryTuning ?? {})
57
+
58
+ return (
59
+ <Box
60
+ mt={2}
61
+ p={2}
62
+ border="1px solid #e0e0e0"
63
+ borderRadius={4}
64
+ data-testid="preview-card"
65
+ >
66
+ <Typography variant="h6">Proposed configuration</Typography>
67
+
68
+ <Box mt={2}>
69
+ <Field
70
+ label="Detected provider"
71
+ value={proposed.detectedProvider || 'unknown'}
72
+ />
73
+ <Field
74
+ label="Docker image"
75
+ value={resolved.dockerPath || mapping.imageType}
76
+ />
77
+ <Field
78
+ label="Postgres major version"
79
+ value={String(proposed.pgMajorVersion || 'unknown')}
80
+ />
81
+ <Field
82
+ label="Databases"
83
+ value={proposed.databases?.join(', ') || '(none)'}
84
+ />
85
+ <Field label="shared_buffers" value={proposed.sharedBuffers || ''} />
86
+ <Field
87
+ label="shared_preload_libraries"
88
+ value={preloadLibraries || ''}
89
+ />
90
+ </Box>
91
+
92
+ {tuningEntries.length > 0 && (
93
+ <Box mt={2}>
94
+ <Typography variant="subtitle2">Query tuning</Typography>
95
+ <Box component="table" mt={1} style={{ borderCollapse: 'collapse' }}>
96
+ <tbody>
97
+ {tuningEntries.map(([k, v]) => (
98
+ <tr key={k}>
99
+ <td style={{ padding: '2px 16px 2px 0', fontWeight: 600 }}>
100
+ {k}
101
+ </td>
102
+ <td style={{ padding: '2px 0' }}>{v}</td>
103
+ </tr>
104
+ ))}
105
+ </tbody>
106
+ </Box>
107
+ </Box>
108
+ )}
109
+
110
+ <Box mt={2}>
111
+ {(mapping.fallback || proposed.detectedProvider === 'generic') && (
112
+ <Callout>
113
+ Could not detect a managed cloud provider; using the generic
114
+ Postgres image. Switch to Expert mode if your source runs on a
115
+ managed service and we missed it.
116
+ </Callout>
117
+ )}
118
+ {!proposed.memoryProbed && (
119
+ <Callout>
120
+ Could not detect host memory; <code>shared_buffers</code> is set
121
+ to a 1&nbsp;GB safe default. Adjust in Expert mode if your host
122
+ has more RAM.
123
+ </Callout>
124
+ )}
125
+ <Callout>
126
+ Query tuning is copied from your source. If you use the RDS refresh
127
+ tool, these values may not match production — review in Expert mode
128
+ after the first retrieval run.
129
+ </Callout>
130
+ {resolved.isSe ? (
131
+ <Callout>
132
+ Shipping the <code>{resolved.dockerPath}</code> image with its
133
+ curated <code>shared_preload_libraries</code> preset, so the
134
+ detected extensions are bundled.
135
+ </Callout>
136
+ ) : (
137
+ <Callout>
138
+ We'll ship <code>{preloadLibraries}</code>. If the chosen image
139
+ does not bundle one of these libraries, the clone container will
140
+ fail to start with a "could not load library" error — check{' '}
141
+ <code>docker logs dblab_server</code> after Apply.
142
+ </Callout>
143
+ )}
144
+ </Box>
145
+
146
+ <Box mt={2} display="flex" alignItems="center">
147
+ <Button
148
+ variant="contained"
149
+ color="secondary"
150
+ onClick={onApply}
151
+ disabled={applying}
152
+ data-testid="preview-apply"
153
+ >
154
+ Apply &amp; start retrieval
155
+ {applying && <Spinner size="sm" />}
156
+ </Button>
157
+ <Box ml={2}>
158
+ <Link
159
+ component="button"
160
+ type="button"
161
+ onClick={onEdit}
162
+ data-testid="preview-edit"
163
+ >
164
+ Edit before applying
165
+ </Link>
166
+ </Box>
167
+ </Box>
168
+
169
+ {applyError && (
170
+ <Box mt={1} color="#d32f2f" fontSize={13} data-testid="apply-error">
171
+ {applyError}
172
+ </Box>
173
+ )}
174
+ </Box>
175
+ )
176
+ }