@postgres.ai/shared 4.1.2 → 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.2.tgz +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*--------------------------------------------------------------------------
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useEffect, useMemo } from 'react'
|
|
8
|
+
import { useState, useEffect, useMemo, useRef } from 'react'
|
|
9
9
|
import { observer } from 'mobx-react-lite'
|
|
10
10
|
import Editor from '@monaco-editor/react'
|
|
11
11
|
import {
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
Snackbar,
|
|
16
16
|
makeStyles,
|
|
17
17
|
Button,
|
|
18
|
+
Tab,
|
|
19
|
+
Tabs,
|
|
20
|
+
Radio,
|
|
21
|
+
RadioGroup,
|
|
18
22
|
} from '@material-ui/core'
|
|
19
23
|
import Box from '@mui/material/Box'
|
|
20
24
|
|
|
@@ -45,6 +49,10 @@ import {
|
|
|
45
49
|
InputWithTooltip,
|
|
46
50
|
} from './InputWithTooltip'
|
|
47
51
|
|
|
52
|
+
import { SimpleMode, buildProjectionFromProposed } from './SimpleMode'
|
|
53
|
+
import { ConfigMode, getInitialConfigMode } from './configMode'
|
|
54
|
+
import { PhysicalMode } from './PhysicalMode'
|
|
55
|
+
|
|
48
56
|
import styles from './styles.module.scss'
|
|
49
57
|
import { SeImages } from '@postgres.ai/shared/types/api/endpoints/getSeImages'
|
|
50
58
|
import {
|
|
@@ -58,8 +66,6 @@ type PgOptionsType = {
|
|
|
58
66
|
pgRestoreOptions: string[]
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
const NON_LOGICAL_RETRIEVAL_MESSAGE =
|
|
62
|
-
'Configuration editing is only available in logical mode'
|
|
63
69
|
const PREVENT_MODIFYING_MESSAGE = 'Editing is disabled by admin'
|
|
64
70
|
|
|
65
71
|
const useStyles = makeStyles(
|
|
@@ -80,13 +86,11 @@ export const Configuration = observer(
|
|
|
80
86
|
instanceId,
|
|
81
87
|
switchActiveTab,
|
|
82
88
|
reload,
|
|
83
|
-
isConfigurationActive,
|
|
84
89
|
disableConfigModification,
|
|
85
90
|
}: {
|
|
86
91
|
instanceId: string
|
|
87
92
|
switchActiveTab: (_: null, activeTab: number) => void
|
|
88
93
|
reload: () => void
|
|
89
|
-
isConfigurationActive: boolean
|
|
90
94
|
disableConfigModification?: boolean
|
|
91
95
|
}) => {
|
|
92
96
|
const classes = useStyles()
|
|
@@ -106,8 +110,7 @@ export const Configuration = observer(
|
|
|
106
110
|
|
|
107
111
|
const configData: MainStore['config'] =
|
|
108
112
|
config && JSON.parse(JSON.stringify(config))
|
|
109
|
-
const isConfigurationDisabled =
|
|
110
|
-
!isConfigurationActive || disableConfigModification
|
|
113
|
+
const isConfigurationDisabled = disableConfigModification
|
|
111
114
|
|
|
112
115
|
const [dleEdition, setDledition] = useState('')
|
|
113
116
|
const isCeEdition = dleEdition === 'community'
|
|
@@ -118,6 +121,25 @@ export const Configuration = observer(
|
|
|
118
121
|
)
|
|
119
122
|
: dockerImageOptions
|
|
120
123
|
|
|
124
|
+
const [userPickedMode, setUserPickedMode] = useState<ConfigMode | null>(
|
|
125
|
+
null,
|
|
126
|
+
)
|
|
127
|
+
// Seeded lazily — only once configData transitions from null to non-null.
|
|
128
|
+
// Prevents the Simple/Expert default from flipping mid-session when an
|
|
129
|
+
// Apply triggers a refetch and the recomputed default would differ.
|
|
130
|
+
const [initialMode, setInitialMode] = useState<ConfigMode | null>(null)
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (initialMode === null && configData) {
|
|
133
|
+
setInitialMode(
|
|
134
|
+
getInitialConfigMode(configData.host, configData.retrievalMode),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}, [configData, initialMode])
|
|
138
|
+
const configMode: ConfigMode =
|
|
139
|
+
userPickedMode ?? initialMode ?? 'simple'
|
|
140
|
+
const setConfigMode = setUserPickedMode
|
|
141
|
+
const portInitFromConfig = useRef(false)
|
|
142
|
+
|
|
121
143
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
122
144
|
const [submitState, setSubmitState] = useState({
|
|
123
145
|
status: '',
|
|
@@ -172,6 +194,8 @@ export const Configuration = observer(
|
|
|
172
194
|
await updateConfig(
|
|
173
195
|
{
|
|
174
196
|
...values,
|
|
197
|
+
// Preserve configs that originally had no port: key.
|
|
198
|
+
...(omitPortOnSubmit && { port: '' }),
|
|
175
199
|
tuningParams: formatTuningParamsToObj(
|
|
176
200
|
values.tuningParams,
|
|
177
201
|
) as unknown as string,
|
|
@@ -194,8 +218,18 @@ export const Configuration = observer(
|
|
|
194
218
|
}
|
|
195
219
|
})
|
|
196
220
|
}
|
|
197
|
-
const [
|
|
198
|
-
|
|
221
|
+
const [
|
|
222
|
+
{
|
|
223
|
+
formik,
|
|
224
|
+
connectionData,
|
|
225
|
+
isConnectionDataValid,
|
|
226
|
+
connectionString,
|
|
227
|
+
connectionStringError,
|
|
228
|
+
onConnectionStringChange,
|
|
229
|
+
markPortInitialState,
|
|
230
|
+
omitPortOnSubmit,
|
|
231
|
+
},
|
|
232
|
+
] = useForm(onSubmit)
|
|
199
233
|
|
|
200
234
|
// Memoized enhanced Docker images to avoid recreation on every render
|
|
201
235
|
// This combines predefined images with any custom image from configuration
|
|
@@ -578,6 +612,12 @@ export const Configuration = observer(
|
|
|
578
612
|
// Set initial data, empty string for password
|
|
579
613
|
useEffect(() => {
|
|
580
614
|
if (configData) {
|
|
615
|
+
if (!portInitFromConfig.current) {
|
|
616
|
+
markPortInitialState(
|
|
617
|
+
configData.port == null || configData.port === '',
|
|
618
|
+
)
|
|
619
|
+
portInitFromConfig.current = true
|
|
620
|
+
}
|
|
581
621
|
for (const [key, value] of Object.entries(configData)) {
|
|
582
622
|
if (key !== 'password') {
|
|
583
623
|
formik.setFieldValue(key, value)
|
|
@@ -681,9 +721,7 @@ export const Configuration = observer(
|
|
|
681
721
|
message={
|
|
682
722
|
Boolean(dockerState.error)
|
|
683
723
|
? dockerState.error
|
|
684
|
-
:
|
|
685
|
-
? PREVENT_MODIFYING_MESSAGE
|
|
686
|
-
: NON_LOGICAL_RETRIEVAL_MESSAGE
|
|
724
|
+
: PREVENT_MODIFYING_MESSAGE
|
|
687
725
|
}
|
|
688
726
|
className={styles.snackbar}
|
|
689
727
|
/>
|
|
@@ -693,7 +731,37 @@ export const Configuration = observer(
|
|
|
693
731
|
</div>
|
|
694
732
|
) : (
|
|
695
733
|
<Box>
|
|
696
|
-
<Header
|
|
734
|
+
<Header
|
|
735
|
+
retrievalMode={formik.values.retrievalMode}
|
|
736
|
+
setOpen={handleModalClick}
|
|
737
|
+
/>
|
|
738
|
+
<Tabs
|
|
739
|
+
value={configMode}
|
|
740
|
+
onChange={(_, value: ConfigMode) => setConfigMode(value)}
|
|
741
|
+
indicatorColor="primary"
|
|
742
|
+
textColor="primary"
|
|
743
|
+
aria-label="Configuration mode"
|
|
744
|
+
>
|
|
745
|
+
<Tab value="simple" label="Simple" />
|
|
746
|
+
<Tab value="expert" label="Expert" />
|
|
747
|
+
</Tabs>
|
|
748
|
+
{configMode === 'simple' ? (
|
|
749
|
+
<SimpleMode
|
|
750
|
+
instanceId={instanceId}
|
|
751
|
+
disabled={isConfigurationDisabled}
|
|
752
|
+
onApplied={switchTab}
|
|
753
|
+
onEdit={(proposed, password) => {
|
|
754
|
+
const projection = buildProjectionFromProposed(
|
|
755
|
+
proposed,
|
|
756
|
+
password,
|
|
757
|
+
)
|
|
758
|
+
formik.setValues({ ...formik.values, ...projection })
|
|
759
|
+
setConfigMode('expert')
|
|
760
|
+
}}
|
|
761
|
+
/>
|
|
762
|
+
) : null}
|
|
763
|
+
{configMode === 'expert' ? (
|
|
764
|
+
<>
|
|
697
765
|
<Box>
|
|
698
766
|
<Box>
|
|
699
767
|
<FormControlLabel
|
|
@@ -715,6 +783,45 @@ export const Configuration = observer(
|
|
|
715
783
|
</Box>
|
|
716
784
|
<Box mb={1} mt={1}>
|
|
717
785
|
<ConfigSectionTitle tag="retrieval" />
|
|
786
|
+
<Box mt={1} mb={1}>
|
|
787
|
+
<Typography className={styles.subsection}>
|
|
788
|
+
Retrieval mode
|
|
789
|
+
</Typography>
|
|
790
|
+
<RadioGroup
|
|
791
|
+
row
|
|
792
|
+
aria-label="retrieval mode"
|
|
793
|
+
value={formik.values.retrievalMode}
|
|
794
|
+
onChange={(_, value) => {
|
|
795
|
+
formik.setFieldValue('retrievalMode', value)
|
|
796
|
+
}}
|
|
797
|
+
>
|
|
798
|
+
<FormControlLabel
|
|
799
|
+
value="logical"
|
|
800
|
+
control={<Radio disabled={isConfigurationDisabled} />}
|
|
801
|
+
label="Logical (dump/restore)"
|
|
802
|
+
/>
|
|
803
|
+
<FormControlLabel
|
|
804
|
+
value="physical"
|
|
805
|
+
control={<Radio disabled={isConfigurationDisabled} />}
|
|
806
|
+
label="Physical (WAL-G / pgBackRest)"
|
|
807
|
+
/>
|
|
808
|
+
</RadioGroup>
|
|
809
|
+
</Box>
|
|
810
|
+
{formik.values.retrievalMode === 'physical' && (
|
|
811
|
+
<PhysicalMode
|
|
812
|
+
values={formik.values}
|
|
813
|
+
onChange={(key, value) =>
|
|
814
|
+
formik.setFieldValue(key, value, key === 'physicalEnvs')
|
|
815
|
+
}
|
|
816
|
+
disabled={isConfigurationDisabled}
|
|
817
|
+
envsKeyErrors={(
|
|
818
|
+
formik.errors.physicalEnvs as
|
|
819
|
+
| Array<{ key?: string } | undefined>
|
|
820
|
+
| undefined
|
|
821
|
+
)?.map((row) => row?.key)}
|
|
822
|
+
/>
|
|
823
|
+
)}
|
|
824
|
+
{formik.values.retrievalMode === 'logical' && (
|
|
718
825
|
<Box mt={1}>
|
|
719
826
|
<Typography className={styles.subsection}>
|
|
720
827
|
Subsection "retrieval.spec.logicalDump"
|
|
@@ -723,35 +830,35 @@ export const Configuration = observer(
|
|
|
723
830
|
Source database credentials and dumping options.
|
|
724
831
|
</span>
|
|
725
832
|
<InputWithTooltip
|
|
726
|
-
label="
|
|
727
|
-
value={
|
|
728
|
-
error={
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
formik.
|
|
833
|
+
label="Connection string *"
|
|
834
|
+
value={connectionString}
|
|
835
|
+
error={
|
|
836
|
+
connectionStringError ||
|
|
837
|
+
formik.errors.host ||
|
|
838
|
+
formik.errors.username ||
|
|
839
|
+
formik.errors.dbname
|
|
733
840
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
841
|
+
tooltipText={() => (
|
|
842
|
+
<>
|
|
843
|
+
URI form: <code>postgres://user@host:5432/dbname</code>
|
|
844
|
+
. DSN form is also accepted. Do not include the
|
|
845
|
+
password here — use the Password field below.
|
|
846
|
+
</>
|
|
847
|
+
)}
|
|
740
848
|
disabled={isConfigurationDisabled}
|
|
741
|
-
onChange={(e) =>
|
|
742
|
-
formik.setFieldValue('port', e.target.value)
|
|
743
|
-
}
|
|
744
|
-
/>
|
|
745
|
-
<InputWithTooltip
|
|
746
|
-
label="source.connection.username *"
|
|
747
|
-
value={formik.values.username}
|
|
748
|
-
error={formik.errors.username}
|
|
749
|
-
tooltipText={tooltipText.username}
|
|
750
|
-
disabled={isConfigurationDisabled}
|
|
751
|
-
onChange={(e) =>
|
|
752
|
-
formik.setFieldValue('username', e.target.value)
|
|
753
|
-
}
|
|
849
|
+
onChange={(e) => onConnectionStringChange(e.target.value)}
|
|
754
850
|
/>
|
|
851
|
+
<Box mt={0.5} mb={1}>
|
|
852
|
+
<span
|
|
853
|
+
className={classes.grayText}
|
|
854
|
+
data-testid="connection-string-parsed"
|
|
855
|
+
>
|
|
856
|
+
host: {formik.values.host || '—'} | port:{' '}
|
|
857
|
+
{formik.values.port || '5432 (default)'} | user:{' '}
|
|
858
|
+
{formik.values.username || '—'} | dbname:{' '}
|
|
859
|
+
{formik.values.dbname || '—'}
|
|
860
|
+
</span>
|
|
861
|
+
</Box>
|
|
755
862
|
<InputWithTooltip
|
|
756
863
|
type="password"
|
|
757
864
|
value={formik.values.password}
|
|
@@ -762,16 +869,6 @@ export const Configuration = observer(
|
|
|
762
869
|
formik.setFieldValue('password', e.target.value)
|
|
763
870
|
}
|
|
764
871
|
/>
|
|
765
|
-
<InputWithTooltip
|
|
766
|
-
label="source.connection.dbname *"
|
|
767
|
-
value={formik.values.dbname}
|
|
768
|
-
error={formik.errors.dbname}
|
|
769
|
-
tooltipText={tooltipText.dbname}
|
|
770
|
-
disabled={isConfigurationDisabled}
|
|
771
|
-
onChange={(e) =>
|
|
772
|
-
formik.setFieldValue('dbname', e.target.value)
|
|
773
|
-
}
|
|
774
|
-
/>
|
|
775
872
|
<InputWithChip
|
|
776
873
|
id="databases"
|
|
777
874
|
value={formik.values.databases}
|
|
@@ -863,6 +960,7 @@ export const Configuration = observer(
|
|
|
863
960
|
label={'Ignore errors during logical data dump'}
|
|
864
961
|
/>
|
|
865
962
|
</Box>
|
|
963
|
+
)}
|
|
866
964
|
</Box>
|
|
867
965
|
<Box mb={2} mt={1}>
|
|
868
966
|
<ConfigSectionTitle tag="databaseContainer" />
|
|
@@ -1090,6 +1188,7 @@ export const Configuration = observer(
|
|
|
1090
1188
|
/>
|
|
1091
1189
|
) : null}
|
|
1092
1190
|
</Box>
|
|
1191
|
+
{formik.values.retrievalMode === 'logical' && (
|
|
1093
1192
|
<Box>
|
|
1094
1193
|
<Box>
|
|
1095
1194
|
<Typography className={styles.subsection}>
|
|
@@ -1151,6 +1250,7 @@ export const Configuration = observer(
|
|
|
1151
1250
|
label={'Ignore errors during logical data restore'}
|
|
1152
1251
|
/>
|
|
1153
1252
|
</Box>
|
|
1253
|
+
)}
|
|
1154
1254
|
<Box mt={1}>
|
|
1155
1255
|
<Typography className={styles.subsection}>
|
|
1156
1256
|
Subsection "retrieval.refresh"
|
|
@@ -1219,6 +1319,8 @@ export const Configuration = observer(
|
|
|
1219
1319
|
message={configError || submitState.response}
|
|
1220
1320
|
/>
|
|
1221
1321
|
) : null}
|
|
1322
|
+
</>
|
|
1323
|
+
) : null}
|
|
1222
1324
|
</Box>
|
|
1223
1325
|
)}
|
|
1224
1326
|
<Modal
|
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
*--------------------------------------------------------------------------
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { useMemo, useState } from 'react'
|
|
8
9
|
import { useFormik } from 'formik'
|
|
9
10
|
import * as Yup from 'yup'
|
|
10
11
|
|
|
12
|
+
import {
|
|
13
|
+
ConnectionStringError,
|
|
14
|
+
connectionStringFromFields,
|
|
15
|
+
connectionStringToFields,
|
|
16
|
+
} from './connectionString'
|
|
17
|
+
|
|
18
|
+
export type RetrievalMode = 'logical' | 'physical'
|
|
19
|
+
export type PhysicalTool = 'walg' | 'pgbackrest' | 'customTool' | ''
|
|
20
|
+
export type PhysicalEnv = { key: string; value: string }
|
|
21
|
+
|
|
11
22
|
export type FormValues = {
|
|
12
23
|
debug: boolean
|
|
13
24
|
dockerImage: string
|
|
@@ -31,14 +42,72 @@ export type FormValues = {
|
|
|
31
42
|
restoreConfigs: string
|
|
32
43
|
pgDumpCustomOptions: string
|
|
33
44
|
pgRestoreCustomOptions: string
|
|
45
|
+
retrievalMode: RetrievalMode
|
|
46
|
+
physicalTool: PhysicalTool
|
|
47
|
+
physicalDockerImage: string
|
|
48
|
+
physicalSyncEnabled: boolean
|
|
49
|
+
physicalWalgBackupName: string
|
|
50
|
+
physicalPgbackrestStanza: string
|
|
51
|
+
physicalPgbackrestDelta: boolean
|
|
52
|
+
physicalEnvs: PhysicalEnv[]
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
// Logical-mode required fields. Validation skips the source-connection block
|
|
56
|
+
// when retrievalMode is "physical" — that branch uses tool-specific inputs
|
|
57
|
+
// instead of a connection URL.
|
|
36
58
|
const Schema = Yup.object().shape({
|
|
37
59
|
dockerImage: Yup.string().required('Docker image is required'),
|
|
38
|
-
dbname: Yup.string().
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
dbname: Yup.string().when('retrievalMode', {
|
|
61
|
+
is: 'logical',
|
|
62
|
+
then: (s) => s.required('Dbname is required'),
|
|
63
|
+
}),
|
|
64
|
+
host: Yup.string().when('retrievalMode', {
|
|
65
|
+
is: 'logical',
|
|
66
|
+
then: (s) => s.required('Host is required'),
|
|
67
|
+
}),
|
|
68
|
+
port: Yup.string().when('retrievalMode', {
|
|
69
|
+
is: 'logical',
|
|
70
|
+
then: (s) => s.required('Port is required'),
|
|
71
|
+
}),
|
|
72
|
+
username: Yup.string().when('retrievalMode', {
|
|
73
|
+
is: 'logical',
|
|
74
|
+
then: (s) => s.required('Username is required'),
|
|
75
|
+
}),
|
|
76
|
+
physicalEnvs: Yup.array()
|
|
77
|
+
.of(
|
|
78
|
+
Yup.object().shape({
|
|
79
|
+
key: Yup.string(),
|
|
80
|
+
value: Yup.string(),
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
.test('unique-keys', '', function (envs) {
|
|
84
|
+
if (!envs) return true
|
|
85
|
+
|
|
86
|
+
const positions = new Map<string, number[]>()
|
|
87
|
+
envs.forEach((row, i) => {
|
|
88
|
+
const trimmed = (row?.key ?? '').trim()
|
|
89
|
+
if (!trimmed) return
|
|
90
|
+
const list = positions.get(trimmed) ?? []
|
|
91
|
+
list.push(i)
|
|
92
|
+
positions.set(trimmed, list)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const dupes: Yup.ValidationError[] = []
|
|
96
|
+
for (const indices of positions.values()) {
|
|
97
|
+
if (indices.length < 2) continue
|
|
98
|
+
for (const i of indices) {
|
|
99
|
+
dupes.push(
|
|
100
|
+
this.createError({
|
|
101
|
+
path: `${this.path}[${i}].key`,
|
|
102
|
+
message: 'Duplicate key',
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (dupes.length === 0) return true
|
|
109
|
+
return new Yup.ValidationError(dupes)
|
|
110
|
+
}),
|
|
42
111
|
})
|
|
43
112
|
|
|
44
113
|
export const useForm = (onSubmit: (values: FormValues) => void) => {
|
|
@@ -66,6 +135,14 @@ export const useForm = (onSubmit: (values: FormValues) => void) => {
|
|
|
66
135
|
pgRestoreCustomOptions: '',
|
|
67
136
|
dumpIgnoreErrors: false,
|
|
68
137
|
restoreIgnoreErrors: false,
|
|
138
|
+
retrievalMode: 'logical',
|
|
139
|
+
physicalTool: '',
|
|
140
|
+
physicalDockerImage: '',
|
|
141
|
+
physicalSyncEnabled: false,
|
|
142
|
+
physicalWalgBackupName: '',
|
|
143
|
+
physicalPgbackrestStanza: '',
|
|
144
|
+
physicalPgbackrestDelta: false,
|
|
145
|
+
physicalEnvs: [],
|
|
69
146
|
},
|
|
70
147
|
validationSchema: Schema,
|
|
71
148
|
onSubmit,
|
|
@@ -73,6 +150,67 @@ export const useForm = (onSubmit: (values: FormValues) => void) => {
|
|
|
73
150
|
validateOnChange: false,
|
|
74
151
|
})
|
|
75
152
|
|
|
153
|
+
const [originalPortWasUnset, setOriginalPortWasUnset] = useState(false)
|
|
154
|
+
const [portDirty, setPortDirty] = useState(false)
|
|
155
|
+
const [connectionStringError, setConnectionStringError] = useState<string | null>(null)
|
|
156
|
+
|
|
157
|
+
const connectionString = useMemo(
|
|
158
|
+
() =>
|
|
159
|
+
connectionStringFromFields(
|
|
160
|
+
{
|
|
161
|
+
host: formik.values.host,
|
|
162
|
+
port: formik.values.port || '5432',
|
|
163
|
+
username: formik.values.username,
|
|
164
|
+
dbname: formik.values.dbname,
|
|
165
|
+
},
|
|
166
|
+
{ omitDefaultPort: originalPortWasUnset && !portDirty },
|
|
167
|
+
),
|
|
168
|
+
[
|
|
169
|
+
formik.values.host,
|
|
170
|
+
formik.values.port,
|
|
171
|
+
formik.values.username,
|
|
172
|
+
formik.values.dbname,
|
|
173
|
+
originalPortWasUnset,
|
|
174
|
+
portDirty,
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
const onConnectionStringChange = (s: string) => {
|
|
179
|
+
if (!s.trim()) {
|
|
180
|
+
setConnectionStringError(null)
|
|
181
|
+
formik.setValues({
|
|
182
|
+
...formik.values,
|
|
183
|
+
host: '',
|
|
184
|
+
port: '',
|
|
185
|
+
username: '',
|
|
186
|
+
dbname: '',
|
|
187
|
+
})
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const parsed = connectionStringToFields(s)
|
|
193
|
+
setConnectionStringError(null)
|
|
194
|
+
|
|
195
|
+
if (parsed.portWasExplicit) setPortDirty(true)
|
|
196
|
+
|
|
197
|
+
formik.setValues({
|
|
198
|
+
...formik.values,
|
|
199
|
+
host: parsed.fields.host,
|
|
200
|
+
port: parsed.fields.port,
|
|
201
|
+
username: parsed.fields.username,
|
|
202
|
+
dbname: parsed.fields.dbname,
|
|
203
|
+
})
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (err instanceof ConnectionStringError) setConnectionStringError(err.message)
|
|
206
|
+
else throw err
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const markPortInitialState = (wasUnset: boolean) => setOriginalPortWasUnset(wasUnset)
|
|
211
|
+
const markPortDirty = () => setPortDirty(true)
|
|
212
|
+
const omitPortOnSubmit = originalPortWasUnset && !portDirty
|
|
213
|
+
|
|
76
214
|
const formatDatabaseArray = (database: string) => {
|
|
77
215
|
let databases = []
|
|
78
216
|
const splitDatabaseArray = database.split(/[,(\s)(\n)(\r)(\t)(\r\n)]/)
|
|
@@ -106,5 +244,17 @@ export const useForm = (onSubmit: (values: FormValues) => void) => {
|
|
|
106
244
|
formik.values.username &&
|
|
107
245
|
formik.values.dbname
|
|
108
246
|
|
|
109
|
-
return [
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
formik,
|
|
250
|
+
connectionData,
|
|
251
|
+
isConnectionDataValid,
|
|
252
|
+
connectionString,
|
|
253
|
+
connectionStringError,
|
|
254
|
+
onConnectionStringChange,
|
|
255
|
+
markPortInitialState,
|
|
256
|
+
markPortDirty,
|
|
257
|
+
omitPortOnSubmit,
|
|
258
|
+
},
|
|
259
|
+
]
|
|
110
260
|
}
|
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
import { DatabaseType } from '@postgres.ai/shared/types/api/entities/config'
|
|
2
2
|
|
|
3
3
|
import { dockerImageOptions } from '../configOptions'
|
|
4
|
+
import { dockerImagesConfig, genericImagePrefix } from '../dockerCatalog'
|
|
4
5
|
import { FormValues } from '../useForm'
|
|
5
6
|
|
|
6
7
|
const seContainerRegistry = 'se-images'
|
|
7
|
-
const genericImagePrefix = 'postgresai/extended-postgres'
|
|
8
|
-
// Predefined list of Docker images for UI display
|
|
9
|
-
// This list is shown to users for convenient selection
|
|
10
|
-
// IMPORTANT: if user specified an image in config that's not in this list,
|
|
11
|
-
// it will be automatically added via createEnhancedDockerImages()
|
|
12
|
-
const dockerImagesConfig = {
|
|
13
|
-
'9.6': ['0.5.3', '0.5.2', '0.5.1'],
|
|
14
|
-
'10': ['0.5.3', '0.5.2', '0.5.1'],
|
|
15
|
-
'11': ['0.5.3', '0.5.2', '0.5.1'],
|
|
16
|
-
'12': ['0.5.3', '0.5.2', '0.5.1'],
|
|
17
|
-
'13': ['0.5.3', '0.5.2', '0.5.1'],
|
|
18
|
-
'14': ['0.5.3', '0.5.2', '0.5.1'],
|
|
19
|
-
'15': ['0.5.3', '0.5.2', '0.5.1'],
|
|
20
|
-
'16': ['0.5.3', '0.5.2', '0.5.1'],
|
|
21
|
-
'17': ['0.5.3', '0.5.2', '0.5.1'],
|
|
22
|
-
'18': ['0.6.1'],
|
|
23
|
-
}
|
|
24
8
|
|
|
25
9
|
export type FormValuesKey = keyof FormValues
|
|
26
10
|
|
|
@@ -21,7 +21,7 @@ const useStyles = makeStyles(
|
|
|
21
21
|
position: 'relative',
|
|
22
22
|
cursor: 'default',
|
|
23
23
|
flex: `0 0 ${CELL_SIZE}px`,
|
|
24
|
-
background: '
|
|
24
|
+
background: 'rgba(128, 128, 128, 0.1)',
|
|
25
25
|
height: `${CELL_SIZE}px`,
|
|
26
26
|
display: 'flex',
|
|
27
27
|
borderRadius: `${CELL_SIZE / 2}px`,
|
|
@@ -31,7 +31,7 @@ const useStyles = makeStyles(
|
|
|
31
31
|
fontSize: '12px',
|
|
32
32
|
},
|
|
33
33
|
rootHasSnapshots: {
|
|
34
|
-
background:
|
|
34
|
+
background: 'rgba(15, 135, 157, 0.32)',
|
|
35
35
|
cursor: 'pointer',
|
|
36
36
|
},
|
|
37
37
|
rootCurrent: {
|
|
@@ -46,7 +46,7 @@ const useStyles = makeStyles(
|
|
|
46
46
|
right: '-6px',
|
|
47
47
|
position: 'absolute',
|
|
48
48
|
fontSize: '8px',
|
|
49
|
-
backgroundColor:
|
|
49
|
+
backgroundColor: 'inherit',
|
|
50
50
|
border: `1px solid ${colors.secondary2.lightLight}`,
|
|
51
51
|
borderRadius: '8px',
|
|
52
52
|
height: '16px',
|
|
@@ -50,6 +50,16 @@ const useStyles = makeStyles(
|
|
|
50
50
|
header: {
|
|
51
51
|
fontWeight: 500,
|
|
52
52
|
},
|
|
53
|
+
protectedBadge: {
|
|
54
|
+
marginLeft: '8px',
|
|
55
|
+
padding: '1px 6px',
|
|
56
|
+
fontSize: '11px',
|
|
57
|
+
fontWeight: 400,
|
|
58
|
+
color: '#1a7f37',
|
|
59
|
+
border: '1px solid #1a7f37',
|
|
60
|
+
borderRadius: '4px',
|
|
61
|
+
whiteSpace: 'nowrap',
|
|
62
|
+
},
|
|
53
63
|
infoContent: {
|
|
54
64
|
fontSize: '12px',
|
|
55
65
|
color: '#808080',
|
|
@@ -139,7 +149,17 @@ const SnapshotListItem = ({
|
|
|
139
149
|
<div className={classes.commitItem}>
|
|
140
150
|
<div className={classes.gridContainer}>
|
|
141
151
|
<div className={classes.infoBlock}>
|
|
142
|
-
<div className={classes.header}>
|
|
152
|
+
<div className={classes.header}>
|
|
153
|
+
{snapshot.message || '-'}
|
|
154
|
+
{snapshot.protected && (
|
|
155
|
+
<span
|
|
156
|
+
className={classes.protectedBadge}
|
|
157
|
+
title="Protected from deletion"
|
|
158
|
+
>
|
|
159
|
+
Protected
|
|
160
|
+
</span>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
143
163
|
<div className={classes.infoContent} title={snapshot.dataStateAt}>
|
|
144
164
|
{timeAgo}
|
|
145
165
|
</div>
|
|
@@ -44,6 +44,10 @@ const useStyles = makeStyles(
|
|
|
44
44
|
{ index: 1 },
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
+
// listRefreshIntervalMs is how often the snapshot list is silently refreshed so background
|
|
48
|
+
// auto-deletion does not leave stale rows in the list.
|
|
49
|
+
const listRefreshIntervalMs = 30_000
|
|
50
|
+
|
|
47
51
|
interface SnapshotsProps {
|
|
48
52
|
instanceId: string
|
|
49
53
|
}
|
|
@@ -97,11 +101,17 @@ export const Snapshots: React.FC<SnapshotsProps> = observer(
|
|
|
97
101
|
}, [])
|
|
98
102
|
|
|
99
103
|
useEffect(() => {
|
|
100
|
-
if (selectedBranch)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
if (!selectedBranch) return
|
|
105
|
+
|
|
106
|
+
const branchName = selectedBranch === 'All branches' ? '' : selectedBranch
|
|
107
|
+
|
|
108
|
+
stores.main.reloadSnapshots(branchName)
|
|
109
|
+
|
|
110
|
+
const intervalId = setInterval(() => {
|
|
111
|
+
stores.main.reloadSnapshots(branchName, true)
|
|
112
|
+
}, listRefreshIntervalMs)
|
|
113
|
+
|
|
114
|
+
return () => clearInterval(intervalId)
|
|
105
115
|
}, [selectedBranch])
|
|
106
116
|
|
|
107
117
|
if (!instance && !snapshots.isLoading) return <></>
|