@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.
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.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: if-not-present
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
- - pnpm publish "$TARBALL" --no-git-checks
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.1.2",
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": "^12.20.33",
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] })] }), _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: {
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
- const { instance, snapshots, clone, isResettingClone, isDestroyingClone, isReloading, isUpdatingClone, isCloneStable, } = stores.main;
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 cloneUpdateTimeout?;
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
  }