@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
package/.gitlab-ci.yml
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
.shared_base: &shared_base
|
|
12
12
|
image:
|
|
13
13
|
name: node:lts-alpine
|
|
14
|
-
pull_policy:
|
|
14
|
+
pull_policy: always
|
|
15
15
|
allow_failure: true
|
|
16
16
|
cache:
|
|
17
17
|
key: "$CI_COMMIT_REF_SLUG"
|
|
@@ -74,6 +74,10 @@ publish-shared-release:
|
|
|
74
74
|
- npm version "$VERSION" --no-git-tag-version --allow-same-version
|
|
75
75
|
- pnpm run pack
|
|
76
76
|
|
|
77
|
-
# Publish
|
|
77
|
+
# Publish: stable releases go to the "latest" dist-tag, prereleases (e.g. 4.2.0-rc.1)
|
|
78
|
+
# go to a channel dist-tag matching their identifier (rc/beta/alpha), since npm refuses
|
|
79
|
+
# to publish a prerelease version to "latest" without an explicit --tag.
|
|
78
80
|
- TARBALL=$(ls postgres.ai-shared-*.tgz)
|
|
79
|
-
-
|
|
81
|
+
- DIST_TAG=latest
|
|
82
|
+
- case "$VERSION" in *-*) PRERELEASE=${VERSION#*-}; DIST_TAG=${PRERELEASE%%.*} ;; esac
|
|
83
|
+
- pnpm publish "$TARBALL" --no-git-checks --tag "$DIST_TAG"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*--------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai
|
|
3
|
+
* All Rights Reserved. Proprietary and confidential.
|
|
4
|
+
* Unauthorized copying of this file, via any medium is strictly prohibited
|
|
5
|
+
*--------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState } from 'react'
|
|
9
|
+
import { makeStyles, TextField } from '@material-ui/core'
|
|
10
|
+
import { Clone } from '@postgres.ai/shared/types/api/entities/clone'
|
|
11
|
+
import { Text } from '@postgres.ai/shared/components/Text'
|
|
12
|
+
import { Modal } from '@postgres.ai/shared/components/Modal'
|
|
13
|
+
import { ImportantText } from '@postgres.ai/shared/components/ImportantText'
|
|
14
|
+
import { SimpleModalControls } from '@postgres.ai/shared/components/SimpleModalControls'
|
|
15
|
+
import { colors } from '@postgres.ai/shared/styles/vars'
|
|
16
|
+
|
|
17
|
+
// The upgrade image ships binaries for the four majors preceding its own, which is the same
|
|
18
|
+
// bound the engine enforces on the request.
|
|
19
|
+
const MAX_MAJOR_JUMP = 4
|
|
20
|
+
|
|
21
|
+
type Props = {
|
|
22
|
+
isOpen: boolean
|
|
23
|
+
onClose: () => void
|
|
24
|
+
clone: Clone
|
|
25
|
+
// The version the instance upgrades to, read from its status. It follows from the configured
|
|
26
|
+
// upgrade image, so it is shown rather than asked for.
|
|
27
|
+
targetVersion: number
|
|
28
|
+
onUpgradeClone: (dockerImage?: string) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const useStyles = makeStyles(
|
|
32
|
+
{
|
|
33
|
+
field: {
|
|
34
|
+
margin: '16px 0 0 0',
|
|
35
|
+
},
|
|
36
|
+
hint: {
|
|
37
|
+
margin: '8px 0 0 0',
|
|
38
|
+
},
|
|
39
|
+
error: {
|
|
40
|
+
margin: '8px 0 0 0',
|
|
41
|
+
color: colors.status.error,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{ index: 1 },
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
export const UpgradeCloneModal = (props: Props) => {
|
|
48
|
+
const { isOpen, onClose, clone, targetVersion, onUpgradeClone } = props
|
|
49
|
+
|
|
50
|
+
const classes = useStyles()
|
|
51
|
+
|
|
52
|
+
const [dockerImage, setDockerImage] = useState('')
|
|
53
|
+
|
|
54
|
+
// The modal stays mounted while the clone page is open, so without this it reopens holding the
|
|
55
|
+
// previous attempt's image, which would ride along unnoticed.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (isOpen) setDockerImage('')
|
|
58
|
+
}, [isOpen])
|
|
59
|
+
|
|
60
|
+
const currentVersion = clone.dbVersion ? parseInt(clone.dbVersion, 10) : NaN
|
|
61
|
+
|
|
62
|
+
// Nothing here is decidable without the current version, so the engine has the last word: it
|
|
63
|
+
// reads the major out of the data directory once the upgrade starts.
|
|
64
|
+
const getBlockingReason = () => {
|
|
65
|
+
if (Number.isNaN(currentVersion)) return null
|
|
66
|
+
|
|
67
|
+
if (targetVersion <= currentVersion) {
|
|
68
|
+
return `This clone already runs PostgreSQL ${currentVersion}, and the instance upgrades to ${targetVersion}.`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (targetVersion - currentVersion > MAX_MAJOR_JUMP) {
|
|
72
|
+
return `The upgrade image covers at most ${MAX_MAJOR_JUMP} preceding majors, and this clone runs PostgreSQL ${currentVersion}.`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const blockingReason = getBlockingReason()
|
|
79
|
+
|
|
80
|
+
const handleClickUpgrade = () => {
|
|
81
|
+
if (blockingReason) return
|
|
82
|
+
|
|
83
|
+
onUpgradeClone(dockerImage || undefined)
|
|
84
|
+
onClose()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Modal title={`Upgrade clone ${clone.id}`} isOpen={isOpen} onClose={onClose}>
|
|
89
|
+
<Text>
|
|
90
|
+
Clone <ImportantText>{clone.id}</ImportantText>
|
|
91
|
+
{clone.dbVersion
|
|
92
|
+
? ` currently runs PostgreSQL ${clone.dbVersion}. `
|
|
93
|
+
: ' has no recorded PostgreSQL version. '}
|
|
94
|
+
This instance upgrades clones to{' '}
|
|
95
|
+
<ImportantText>PostgreSQL {targetVersion}</ImportantText>, the major its
|
|
96
|
+
configured upgrade image is built on.
|
|
97
|
+
</Text>
|
|
98
|
+
{blockingReason ? (
|
|
99
|
+
<div className={classes.error}>
|
|
100
|
+
<Text>{blockingReason}</Text>
|
|
101
|
+
</div>
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
<div className={classes.hint}>
|
|
105
|
+
<Text>
|
|
106
|
+
The data directory is upgraded in place, and the clone restarts on
|
|
107
|
+
the target version. Resetting the clone afterwards returns it to
|
|
108
|
+
the version the instance is configured with.
|
|
109
|
+
</Text>
|
|
110
|
+
</div>
|
|
111
|
+
<div className={classes.hint}>
|
|
112
|
+
<Text>
|
|
113
|
+
If the upgrade fails before any data is converted, the clone keeps
|
|
114
|
+
running on its current version. If it fails afterwards, the clone
|
|
115
|
+
is restored from its snapshot and changes made since it was
|
|
116
|
+
created are lost.
|
|
117
|
+
</Text>
|
|
118
|
+
</div>
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
<TextField
|
|
122
|
+
label="Docker image (optional)"
|
|
123
|
+
value={dockerImage}
|
|
124
|
+
onChange={(e) => setDockerImage(e.target.value)}
|
|
125
|
+
helperText="Leave empty to keep the current image and change only its major version"
|
|
126
|
+
fullWidth
|
|
127
|
+
className={classes.field}
|
|
128
|
+
/>
|
|
129
|
+
<SimpleModalControls
|
|
130
|
+
items={[
|
|
131
|
+
{
|
|
132
|
+
text: 'Cancel',
|
|
133
|
+
onClick: onClose,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
text: 'Upgrade',
|
|
137
|
+
onClick: handleClickUpgrade,
|
|
138
|
+
variant: 'primary',
|
|
139
|
+
isDisabled: Boolean(blockingReason),
|
|
140
|
+
},
|
|
141
|
+
]}
|
|
142
|
+
/>
|
|
143
|
+
</Modal>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { Clone } from '@postgres.ai/shared/types/api/entities/clone';
|
|
3
|
+
declare type Props = {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
clone: Clone;
|
|
7
|
+
targetVersion: number;
|
|
8
|
+
onUpgradeClone: (dockerImage?: string) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare const UpgradeCloneModal: (props: Props) => JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*--------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai
|
|
4
|
+
* All Rights Reserved. Proprietary and confidential.
|
|
5
|
+
* Unauthorized copying of this file, via any medium is strictly prohibited
|
|
6
|
+
*--------------------------------------------------------------------------
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { makeStyles, TextField } from '@material-ui/core';
|
|
10
|
+
import { Text } from '@postgres.ai/shared/components/Text';
|
|
11
|
+
import { Modal } from '@postgres.ai/shared/components/Modal';
|
|
12
|
+
import { ImportantText } from '@postgres.ai/shared/components/ImportantText';
|
|
13
|
+
import { SimpleModalControls } from '@postgres.ai/shared/components/SimpleModalControls';
|
|
14
|
+
import { colors } from '@postgres.ai/shared/styles/vars';
|
|
15
|
+
// The upgrade image ships binaries for the four majors preceding its own, which is the same
|
|
16
|
+
// bound the engine enforces on the request.
|
|
17
|
+
const MAX_MAJOR_JUMP = 4;
|
|
18
|
+
const useStyles = makeStyles({
|
|
19
|
+
field: {
|
|
20
|
+
margin: '16px 0 0 0',
|
|
21
|
+
},
|
|
22
|
+
hint: {
|
|
23
|
+
margin: '8px 0 0 0',
|
|
24
|
+
},
|
|
25
|
+
error: {
|
|
26
|
+
margin: '8px 0 0 0',
|
|
27
|
+
color: colors.status.error,
|
|
28
|
+
},
|
|
29
|
+
}, { index: 1 });
|
|
30
|
+
export const UpgradeCloneModal = (props) => {
|
|
31
|
+
const { isOpen, onClose, clone, targetVersion, onUpgradeClone } = props;
|
|
32
|
+
const classes = useStyles();
|
|
33
|
+
const [dockerImage, setDockerImage] = useState('');
|
|
34
|
+
// The modal stays mounted while the clone page is open, so without this it reopens holding the
|
|
35
|
+
// previous attempt's image, which would ride along unnoticed.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (isOpen)
|
|
38
|
+
setDockerImage('');
|
|
39
|
+
}, [isOpen]);
|
|
40
|
+
const currentVersion = clone.dbVersion ? parseInt(clone.dbVersion, 10) : NaN;
|
|
41
|
+
// Nothing here is decidable without the current version, so the engine has the last word: it
|
|
42
|
+
// reads the major out of the data directory once the upgrade starts.
|
|
43
|
+
const getBlockingReason = () => {
|
|
44
|
+
if (Number.isNaN(currentVersion))
|
|
45
|
+
return null;
|
|
46
|
+
if (targetVersion <= currentVersion) {
|
|
47
|
+
return `This clone already runs PostgreSQL ${currentVersion}, and the instance upgrades to ${targetVersion}.`;
|
|
48
|
+
}
|
|
49
|
+
if (targetVersion - currentVersion > MAX_MAJOR_JUMP) {
|
|
50
|
+
return `The upgrade image covers at most ${MAX_MAJOR_JUMP} preceding majors, and this clone runs PostgreSQL ${currentVersion}.`;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
const blockingReason = getBlockingReason();
|
|
55
|
+
const handleClickUpgrade = () => {
|
|
56
|
+
if (blockingReason)
|
|
57
|
+
return;
|
|
58
|
+
onUpgradeClone(dockerImage || undefined);
|
|
59
|
+
onClose();
|
|
60
|
+
};
|
|
61
|
+
return (_jsxs(Modal, { title: `Upgrade clone ${clone.id}`, isOpen: isOpen, onClose: onClose, children: [_jsxs(Text, { children: ["Clone ", _jsx(ImportantText, { children: clone.id }), clone.dbVersion
|
|
62
|
+
? ` currently runs PostgreSQL ${clone.dbVersion}. `
|
|
63
|
+
: ' has no recorded PostgreSQL version. ', "This instance upgrades clones to", ' ', _jsxs(ImportantText, { children: ["PostgreSQL ", targetVersion] }), ", the major its configured upgrade image is built on."] }), blockingReason ? (_jsx("div", { className: classes.error, children: _jsx(Text, { children: blockingReason }) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: classes.hint, children: _jsx(Text, { children: "The data directory is upgraded in place, and the clone restarts on the target version. Resetting the clone afterwards returns it to the version the instance is configured with." }) }), _jsx("div", { className: classes.hint, children: _jsx(Text, { children: "If the upgrade fails before any data is converted, the clone keeps running on its current version. If it fails afterwards, the clone is restored from its snapshot and changes made since it was created are lost." }) })] })), _jsx(TextField, { label: "Docker image (optional)", value: dockerImage, onChange: (e) => setDockerImage(e.target.value), helperText: "Leave empty to keep the current image and change only its major version", fullWidth: true, className: classes.field }), _jsx(SimpleModalControls, { items: [
|
|
64
|
+
{
|
|
65
|
+
text: 'Cancel',
|
|
66
|
+
onClick: onClose,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
text: 'Upgrade',
|
|
70
|
+
onClick: handleClickUpgrade,
|
|
71
|
+
variant: 'primary',
|
|
72
|
+
isDisabled: Boolean(blockingReason),
|
|
73
|
+
},
|
|
74
|
+
] })] }));
|
|
75
|
+
};
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postgres.ai/shared",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tsc -p tsconfig.build.json && node scripts/copy-assets.js",
|
|
6
6
|
"pack": "node scripts/pack.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@material-ui/styles": "^4.11.4",
|
|
18
18
|
"@monaco-editor/react": "^4.4.5",
|
|
19
19
|
"@mui/material": "^5.10.12",
|
|
20
|
-
"@types/node": "^
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
21
|
"@types/react": "^17.0.30",
|
|
22
22
|
"@types/react-dom": "^17.0.10",
|
|
23
23
|
"@types/react-router": "^5.1.17",
|
|
@@ -13,8 +13,11 @@ import { makeStyles, Button, TextField, IconButton, Table, TableHead, TableRow,
|
|
|
13
13
|
import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub';
|
|
14
14
|
import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner';
|
|
15
15
|
import { SectionTitle } from '@postgres.ai/shared/components/SectionTitle';
|
|
16
|
+
import { Select } from '@postgres.ai/shared/components/Select';
|
|
16
17
|
import { Spinner } from '@postgres.ai/shared/components/Spinner';
|
|
17
18
|
import { Tooltip } from '@postgres.ai/shared/components/Tooltip';
|
|
19
|
+
import { parseDate } from '@postgres.ai/shared/utils/date';
|
|
20
|
+
import { protectionOptions, getProtectionSelectValue, } from '@postgres.ai/shared/utils/protection';
|
|
18
21
|
import { icons } from '@postgres.ai/shared/styles/icons';
|
|
19
22
|
import { styles } from '@postgres.ai/shared/styles/styles';
|
|
20
23
|
import { DeleteBranchModal } from '@postgres.ai/shared/pages/Branches/components/Modals/DeleteBranchModal';
|
|
@@ -73,6 +76,13 @@ const useStyles = makeStyles(() => ({
|
|
|
73
76
|
summary: {
|
|
74
77
|
marginTop: 20,
|
|
75
78
|
},
|
|
79
|
+
protectionControl: {
|
|
80
|
+
display: 'flex',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
marginTop: '8px',
|
|
83
|
+
marginBottom: '8px',
|
|
84
|
+
maxWidth: 525,
|
|
85
|
+
},
|
|
76
86
|
text: {
|
|
77
87
|
marginTop: '4px',
|
|
78
88
|
},
|
|
@@ -120,8 +130,15 @@ export const BranchesPage = observer((props) => {
|
|
|
120
130
|
const history = useHistory();
|
|
121
131
|
const stores = useCreatedStores(props);
|
|
122
132
|
const [isOpenDestroyModal, setIsOpenDestroyModal] = useState(false);
|
|
123
|
-
const { branch, snapshotList, deleteBranch, reload, load, isReloading, isBranchesLoading, getBranchesError, snapshotListError, getBranchError, } = stores.main;
|
|
133
|
+
const { branch, snapshotList, deleteBranch, reload, load, isReloading, isBranchesLoading, isUpdatingBranch, updateBranchProtection, updateBranchError, getBranchesError, snapshotListError, getBranchError, } = stores.main;
|
|
124
134
|
const hasBranchError = getBranchesError || getBranchError || snapshotListError;
|
|
135
|
+
const protectedTillDate = (branch === null || branch === void 0 ? void 0 : branch.protectedTill)
|
|
136
|
+
? parseDate(branch.protectedTill)
|
|
137
|
+
: null;
|
|
138
|
+
const handleProtectionChange = (e) => {
|
|
139
|
+
const value = e.target.value;
|
|
140
|
+
updateBranchProtection(props.branchId, props.instanceId, value === 'none' ? null : parseInt(value, 10));
|
|
141
|
+
};
|
|
125
142
|
const branchLogLength = snapshotList === null || snapshotList === void 0 ? void 0 : snapshotList.reduce((acc, snapshot) => {
|
|
126
143
|
var _a;
|
|
127
144
|
if ((snapshot === null || snapshot === void 0 ? void 0 : snapshot.branch) !== null) {
|
|
@@ -144,7 +161,15 @@ export const BranchesPage = observer((props) => {
|
|
|
144
161
|
(getBranchError === null || getBranchError === void 0 ? void 0 : getBranchError.message) ||
|
|
145
162
|
(snapshotListError === null || snapshotListError === void 0 ? void 0 : snapshotListError.message), className: classes.marginTop })] }));
|
|
146
163
|
}
|
|
147
|
-
return (_jsxs(_Fragment, { children: [headRendered, _jsxs("div", { className: classes.wrapper, children: [_jsxs("div", { className: classes.container, children: [_jsxs("div", { className: classes.actions, children: [_jsx(Button, { variant: "contained", color: "primary", onClick: () => history.push(props.routes.createClone(props.branchId)), disabled: isReloading, title: 'Create clone', className: classes.actionButton, children: "Create clone" }), _jsx(Button, { variant: "contained", color: "primary", onClick: () => setIsOpenDestroyModal(true), disabled: isReloading, title: 'Delete this branch', className: classes.actionButton, children: "Delete branch" }), _jsxs(Button, { variant: "outlined", color: "secondary", onClick: () => reload(props.branchId, props.instanceId), disabled: isReloading, title: 'Refresh branch information', className: classes.actionButton, children: ["Reload info", isReloading && _jsx(Spinner, { size: "sm", className: classes.spinner })] })] }), _jsx("br", {}), _jsxs("div", { children: [_jsxs("div", { children: [_jsxs("p", { children: [_jsx("strong", { children: "Data state at" }), "\u00A0", _jsx(Tooltip, { content: _jsxs(_Fragment, { children: [_jsx("strong", { children: "Data state time" }), " is a time at which data is\u00A0 recovered for this branch."] }), children: icons.infoIcon })] }), _jsx("p", { className: classes.text, children: (branch === null || branch === void 0 ? void 0 : branch.dataStateAt) || '-' })] }), _jsxs("div", { className: classes.summary, children: [_jsxs("p", { children: [_jsx("strong", { children: "Summary" }), "\u00A0"] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Branch name:" }), branch === null || branch === void 0 ? void 0 : branch.name] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Parent branch:" }), branch === null || branch === void 0 ? void 0 : branch.parent] })] }),
|
|
164
|
+
return (_jsxs(_Fragment, { children: [headRendered, _jsxs("div", { className: classes.wrapper, children: [_jsxs("div", { className: classes.container, children: [_jsxs("div", { className: classes.actions, children: [_jsx(Button, { variant: "contained", color: "primary", onClick: () => history.push(props.routes.createClone(props.branchId)), disabled: isReloading, title: 'Create clone', className: classes.actionButton, children: "Create clone" }), _jsx(Button, { variant: "contained", color: "primary", onClick: () => setIsOpenDestroyModal(true), disabled: isReloading, title: 'Delete this branch', className: classes.actionButton, children: "Delete branch" }), _jsxs(Button, { variant: "outlined", color: "secondary", onClick: () => reload(props.branchId, props.instanceId), disabled: isReloading, title: 'Refresh branch information', className: classes.actionButton, children: ["Reload info", isReloading && _jsx(Spinner, { size: "sm", className: classes.spinner })] })] }), _jsx("br", {}), _jsxs("div", { children: [_jsxs("div", { children: [_jsxs("p", { children: [_jsx("strong", { children: "Data state at" }), "\u00A0", _jsx(Tooltip, { content: _jsxs(_Fragment, { children: [_jsx("strong", { children: "Data state time" }), " is a time at which data is\u00A0 recovered for this branch."] }), children: icons.infoIcon })] }), _jsx("p", { className: classes.text, children: (branch === null || branch === void 0 ? void 0 : branch.dataStateAt) || '-' })] }), _jsxs("div", { className: classes.summary, children: [_jsxs("p", { children: [_jsx("strong", { children: "Summary" }), "\u00A0"] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Branch name:" }), branch === null || branch === void 0 ? void 0 : branch.name] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Parent branch:" }), branch === null || branch === void 0 ? void 0 : branch.parent] })] }), _jsxs("div", { className: classes.summary, children: [_jsx("p", { children: _jsx("strong", { children: "Deletion protection" }) }), _jsxs("div", { className: classes.protectionControl, children: [_jsx(Select, { label: "Deletion protection", items: (branch === null || branch === void 0 ? void 0 : branch.protected) && protectedTillDate
|
|
165
|
+
? [
|
|
166
|
+
{
|
|
167
|
+
value: 'current',
|
|
168
|
+
children: `Protected until ${protectedTillDate.toLocaleString()}`,
|
|
169
|
+
},
|
|
170
|
+
...protectionOptions,
|
|
171
|
+
]
|
|
172
|
+
: protectionOptions, value: getProtectionSelectValue(Boolean(branch === null || branch === void 0 ? void 0 : branch.protected), protectedTillDate), onChange: handleProtectionChange, disabled: isReloading || isUpdatingBranch, fullWidth: true }), isUpdatingBranch && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), _jsx("p", { className: classes.text, children: "Protected branches cannot be deleted manually or automatically." }), updateBranchError && (_jsx(ErrorStub, { title: "Update error", message: updateBranchError, className: classes.marginTop }))] }), _jsx("br", {}), _jsx("p", { children: _jsx("strong", { children: "Snapshot info" }) }), _jsxs("div", { className: classes.copyFieldContainer, children: [_jsx(TextField, { variant: "outlined", label: "Snapshot ID", value: branch === null || branch === void 0 ? void 0 : branch.snapshotID, className: classes.textField, margin: "normal", fullWidth: true, InputLabelProps: {
|
|
148
173
|
shrink: true,
|
|
149
174
|
style: styles.inputFieldLabel,
|
|
150
175
|
}, FormHelperTextProps: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GetBranches } from '@postgres.ai/shared/types/api/endpoints/getBranches';
|
|
2
2
|
import { Branch } from '@postgres.ai/shared/types/api/endpoints/getBranches';
|
|
3
3
|
import { DeleteBranch } from '@postgres.ai/shared/types/api/endpoints/deleteBranch';
|
|
4
|
+
import { UpdateBranch } from '@postgres.ai/shared/types/api/endpoints/updateBranch';
|
|
4
5
|
import { SnapshotList, GetSnapshotList } from '@postgres.ai/shared/types/api/endpoints/getSnapshotList';
|
|
5
6
|
import { InitWS } from '@postgres.ai/shared/types/api/endpoints/initWS';
|
|
6
7
|
declare type Error = {
|
|
@@ -11,6 +12,7 @@ export declare type Api = {
|
|
|
11
12
|
getBranches: GetBranches;
|
|
12
13
|
deleteBranch: DeleteBranch;
|
|
13
14
|
getSnapshotList: GetSnapshotList;
|
|
15
|
+
updateBranch?: UpdateBranch;
|
|
14
16
|
initWS?: InitWS;
|
|
15
17
|
};
|
|
16
18
|
export declare class MainStore {
|
|
@@ -19,6 +21,8 @@ export declare class MainStore {
|
|
|
19
21
|
getBranchesError: Error | null;
|
|
20
22
|
isReloading: boolean;
|
|
21
23
|
isBranchesLoading: boolean;
|
|
24
|
+
isUpdatingBranch: boolean;
|
|
25
|
+
updateBranchError: string | null;
|
|
22
26
|
branches: Branch[] | null;
|
|
23
27
|
branch: Branch | null;
|
|
24
28
|
snapshotList: SnapshotList[] | null;
|
|
@@ -28,6 +32,7 @@ export declare class MainStore {
|
|
|
28
32
|
reload: (branchId: string, instanceId: string) => Promise<void>;
|
|
29
33
|
getBranches: (branchId: string, instanceId: string) => Promise<Branch[] | null | undefined>;
|
|
30
34
|
getBranch: (branchId: string, instanceId: string) => Promise<boolean>;
|
|
35
|
+
updateBranchProtection: (branchName: string, instanceId: string, durationMinutes: number | null) => Promise<void>;
|
|
31
36
|
deleteBranch: (branchName: string, instanceId: string) => Promise<{
|
|
32
37
|
response: Response | null;
|
|
33
38
|
error: globalThis.Error | null;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*--------------------------------------------------------------------------
|
|
6
6
|
*/
|
|
7
7
|
import { makeAutoObservable } from 'mobx';
|
|
8
|
+
import { getTextFromUnknownApiError } from '@postgres.ai/shared/utils/api';
|
|
8
9
|
export class MainStore {
|
|
9
10
|
constructor(api) {
|
|
10
11
|
this.getBranchError = null;
|
|
@@ -12,6 +13,8 @@ export class MainStore {
|
|
|
12
13
|
this.getBranchesError = null;
|
|
13
14
|
this.isReloading = false;
|
|
14
15
|
this.isBranchesLoading = false;
|
|
16
|
+
this.isUpdatingBranch = false;
|
|
17
|
+
this.updateBranchError = null;
|
|
15
18
|
this.branches = null;
|
|
16
19
|
this.branch = null;
|
|
17
20
|
this.snapshotList = null;
|
|
@@ -60,6 +63,32 @@ export class MainStore {
|
|
|
60
63
|
}
|
|
61
64
|
return !!currentBranch;
|
|
62
65
|
};
|
|
66
|
+
this.updateBranchProtection = async (branchName, instanceId, durationMinutes) => {
|
|
67
|
+
if (!this.api.updateBranch || !this.branch)
|
|
68
|
+
return;
|
|
69
|
+
this.isUpdatingBranch = true;
|
|
70
|
+
this.updateBranchError = null;
|
|
71
|
+
const prevProtected = this.branch.protected;
|
|
72
|
+
const isProtected = durationMinutes !== null;
|
|
73
|
+
this.branch.protected = isProtected;
|
|
74
|
+
const { response, error } = await this.api.updateBranch({
|
|
75
|
+
instanceId,
|
|
76
|
+
branchName,
|
|
77
|
+
branch: {
|
|
78
|
+
isProtected,
|
|
79
|
+
protectionDurationMinutes: durationMinutes !== null && durationMinutes !== void 0 ? durationMinutes : undefined,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
if (response) {
|
|
83
|
+
await this.reload(branchName, instanceId);
|
|
84
|
+
}
|
|
85
|
+
else if (this.branch) {
|
|
86
|
+
this.branch.protected = prevProtected;
|
|
87
|
+
}
|
|
88
|
+
if (error)
|
|
89
|
+
this.updateBranchError = await getTextFromUnknownApiError(error);
|
|
90
|
+
this.isUpdatingBranch = false;
|
|
91
|
+
};
|
|
63
92
|
this.deleteBranch = async (branchName, instanceId) => {
|
|
64
93
|
var _a;
|
|
65
94
|
if (!branchName)
|
|
@@ -77,7 +77,7 @@ export const BranchesTable = ({ branches, emptyTableText, deleteBranch, branches
|
|
|
77
77
|
if (!state.branches.length) {
|
|
78
78
|
return _jsx("p", { className: classes.marginTop, children: emptyTableText });
|
|
79
79
|
}
|
|
80
|
-
return (_jsx(HorizontalScrollContainer, { children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, {}), _jsx(TableHeaderCell, { children: "Branch" }), _jsx(TableHeaderCell, { children: _jsxs("div", { onClick: handlesortByParent, className: cn(classes.interactiveRow, classes.verticalCentered), children: ["Parent", _jsx(ArrowDropDownIcon, { className: cn(state.sortByParent === 'asc' && classes.sortIconUp, classes.sortIcon) })] }) }), _jsx(TableHeaderCell, { children: "Data state time" }), _jsx(TableHeaderCell, { children: "Latest Snapshot ID" }), _jsx(TableHeaderCell, { children: "Number of snapshots" })] }) }), _jsx(TableBody, { children: (_a = state.branches) === null || _a === void 0 ? void 0 : _a.map((branch) => {
|
|
80
|
+
return (_jsx(HorizontalScrollContainer, { children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, {}), _jsx(TableHeaderCell, { children: "Branch" }), _jsx(TableHeaderCell, { children: _jsxs("div", { onClick: handlesortByParent, className: cn(classes.interactiveRow, classes.verticalCentered), children: ["Parent", _jsx(ArrowDropDownIcon, { className: cn(state.sortByParent === 'asc' && classes.sortIconUp, classes.sortIcon) })] }) }), _jsx(TableHeaderCell, { children: "Data state time" }), _jsx(TableHeaderCell, { children: "Latest Snapshot ID" }), _jsx(TableHeaderCell, { children: "Number of snapshots" }), _jsx(TableHeaderCell, { children: "Protected" })] }) }), _jsx(TableBody, { children: (_a = state.branches) === null || _a === void 0 ? void 0 : _a.map((branch) => {
|
|
81
81
|
const branchPagePath = host.routes.branch(branch.name);
|
|
82
82
|
return (_jsxs(TableRow, { hover: true, onClick: () => history.push(branchPagePath), className: classes.pointerCursor, children: [_jsx(TableBodyCellMenu, { actions: [
|
|
83
83
|
{
|
|
@@ -91,7 +91,7 @@ export const BranchesTable = ({ branches, emptyTableText, deleteBranch, branches
|
|
|
91
91
|
setIsOpenDestroyModal(true);
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
|
-
] }), _jsx(TableBodyCell, { children: branch.name }), _jsx(TableBodyCell, { children: branch.parent }), _jsx(TableBodyCell, { children: formatDateWithDistance(branch.dataStateAt, new Date(branch.dataStateAt)) }), _jsx(TableBodyCell, { children: branch.snapshotID }), _jsx(TableBodyCell, { children: branch.numSnapshots })] }, branch.name));
|
|
94
|
+
] }), _jsx(TableBodyCell, { children: branch.name }), _jsx(TableBodyCell, { children: branch.parent }), _jsx(TableBodyCell, { children: formatDateWithDistance(branch.dataStateAt, new Date(branch.dataStateAt)) }), _jsx(TableBodyCell, { children: branch.snapshotID }), _jsx(TableBodyCell, { children: branch.numSnapshots }), _jsx(TableBodyCell, { children: branch.protected ? 'Yes' : '—' })] }, branch.name));
|
|
95
95
|
}) }), _jsx(DeleteBranchModal, { isOpen: isOpenDestroyModal, onClose: () => {
|
|
96
96
|
setIsOpenDestroyModal(false);
|
|
97
97
|
setBranchId('');
|
|
@@ -33,6 +33,9 @@ const useStyles = makeStyles({
|
|
|
33
33
|
transform: 'translate(-50%, -50%)',
|
|
34
34
|
},
|
|
35
35
|
}, { index: 1 });
|
|
36
|
+
// listRefreshIntervalMs is how often the branch list is silently refreshed so background
|
|
37
|
+
// auto-deletion does not leave stale rows in the table.
|
|
38
|
+
const listRefreshIntervalMs = 30000;
|
|
36
39
|
export const Branches = observer(({ instanceId }) => {
|
|
37
40
|
const host = useHost();
|
|
38
41
|
const stores = useStores();
|
|
@@ -51,7 +54,13 @@ export const Branches = observer(({ instanceId }) => {
|
|
|
51
54
|
};
|
|
52
55
|
useEffect(() => {
|
|
53
56
|
loadBranches();
|
|
54
|
-
|
|
57
|
+
const intervalId = setInterval(() => {
|
|
58
|
+
getBranches(instanceId, true).then((response) => {
|
|
59
|
+
response && setBranches(response);
|
|
60
|
+
});
|
|
61
|
+
}, listRefreshIntervalMs);
|
|
62
|
+
return () => clearInterval(intervalId);
|
|
63
|
+
}, [instanceId]);
|
|
55
64
|
if (!instance && !isBranchesLoading)
|
|
56
65
|
return _jsx(_Fragment, {});
|
|
57
66
|
if (getBranchesError)
|
|
@@ -21,6 +21,9 @@ const useStyles = makeStyles({
|
|
|
21
21
|
},
|
|
22
22
|
message: {
|
|
23
23
|
margin: '4px 0 0 0',
|
|
24
|
+
// A failed upgrade appends up to 20 lines of pg_upgrade log to the WARNING message, and
|
|
25
|
+
// HTML would otherwise collapse them into one unreadable run of text.
|
|
26
|
+
whiteSpace: 'pre-wrap',
|
|
24
27
|
},
|
|
25
28
|
errorMessage: {
|
|
26
29
|
marginTop: '8px',
|
|
@@ -18,6 +18,7 @@ import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner';
|
|
|
18
18
|
import { DestroyCloneRestrictionModal } from '@postgres.ai/shared/components/DestroyCloneRestrictionModal';
|
|
19
19
|
import { DestroyCloneModal } from '@postgres.ai/shared/components/DestroyCloneModal';
|
|
20
20
|
import { ResetCloneModal } from '@postgres.ai/shared/components/ResetCloneModal';
|
|
21
|
+
import { UpgradeCloneModal } from '@postgres.ai/shared/components/UpgradeCloneModal';
|
|
21
22
|
import { Spinner } from '@postgres.ai/shared/components/Spinner';
|
|
22
23
|
import { round } from '@postgres.ai/shared/utils/numbers';
|
|
23
24
|
import { Tooltip } from '@postgres.ai/shared/components/Tooltip';
|
|
@@ -172,11 +173,14 @@ export const Clone = observer((props) => {
|
|
|
172
173
|
const [isOpenRestrictionModal, setIsOpenRestrictionModal] = useState(false);
|
|
173
174
|
const [isOpenDestroyModal, setIsOpenDestroyModal] = useState(false);
|
|
174
175
|
const [isOpenResetModal, setIsOpenResetModal] = useState(false);
|
|
176
|
+
const [isOpenUpgradeModal, setIsOpenUpgradeModal] = useState(false);
|
|
175
177
|
// Initial loading data.
|
|
176
178
|
useEffect(() => {
|
|
177
179
|
stores.main.load(props.instanceId, props.cloneId);
|
|
178
180
|
}, []);
|
|
179
|
-
|
|
181
|
+
// Stop clone status polling on unmount.
|
|
182
|
+
useEffect(() => () => stores.main.stopPolling(), []);
|
|
183
|
+
const { instance, snapshots, clone, isResettingClone, isUpgradingClone, isDestroyingClone, isReloading, isUpdatingClone, isCloneStable, isUpgradeSupported, upgradeTargetVersion, } = stores.main;
|
|
180
184
|
const headRendered = (_jsxs(_Fragment, { children: [_jsx("style", { children: 'p { margin: 0;}' }), props.elements.breadcrumbs, _jsx(SectionTitle, { className: classes.title, tag: "h1", level: 1, text: `Clone ${props.cloneId}`, children: _jsx(InstanceTabs, { tab: TABS_INDEX.CLONES, isPlatform: props.isPlatform, instanceId: props.instanceId, hasLogs: props.api.initWS !== undefined, hideInstanceTabs: props.hideBranchingFeatures }) })] }));
|
|
181
185
|
const cloneErrorMessage = ((_a = stores.main.instanceError) === null || _a === void 0 ? void 0 : _a.message) || ((_b = stores.main.cloneError) === null || _b === void 0 ? void 0 : _b.message);
|
|
182
186
|
const cloneErrorTitle = ((_c = stores.main.instanceError) === null || _c === void 0 ? void 0 : _c.title) || ((_d = stores.main.cloneError) === null || _d === void 0 ? void 0 : _d.title);
|
|
@@ -189,6 +193,9 @@ export const Clone = observer((props) => {
|
|
|
189
193
|
// Clone reset.
|
|
190
194
|
const requestResetClone = () => setIsOpenResetModal(true);
|
|
191
195
|
const resetClone = (snapshotId) => stores.main.resetClone(snapshotId);
|
|
196
|
+
// Clone major upgrade.
|
|
197
|
+
const requestUpgradeClone = () => setIsOpenUpgradeModal(true);
|
|
198
|
+
const upgradeClone = (dockerImage) => stores.main.upgradeClone(dockerImage);
|
|
192
199
|
// Clone destroy.
|
|
193
200
|
const requestDestroyClone = () => {
|
|
194
201
|
if (clone.protected) {
|
|
@@ -254,12 +261,13 @@ export const Clone = observer((props) => {
|
|
|
254
261
|
const hasConnectionInfo = sshPortForwardingUrl || jdbcConnectionStr || psqlConnectionStr;
|
|
255
262
|
// Controls.
|
|
256
263
|
const isDisabledControls = isResettingClone ||
|
|
264
|
+
isUpgradingClone ||
|
|
257
265
|
isDestroyingClone ||
|
|
258
266
|
isReloading ||
|
|
259
267
|
isUpdatingClone ||
|
|
260
268
|
!isCloneStable;
|
|
261
|
-
return (_jsxs(_Fragment, { children: [headRendered, _jsxs("div", { className: classes.wrapper, children: [_jsxs("div", { className: classes.summary, children: [_jsxs("div", { className: classes.actions, children: [_jsxs(Button, { variant: "contained", color: "primary", onClick: requestResetClone, disabled: isDisabledControls, title: 'Reset clone', className: classes.actionButton, children: ["Reset clone", isResettingClone && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), _jsxs(Button, { variant: "contained", color: "primary", onClick: requestDestroyClone, disabled: isDisabledControls, title: 'Delete this clone', className: classes.actionButton, children: ["Delete clone", isDestroyingClone && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), !props.hideBranchingFeatures && _jsxs(Button, { variant: "contained", color: "primary", onClick: createSnapshot, disabled: isDisabledControls, title: 'Create snapshot', className: classes.actionButton, children: ["Create snapshot", snapshots.snapshotDataLoading && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), _jsxs(Button, { variant: "outlined", color: "secondary", onClick: reloadClone, disabled: isDisabledControls, title: 'Refresh clone information', className: classes.actionButton, children: ["Reload info", isReloading && _jsx(Spinner, { size: "sm", className: classes.spinner })] })] }), stores.main.destroyCloneError ||
|
|
262
|
-
(stores.main.resetCloneError && (_jsx(ErrorStub, { title: 'Resetting error', message: stores.main.resetCloneError, className: classes.errorStub }))), !props.hideBranchingFeatures && _jsxs("div", { children: [_jsx("p", { children: _jsx("strong", { children: "Branch" }) }), _jsx("p", { className: classes.text, children: clone.branch })] }), _jsxs("div", { className: classes.title, children: [_jsx("p", { children: _jsx("strong", { children: "Created" }) }), _jsx("p", { className: classes.text, children: clone.createdAt })] }), _jsx("br", {}), _jsxs("div", { children: [_jsxs("p", { children: [_jsx("strong", { children: "Data state at" }), "\u00A0", _jsx(Tooltip, { content: _jsxs(_Fragment, { children: [_jsx("strong", { children: "Data state time" }), " is a time at which data is\u00A0 recovered for this clone."] }), children: icons.infoIcon })] }), _jsx("p", { className: classes.text, children: (_h = clone.snapshot) === null || _h === void 0 ? void 0 : _h.dataStateAt })] }), _jsx("br", {}), _jsxs("div", { children: [_jsx("p", { children: _jsx("strong", { children: "Status" }) }), _jsx(Status, { rawClone: clone, className: classes.status })] }), _jsx("br", {}), _jsxs("div", { children: [_jsxs("p", { children: [_jsx("strong", { children: "Summary" }), "\u00A0", _jsx(Tooltip, { content: _jsxs(_Fragment, { children: [_jsx("strong", { children: "Logical data size" }), " is a logical size of files in PGDATA which is being thin-cloned.", _jsx("br", {}), _jsx("br", {}), _jsx("strong", { children: "Physical data diff size" }), " is an actual size of a\u00A0 thin clone. On creation there is no diff between clone\u2019s\u00A0 and initial data state, all data blocks match. During work\u00A0 with the clone data diverges and data diff size increases.", _jsx("br", {}), _jsx("br", {}), _jsx("strong", { children: "Clone creation time" }), " is time which was\u00A0 spent to provision the clone."] }), children: icons.infoIcon })] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Logical data size:" }), ((_j = instance.state) === null || _j === void 0 ? void 0 : _j.dataSize)
|
|
269
|
+
return (_jsxs(_Fragment, { children: [headRendered, _jsxs("div", { className: classes.wrapper, children: [_jsxs("div", { className: classes.summary, children: [_jsxs("div", { className: classes.actions, children: [_jsxs(Button, { variant: "contained", color: "primary", onClick: requestResetClone, disabled: isDisabledControls, title: 'Reset clone', className: classes.actionButton, children: ["Reset clone", isResettingClone && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), isUpgradeSupported && (_jsxs(Button, { variant: "contained", color: "primary", onClick: requestUpgradeClone, disabled: isDisabledControls, title: 'Upgrade clone to a newer PostgreSQL major version', className: classes.actionButton, children: ["Upgrade clone", isUpgradingClone && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] })), _jsxs(Button, { variant: "contained", color: "primary", onClick: requestDestroyClone, disabled: isDisabledControls, title: 'Delete this clone', className: classes.actionButton, children: ["Delete clone", isDestroyingClone && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), !props.hideBranchingFeatures && _jsxs(Button, { variant: "contained", color: "primary", onClick: createSnapshot, disabled: isDisabledControls, title: 'Create snapshot', className: classes.actionButton, children: ["Create snapshot", snapshots.snapshotDataLoading && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), _jsxs(Button, { variant: "outlined", color: "secondary", onClick: reloadClone, disabled: isDisabledControls, title: 'Refresh clone information', className: classes.actionButton, children: ["Reload info", isReloading && _jsx(Spinner, { size: "sm", className: classes.spinner })] })] }), stores.main.destroyCloneError ||
|
|
270
|
+
(stores.main.resetCloneError && (_jsx(ErrorStub, { title: 'Resetting error', message: stores.main.resetCloneError, className: classes.errorStub }))), stores.main.upgradeCloneError && (_jsx(ErrorStub, { title: 'Upgrade error', message: stores.main.upgradeCloneError, className: classes.errorStub })), !props.hideBranchingFeatures && _jsxs("div", { children: [_jsx("p", { children: _jsx("strong", { children: "Branch" }) }), _jsx("p", { className: classes.text, children: clone.branch })] }), _jsxs("div", { className: classes.title, children: [_jsx("p", { children: _jsx("strong", { children: "Created" }) }), _jsx("p", { className: classes.text, children: clone.createdAt })] }), _jsx("br", {}), _jsxs("div", { children: [_jsxs("p", { children: [_jsx("strong", { children: "Data state at" }), "\u00A0", _jsx(Tooltip, { content: _jsxs(_Fragment, { children: [_jsx("strong", { children: "Data state time" }), " is a time at which data is\u00A0 recovered for this clone."] }), children: icons.infoIcon })] }), _jsx("p", { className: classes.text, children: (_h = clone.snapshot) === null || _h === void 0 ? void 0 : _h.dataStateAt })] }), _jsx("br", {}), _jsxs("div", { children: [_jsx("p", { children: _jsx("strong", { children: "Status" }) }), _jsx(Status, { rawClone: clone, className: classes.status })] }), clone.dbVersion && (_jsxs(_Fragment, { children: [_jsx("br", {}), _jsxs("div", { children: [_jsx("p", { children: _jsx("strong", { children: "PostgreSQL version" }) }), _jsx("p", { className: classes.text, children: clone.dbVersion })] })] })), _jsx("br", {}), _jsxs("div", { children: [_jsxs("p", { children: [_jsx("strong", { children: "Summary" }), "\u00A0", _jsx(Tooltip, { content: _jsxs(_Fragment, { children: [_jsx("strong", { children: "Logical data size" }), " is a logical size of files in PGDATA which is being thin-cloned.", _jsx("br", {}), _jsx("br", {}), _jsx("strong", { children: "Physical data diff size" }), " is an actual size of a\u00A0 thin clone. On creation there is no diff between clone\u2019s\u00A0 and initial data state, all data blocks match. During work\u00A0 with the clone data diverges and data diff size increases.", _jsx("br", {}), _jsx("br", {}), _jsx("strong", { children: "Clone creation time" }), " is time which was\u00A0 spent to provision the clone."] }), children: icons.infoIcon })] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Logical data size:" }), ((_j = instance.state) === null || _j === void 0 ? void 0 : _j.dataSize)
|
|
263
271
|
? formatBytesIEC(instance.state.dataSize)
|
|
264
272
|
: '-'] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Physical data diff size:" }), clone.metadata.cloneDiffSize
|
|
265
273
|
? formatBytesIEC(clone.metadata.cloneDiffSize)
|
|
@@ -291,5 +299,5 @@ export const Clone = observer((props) => {
|
|
|
291
299
|
{ value: 'current', children: `Protected until ${clone.protectedTillDate.toLocaleString()}` },
|
|
292
300
|
...protectionOptions,
|
|
293
301
|
]
|
|
294
|
-
: protectionOptions, value: getCurrentProtectionValue(), onChange: handleProtectionChange, disabled: isDisabledControls, style: { minWidth: 200 } }), isUpdatingClone && _jsx(Spinner, { size: "sm", className: classes.spinner })] }), _jsxs("span", { className: classes.remark, children: ["Select how long this clone should be protected from deletion. Protected clones cannot be deleted manually or automatically.", _jsx("br", {}), "Check disk space regularly and delete this clone once your work is done."] }), stores.main.updateCloneError && (_jsx(ErrorStub, { title: "Updating error", message: stores.main.updateCloneError, className: classes.errorStub }))] }), _jsxs("div", { className: classes.snippetContainer, children: [_jsx(SectionTitle, { tag: "h2", level: 2, text: 'Reset clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can reset the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliResetCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Delete clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can delete the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliDestroyCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Toggle deletion protection using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can toggle deletion protection using CLI for this clone using the following command:" }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(true) }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(false) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Create snapshot for this clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can create a snapshot for this clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCreateSnapshotCommand(props.cloneId) })] }), _jsxs(_Fragment, { children: [_jsx(DestroyCloneRestrictionModal, { isOpen: isOpenRestrictionModal, onClose: () => setIsOpenRestrictionModal(false), cloneId: clone.id }), _jsx(DestroyCloneModal, { isOpen: isOpenDestroyModal, onClose: () => setIsOpenDestroyModal(false), cloneId: clone.id, onDestroyClone: destroyClone }), _jsx(ResetCloneModal, { isOpen: isOpenResetModal, onClose: () => setIsOpenResetModal(false), clone: clone, snapshots: snapshots.data, onResetClone: resetClone, version: (_k = instance.state) === null || _k === void 0 ? void 0 : _k.engine.version })] })] })] }));
|
|
302
|
+
: protectionOptions, value: getCurrentProtectionValue(), onChange: handleProtectionChange, disabled: isDisabledControls, style: { minWidth: 200 } }), isUpdatingClone && _jsx(Spinner, { size: "sm", className: classes.spinner })] }), _jsxs("span", { className: classes.remark, children: ["Select how long this clone should be protected from deletion. Protected clones cannot be deleted manually or automatically.", _jsx("br", {}), "Check disk space regularly and delete this clone once your work is done."] }), stores.main.updateCloneError && (_jsx(ErrorStub, { title: "Updating error", message: stores.main.updateCloneError, className: classes.errorStub }))] }), _jsxs("div", { className: classes.snippetContainer, children: [_jsx(SectionTitle, { tag: "h2", level: 2, text: 'Reset clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can reset the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliResetCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Delete clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can delete the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliDestroyCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Toggle deletion protection using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can toggle deletion protection using CLI for this clone using the following command:" }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(true) }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(false) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Create snapshot for this clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can create a snapshot for this clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCreateSnapshotCommand(props.cloneId) })] }), _jsxs(_Fragment, { children: [_jsx(DestroyCloneRestrictionModal, { isOpen: isOpenRestrictionModal, onClose: () => setIsOpenRestrictionModal(false), cloneId: clone.id }), _jsx(DestroyCloneModal, { isOpen: isOpenDestroyModal, onClose: () => setIsOpenDestroyModal(false), cloneId: clone.id, onDestroyClone: destroyClone }), _jsx(ResetCloneModal, { isOpen: isOpenResetModal, onClose: () => setIsOpenResetModal(false), clone: clone, snapshots: snapshots.data, onResetClone: resetClone, version: (_k = instance.state) === null || _k === void 0 ? void 0 : _k.engine.version }), isUpgradeSupported && upgradeTargetVersion && (_jsx(UpgradeCloneModal, { isOpen: isOpenUpgradeModal, onClose: () => setIsOpenUpgradeModal(false), clone: clone, targetVersion: upgradeTargetVersion, onUpgradeClone: upgradeClone }))] })] })] }));
|
|
295
303
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GetInstance } from '@postgres.ai/shared/types/api/endpoints/getInstance';
|
|
2
2
|
import { GetClone } from '@postgres.ai/shared/types/api/endpoints/getClone';
|
|
3
3
|
import { ResetClone } from '@postgres.ai/shared/types/api/endpoints/resetClone';
|
|
4
|
+
import { UpgradeClone } from '@postgres.ai/shared/types/api/endpoints/upgradeClone';
|
|
4
5
|
import { DestroyClone } from '@postgres.ai/shared/types/api/endpoints/destroyClone';
|
|
5
6
|
import { UpdateClone } from '@postgres.ai/shared/types/api/endpoints/updateClone';
|
|
6
7
|
import { SnapshotsStore, SnapshotsApi } from '@postgres.ai/shared/stores/Snapshots';
|
|
@@ -11,6 +12,7 @@ export declare type Api = SnapshotsApi & {
|
|
|
11
12
|
getInstance: GetInstance;
|
|
12
13
|
getClone: GetClone;
|
|
13
14
|
resetClone: ResetClone;
|
|
15
|
+
upgradeClone?: UpgradeClone;
|
|
14
16
|
destroyClone: DestroyClone;
|
|
15
17
|
updateClone: UpdateClone;
|
|
16
18
|
initWS?: InitWS;
|
|
@@ -27,20 +29,26 @@ export declare class MainStore {
|
|
|
27
29
|
readonly snapshots: SnapshotsStore;
|
|
28
30
|
isResettingClone: boolean;
|
|
29
31
|
resetCloneError: string | null;
|
|
32
|
+
isUpgradingClone: boolean;
|
|
33
|
+
upgradeCloneError: string | null;
|
|
30
34
|
isDestroyingClone: boolean;
|
|
31
35
|
destroyCloneError: string | null;
|
|
32
36
|
isUpdatingClone: boolean;
|
|
33
37
|
updateCloneError: string | null;
|
|
34
38
|
isReloading: boolean;
|
|
35
|
-
private
|
|
39
|
+
private readonly poller;
|
|
36
40
|
private readonly api;
|
|
37
41
|
constructor(api: Api);
|
|
42
|
+
get isUpgradeSupported(): boolean;
|
|
43
|
+
get upgradeTargetVersion(): number | undefined;
|
|
38
44
|
get isCloneStable(): boolean;
|
|
39
45
|
load: (instanceId: string, cloneId: string) => Promise<boolean | undefined>;
|
|
40
46
|
reload: () => Promise<boolean | undefined>;
|
|
47
|
+
stopPolling: () => void;
|
|
41
48
|
private loadInstance;
|
|
42
49
|
private loadClone;
|
|
43
50
|
resetClone: (snapshotId: string) => Promise<boolean>;
|
|
51
|
+
upgradeClone: (dockerImage?: string) => Promise<boolean>;
|
|
44
52
|
destroyClone: () => Promise<boolean>;
|
|
45
53
|
updateCloneProtection: (durationMinutes: number | null) => Promise<void>;
|
|
46
54
|
}
|