@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/dist/utils/date.js
CHANGED
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
*--------------------------------------------------------------------------
|
|
6
6
|
*/
|
|
7
7
|
import { parse, startOfDay, isSameDay, startOfMonth, startOfWeek, endOfMonth, endOfWeek, differenceInDays, addDays, addMonths, isSameMonth, format, formatDistanceToNowStrict, } from 'date-fns';
|
|
8
|
+
const compactDatePattern = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/;
|
|
9
|
+
// formatDateToISO converts a compact UTC timestamp ('20060102150405') to RFC3339 ('2006-01-02T15:04:05Z').
|
|
10
|
+
// The value is a UTC wall-clock, so it must never be interpreted in the browser timezone.
|
|
8
11
|
export const formatDateToISO = (dateString) => {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
var _a;
|
|
13
|
+
const parts = compactDatePattern.exec((_a = dateString === null || dateString === void 0 ? void 0 : dateString.trim()) !== null && _a !== void 0 ? _a : '');
|
|
14
|
+
if (!parts) {
|
|
11
15
|
return '';
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!isValidDate(parsedDate) || isNaN(parsedDate.getTime())) {
|
|
17
|
-
return '';
|
|
18
|
-
}
|
|
19
|
-
return format(parsedDate, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
// Return empty string for invalid date formats
|
|
17
|
+
const [compact, year, month, day, hours, minutes, seconds] = parts;
|
|
18
|
+
const parsedDate = new Date(Date.UTC(+year, +month - 1, +day, +hours, +minutes, +seconds));
|
|
19
|
+
if (!isValidDate(parsedDate)) {
|
|
23
20
|
return '';
|
|
24
21
|
}
|
|
22
|
+
const isoDate = parsedDate.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
23
|
+
// reject values rolled over by Date.UTC, e.g. month 13 or hour 25.
|
|
24
|
+
return isoDate.replace(/[-:TZ]/g, '') === compact ? isoDate : '';
|
|
25
25
|
};
|
|
26
26
|
// parseDate parses date of both format: '2006-01-02 15:04:05 UTC' and `2006-01-02T15:04:05Z` (RFC3339).
|
|
27
27
|
export const parseDate = (dateStr) => parse(dateStr.replace(' UTC', 'Z').replace('T', ' '), 'yyyy-MM-dd HH:mm:ssX', new Date());
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// protectionOptions lists the deletion-protection choices shared by the branch and snapshot
|
|
2
|
+
// detail pages: no protection, a set of bounded durations, and forever. The values are the
|
|
3
|
+
// protectionDurationMinutes passed to the update endpoints ('0' means forever, 'none' clears
|
|
4
|
+
// protection).
|
|
5
|
+
export const protectionOptions = [
|
|
6
|
+
{ value: 'none', children: 'No protection' },
|
|
7
|
+
{ value: '60', children: '1 hour' },
|
|
8
|
+
{ value: '720', children: '12 hours' },
|
|
9
|
+
{ value: '1440', children: '1 day' },
|
|
10
|
+
{ value: '4320', children: '3 days' },
|
|
11
|
+
{ value: '10080', children: '7 days' },
|
|
12
|
+
{ value: '43200', children: '30 days' },
|
|
13
|
+
{ value: '0', children: 'Forever' },
|
|
14
|
+
];
|
|
15
|
+
// getProtectionSelectValue maps an entity's protection state to the Select value: 'none' when
|
|
16
|
+
// unprotected, 'current' when protected until a specific time, '0' when protected forever.
|
|
17
|
+
export const getProtectionSelectValue = (isProtected, protectedTillDate) => {
|
|
18
|
+
if (!isProtected)
|
|
19
|
+
return 'none';
|
|
20
|
+
if (!protectedTillDate)
|
|
21
|
+
return '0';
|
|
22
|
+
return 'current';
|
|
23
|
+
};
|
package/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",
|
|
@@ -23,8 +23,14 @@ import {
|
|
|
23
23
|
import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub'
|
|
24
24
|
import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner'
|
|
25
25
|
import { SectionTitle } from '@postgres.ai/shared/components/SectionTitle'
|
|
26
|
+
import { Select } from '@postgres.ai/shared/components/Select'
|
|
26
27
|
import { Spinner } from '@postgres.ai/shared/components/Spinner'
|
|
27
28
|
import { Tooltip } from '@postgres.ai/shared/components/Tooltip'
|
|
29
|
+
import { parseDate } from '@postgres.ai/shared/utils/date'
|
|
30
|
+
import {
|
|
31
|
+
protectionOptions,
|
|
32
|
+
getProtectionSelectValue,
|
|
33
|
+
} from '@postgres.ai/shared/utils/protection'
|
|
28
34
|
import { icons } from '@postgres.ai/shared/styles/icons'
|
|
29
35
|
import { styles } from '@postgres.ai/shared/styles/styles'
|
|
30
36
|
import { DeleteBranchModal } from '@postgres.ai/shared/pages/Branches/components/Modals/DeleteBranchModal'
|
|
@@ -96,6 +102,13 @@ const useStyles = makeStyles(
|
|
|
96
102
|
summary: {
|
|
97
103
|
marginTop: 20,
|
|
98
104
|
},
|
|
105
|
+
protectionControl: {
|
|
106
|
+
display: 'flex',
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
marginTop: '8px',
|
|
109
|
+
marginBottom: '8px',
|
|
110
|
+
maxWidth: 525,
|
|
111
|
+
},
|
|
99
112
|
text: {
|
|
100
113
|
marginTop: '4px',
|
|
101
114
|
},
|
|
@@ -156,6 +169,9 @@ export const BranchesPage = observer((props: Props) => {
|
|
|
156
169
|
load,
|
|
157
170
|
isReloading,
|
|
158
171
|
isBranchesLoading,
|
|
172
|
+
isUpdatingBranch,
|
|
173
|
+
updateBranchProtection,
|
|
174
|
+
updateBranchError,
|
|
159
175
|
getBranchesError,
|
|
160
176
|
snapshotListError,
|
|
161
177
|
getBranchError,
|
|
@@ -163,6 +179,19 @@ export const BranchesPage = observer((props: Props) => {
|
|
|
163
179
|
|
|
164
180
|
const hasBranchError = getBranchesError || getBranchError || snapshotListError
|
|
165
181
|
|
|
182
|
+
const protectedTillDate = branch?.protectedTill
|
|
183
|
+
? parseDate(branch.protectedTill)
|
|
184
|
+
: null
|
|
185
|
+
|
|
186
|
+
const handleProtectionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
187
|
+
const value = e.target.value
|
|
188
|
+
updateBranchProtection(
|
|
189
|
+
props.branchId,
|
|
190
|
+
props.instanceId,
|
|
191
|
+
value === 'none' ? null : parseInt(value, 10),
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
166
195
|
const branchLogLength = snapshotList?.reduce((acc, snapshot) => {
|
|
167
196
|
if (snapshot?.branch !== null) {
|
|
168
197
|
return acc + snapshot.branch?.length
|
|
@@ -290,6 +319,47 @@ export const BranchesPage = observer((props: Props) => {
|
|
|
290
319
|
{branch?.parent}
|
|
291
320
|
</p>
|
|
292
321
|
</div>
|
|
322
|
+
<div className={classes.summary}>
|
|
323
|
+
<p>
|
|
324
|
+
<strong>Deletion protection</strong>
|
|
325
|
+
</p>
|
|
326
|
+
<div className={classes.protectionControl}>
|
|
327
|
+
<Select
|
|
328
|
+
label="Deletion protection"
|
|
329
|
+
items={
|
|
330
|
+
branch?.protected && protectedTillDate
|
|
331
|
+
? [
|
|
332
|
+
{
|
|
333
|
+
value: 'current',
|
|
334
|
+
children: `Protected until ${protectedTillDate.toLocaleString()}`,
|
|
335
|
+
},
|
|
336
|
+
...protectionOptions,
|
|
337
|
+
]
|
|
338
|
+
: protectionOptions
|
|
339
|
+
}
|
|
340
|
+
value={getProtectionSelectValue(
|
|
341
|
+
Boolean(branch?.protected),
|
|
342
|
+
protectedTillDate,
|
|
343
|
+
)}
|
|
344
|
+
onChange={handleProtectionChange}
|
|
345
|
+
disabled={isReloading || isUpdatingBranch}
|
|
346
|
+
fullWidth
|
|
347
|
+
/>
|
|
348
|
+
{isUpdatingBranch && (
|
|
349
|
+
<Spinner size="sm" className={classes.spinner} />
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
<p className={classes.text}>
|
|
353
|
+
Protected branches cannot be deleted manually or automatically.
|
|
354
|
+
</p>
|
|
355
|
+
{updateBranchError && (
|
|
356
|
+
<ErrorStub
|
|
357
|
+
title="Update error"
|
|
358
|
+
message={updateBranchError}
|
|
359
|
+
className={classes.marginTop}
|
|
360
|
+
/>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
293
363
|
<br />
|
|
294
364
|
<p>
|
|
295
365
|
<strong>Snapshot info</strong>
|
|
@@ -10,11 +10,13 @@ import { makeAutoObservable } from 'mobx'
|
|
|
10
10
|
import { GetBranches } from '@postgres.ai/shared/types/api/endpoints/getBranches'
|
|
11
11
|
import { Branch } from '@postgres.ai/shared/types/api/endpoints/getBranches'
|
|
12
12
|
import { DeleteBranch } from '@postgres.ai/shared/types/api/endpoints/deleteBranch'
|
|
13
|
+
import { UpdateBranch } from '@postgres.ai/shared/types/api/endpoints/updateBranch'
|
|
13
14
|
import {
|
|
14
15
|
SnapshotList,
|
|
15
16
|
GetSnapshotList,
|
|
16
17
|
} from '@postgres.ai/shared/types/api/endpoints/getSnapshotList'
|
|
17
18
|
import { InitWS } from '@postgres.ai/shared/types/api/endpoints/initWS'
|
|
19
|
+
import { getTextFromUnknownApiError } from '@postgres.ai/shared/utils/api'
|
|
18
20
|
|
|
19
21
|
type Error = {
|
|
20
22
|
title?: string
|
|
@@ -25,6 +27,7 @@ export type Api = {
|
|
|
25
27
|
getBranches: GetBranches
|
|
26
28
|
deleteBranch: DeleteBranch
|
|
27
29
|
getSnapshotList: GetSnapshotList
|
|
30
|
+
updateBranch?: UpdateBranch
|
|
28
31
|
initWS?: InitWS
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -35,6 +38,8 @@ export class MainStore {
|
|
|
35
38
|
|
|
36
39
|
isReloading = false
|
|
37
40
|
isBranchesLoading = false
|
|
41
|
+
isUpdatingBranch = false
|
|
42
|
+
updateBranchError: string | null = null
|
|
38
43
|
|
|
39
44
|
branches: Branch[] | null = null
|
|
40
45
|
branch: Branch | null = null
|
|
@@ -99,6 +104,41 @@ export class MainStore {
|
|
|
99
104
|
return !!currentBranch
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
updateBranchProtection = async (
|
|
108
|
+
branchName: string,
|
|
109
|
+
instanceId: string,
|
|
110
|
+
durationMinutes: number | null,
|
|
111
|
+
) => {
|
|
112
|
+
if (!this.api.updateBranch || !this.branch) return
|
|
113
|
+
|
|
114
|
+
this.isUpdatingBranch = true
|
|
115
|
+
this.updateBranchError = null
|
|
116
|
+
|
|
117
|
+
const prevProtected = this.branch.protected
|
|
118
|
+
const isProtected = durationMinutes !== null
|
|
119
|
+
|
|
120
|
+
this.branch.protected = isProtected
|
|
121
|
+
|
|
122
|
+
const { response, error } = await this.api.updateBranch({
|
|
123
|
+
instanceId,
|
|
124
|
+
branchName,
|
|
125
|
+
branch: {
|
|
126
|
+
isProtected,
|
|
127
|
+
protectionDurationMinutes: durationMinutes ?? undefined,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
if (response) {
|
|
132
|
+
await this.reload(branchName, instanceId)
|
|
133
|
+
} else if (this.branch) {
|
|
134
|
+
this.branch.protected = prevProtected
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (error) this.updateBranchError = await getTextFromUnknownApiError(error)
|
|
138
|
+
|
|
139
|
+
this.isUpdatingBranch = false
|
|
140
|
+
}
|
|
141
|
+
|
|
102
142
|
deleteBranch = async (branchName: string, instanceId: string) => {
|
|
103
143
|
if (!branchName) return
|
|
104
144
|
|
|
@@ -134,6 +134,7 @@ export const BranchesTable = ({
|
|
|
134
134
|
<TableHeaderCell>Data state time</TableHeaderCell>
|
|
135
135
|
<TableHeaderCell>Latest Snapshot ID</TableHeaderCell>
|
|
136
136
|
<TableHeaderCell>Number of snapshots</TableHeaderCell>
|
|
137
|
+
<TableHeaderCell>Protected</TableHeaderCell>
|
|
137
138
|
</TableRow>
|
|
138
139
|
</TableHead>
|
|
139
140
|
<TableBody>
|
|
@@ -170,6 +171,7 @@ export const BranchesTable = ({
|
|
|
170
171
|
</TableBodyCell>
|
|
171
172
|
<TableBodyCell>{branch.snapshotID}</TableBodyCell>
|
|
172
173
|
<TableBodyCell>{branch.numSnapshots}</TableBodyCell>
|
|
174
|
+
<TableBodyCell>{branch.protected ? 'Yes' : '—'}</TableBodyCell>
|
|
173
175
|
</TableRow>
|
|
174
176
|
)
|
|
175
177
|
})}
|
package/pages/Branches/index.tsx
CHANGED
|
@@ -41,6 +41,10 @@ const useStyles = makeStyles(
|
|
|
41
41
|
{ index: 1 },
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
+
// listRefreshIntervalMs is how often the branch list is silently refreshed so background
|
|
45
|
+
// auto-deletion does not leave stale rows in the table.
|
|
46
|
+
const listRefreshIntervalMs = 30_000
|
|
47
|
+
|
|
44
48
|
interface BranchesProps {
|
|
45
49
|
instanceId: string
|
|
46
50
|
}
|
|
@@ -72,7 +76,15 @@ export const Branches: React.FC<BranchesProps> = observer(({ instanceId }) => {
|
|
|
72
76
|
|
|
73
77
|
useEffect(() => {
|
|
74
78
|
loadBranches()
|
|
75
|
-
|
|
79
|
+
|
|
80
|
+
const intervalId = setInterval(() => {
|
|
81
|
+
getBranches(instanceId, true).then((response) => {
|
|
82
|
+
response && setBranches(response)
|
|
83
|
+
})
|
|
84
|
+
}, listRefreshIntervalMs)
|
|
85
|
+
|
|
86
|
+
return () => clearInterval(intervalId)
|
|
87
|
+
}, [instanceId])
|
|
76
88
|
|
|
77
89
|
if (!instance && !isBranchesLoading) return <></>
|
|
78
90
|
|
|
@@ -35,6 +35,9 @@ const useStyles = makeStyles(
|
|
|
35
35
|
},
|
|
36
36
|
message: {
|
|
37
37
|
margin: '4px 0 0 0',
|
|
38
|
+
// A failed upgrade appends up to 20 lines of pg_upgrade log to the WARNING message, and
|
|
39
|
+
// HTML would otherwise collapse them into one unreadable run of text.
|
|
40
|
+
whiteSpace: 'pre-wrap',
|
|
38
41
|
},
|
|
39
42
|
errorMessage: {
|
|
40
43
|
marginTop: '8px',
|
package/pages/Clone/index.tsx
CHANGED
|
@@ -29,6 +29,7 @@ import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner'
|
|
|
29
29
|
import { DestroyCloneRestrictionModal } from '@postgres.ai/shared/components/DestroyCloneRestrictionModal'
|
|
30
30
|
import { DestroyCloneModal } from '@postgres.ai/shared/components/DestroyCloneModal'
|
|
31
31
|
import { ResetCloneModal } from '@postgres.ai/shared/components/ResetCloneModal'
|
|
32
|
+
import { UpgradeCloneModal } from '@postgres.ai/shared/components/UpgradeCloneModal'
|
|
32
33
|
import { Spinner } from '@postgres.ai/shared/components/Spinner'
|
|
33
34
|
import { round } from '@postgres.ai/shared/utils/numbers'
|
|
34
35
|
import { Tooltip } from '@postgres.ai/shared/components/Tooltip'
|
|
@@ -199,21 +200,28 @@ export const Clone = observer((props: Props) => {
|
|
|
199
200
|
const [isOpenRestrictionModal, setIsOpenRestrictionModal] = useState(false)
|
|
200
201
|
const [isOpenDestroyModal, setIsOpenDestroyModal] = useState(false)
|
|
201
202
|
const [isOpenResetModal, setIsOpenResetModal] = useState(false)
|
|
203
|
+
const [isOpenUpgradeModal, setIsOpenUpgradeModal] = useState(false)
|
|
202
204
|
|
|
203
205
|
// Initial loading data.
|
|
204
206
|
useEffect(() => {
|
|
205
207
|
stores.main.load(props.instanceId, props.cloneId)
|
|
206
208
|
}, [])
|
|
207
209
|
|
|
210
|
+
// Stop clone status polling on unmount.
|
|
211
|
+
useEffect(() => () => stores.main.stopPolling(), [])
|
|
212
|
+
|
|
208
213
|
const {
|
|
209
214
|
instance,
|
|
210
215
|
snapshots,
|
|
211
216
|
clone,
|
|
212
217
|
isResettingClone,
|
|
218
|
+
isUpgradingClone,
|
|
213
219
|
isDestroyingClone,
|
|
214
220
|
isReloading,
|
|
215
221
|
isUpdatingClone,
|
|
216
222
|
isCloneStable,
|
|
223
|
+
isUpgradeSupported,
|
|
224
|
+
upgradeTargetVersion,
|
|
217
225
|
} = stores.main
|
|
218
226
|
|
|
219
227
|
const headRendered = (
|
|
@@ -270,6 +278,12 @@ export const Clone = observer((props: Props) => {
|
|
|
270
278
|
|
|
271
279
|
const resetClone = (snapshotId: string) => stores.main.resetClone(snapshotId)
|
|
272
280
|
|
|
281
|
+
// Clone major upgrade.
|
|
282
|
+
const requestUpgradeClone = () => setIsOpenUpgradeModal(true)
|
|
283
|
+
|
|
284
|
+
const upgradeClone = (dockerImage?: string) =>
|
|
285
|
+
stores.main.upgradeClone(dockerImage)
|
|
286
|
+
|
|
273
287
|
// Clone destroy.
|
|
274
288
|
const requestDestroyClone = () => {
|
|
275
289
|
if (clone.protected) {
|
|
@@ -343,6 +357,7 @@ export const Clone = observer((props: Props) => {
|
|
|
343
357
|
// Controls.
|
|
344
358
|
const isDisabledControls =
|
|
345
359
|
isResettingClone ||
|
|
360
|
+
isUpgradingClone ||
|
|
346
361
|
isDestroyingClone ||
|
|
347
362
|
isReloading ||
|
|
348
363
|
isUpdatingClone ||
|
|
@@ -367,6 +382,21 @@ export const Clone = observer((props: Props) => {
|
|
|
367
382
|
<Spinner size="sm" className={classes.spinner} />
|
|
368
383
|
)}
|
|
369
384
|
</Button>
|
|
385
|
+
{isUpgradeSupported && (
|
|
386
|
+
<Button
|
|
387
|
+
variant="contained"
|
|
388
|
+
color="primary"
|
|
389
|
+
onClick={requestUpgradeClone}
|
|
390
|
+
disabled={isDisabledControls}
|
|
391
|
+
title={'Upgrade clone to a newer PostgreSQL major version'}
|
|
392
|
+
className={classes.actionButton}
|
|
393
|
+
>
|
|
394
|
+
Upgrade clone
|
|
395
|
+
{isUpgradingClone && (
|
|
396
|
+
<Spinner size="sm" className={classes.spinner} />
|
|
397
|
+
)}
|
|
398
|
+
</Button>
|
|
399
|
+
)}
|
|
370
400
|
<Button
|
|
371
401
|
variant="contained"
|
|
372
402
|
color="primary"
|
|
@@ -413,6 +443,13 @@ export const Clone = observer((props: Props) => {
|
|
|
413
443
|
className={classes.errorStub}
|
|
414
444
|
/>
|
|
415
445
|
))}
|
|
446
|
+
{stores.main.upgradeCloneError && (
|
|
447
|
+
<ErrorStub
|
|
448
|
+
title={'Upgrade error'}
|
|
449
|
+
message={stores.main.upgradeCloneError}
|
|
450
|
+
className={classes.errorStub}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
416
453
|
{!props.hideBranchingFeatures && <div>
|
|
417
454
|
<p>
|
|
418
455
|
<strong>Branch</strong>
|
|
@@ -451,6 +488,17 @@ export const Clone = observer((props: Props) => {
|
|
|
451
488
|
|
|
452
489
|
<Status rawClone={clone} className={classes.status} />
|
|
453
490
|
</div>
|
|
491
|
+
{clone.dbVersion && (
|
|
492
|
+
<>
|
|
493
|
+
<br />
|
|
494
|
+
<div>
|
|
495
|
+
<p>
|
|
496
|
+
<strong>PostgreSQL version</strong>
|
|
497
|
+
</p>
|
|
498
|
+
<p className={classes.text}>{clone.dbVersion}</p>
|
|
499
|
+
</div>
|
|
500
|
+
</>
|
|
501
|
+
)}
|
|
454
502
|
<br />
|
|
455
503
|
<div>
|
|
456
504
|
<p>
|
|
@@ -749,6 +797,16 @@ export const Clone = observer((props: Props) => {
|
|
|
749
797
|
onResetClone={resetClone}
|
|
750
798
|
version={instance.state?.engine.version}
|
|
751
799
|
/>
|
|
800
|
+
|
|
801
|
+
{isUpgradeSupported && upgradeTargetVersion && (
|
|
802
|
+
<UpgradeCloneModal
|
|
803
|
+
isOpen={isOpenUpgradeModal}
|
|
804
|
+
onClose={() => setIsOpenUpgradeModal(false)}
|
|
805
|
+
clone={clone}
|
|
806
|
+
targetVersion={upgradeTargetVersion}
|
|
807
|
+
onUpgradeClone={upgradeClone}
|
|
808
|
+
/>
|
|
809
|
+
)}
|
|
752
810
|
</>
|
|
753
811
|
</div>
|
|
754
812
|
</>
|
|
@@ -10,6 +10,7 @@ import { makeAutoObservable } from 'mobx'
|
|
|
10
10
|
import { GetInstance } from '@postgres.ai/shared/types/api/endpoints/getInstance'
|
|
11
11
|
import { GetClone } from '@postgres.ai/shared/types/api/endpoints/getClone'
|
|
12
12
|
import { ResetClone } from '@postgres.ai/shared/types/api/endpoints/resetClone'
|
|
13
|
+
import { UpgradeClone } from '@postgres.ai/shared/types/api/endpoints/upgradeClone'
|
|
13
14
|
import { DestroyClone } from '@postgres.ai/shared/types/api/endpoints/destroyClone'
|
|
14
15
|
import { UpdateClone } from '@postgres.ai/shared/types/api/endpoints/updateClone'
|
|
15
16
|
import {
|
|
@@ -19,15 +20,15 @@ import {
|
|
|
19
20
|
import { Clone } from '@postgres.ai/shared/types/api/entities/clone'
|
|
20
21
|
import { Instance } from '@postgres.ai/shared/types/api/entities/instance'
|
|
21
22
|
import { checkIsCloneStable } from '@postgres.ai/shared/utils/clone'
|
|
23
|
+
import { ClonePoller } from '@postgres.ai/shared/utils/clonePoller'
|
|
22
24
|
import { getTextFromUnknownApiError } from '@postgres.ai/shared/utils/api'
|
|
23
25
|
import { InitWS } from '@postgres.ai/shared/types/api/endpoints/initWS'
|
|
24
26
|
|
|
25
|
-
const UNSTABLE_CLONE_UPDATE_TIMEOUT = 1000
|
|
26
|
-
|
|
27
27
|
export type Api = SnapshotsApi & {
|
|
28
28
|
getInstance: GetInstance
|
|
29
29
|
getClone: GetClone
|
|
30
30
|
resetClone: ResetClone
|
|
31
|
+
upgradeClone?: UpgradeClone
|
|
31
32
|
destroyClone: DestroyClone
|
|
32
33
|
updateClone: UpdateClone
|
|
33
34
|
initWS?: InitWS
|
|
@@ -50,6 +51,9 @@ export class MainStore {
|
|
|
50
51
|
isResettingClone = false
|
|
51
52
|
resetCloneError: string | null = null
|
|
52
53
|
|
|
54
|
+
isUpgradingClone = false
|
|
55
|
+
upgradeCloneError: string | null = null
|
|
56
|
+
|
|
53
57
|
isDestroyingClone = false
|
|
54
58
|
destroyCloneError: string | null = null
|
|
55
59
|
|
|
@@ -58,7 +62,7 @@ export class MainStore {
|
|
|
58
62
|
|
|
59
63
|
isReloading = false
|
|
60
64
|
|
|
61
|
-
private
|
|
65
|
+
private readonly poller = new ClonePoller()
|
|
62
66
|
|
|
63
67
|
private readonly api: Api
|
|
64
68
|
|
|
@@ -69,12 +73,29 @@ export class MainStore {
|
|
|
69
73
|
makeAutoObservable(this)
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
// The closed Platform app implements the same Api and may not provide the endpoint, and the
|
|
77
|
+
// engine only accepts an upgrade once one is configured, so the action is hidden rather than
|
|
78
|
+
// offered and then refused.
|
|
79
|
+
get isUpgradeSupported() {
|
|
80
|
+
return Boolean(this.api.upgradeClone) && Boolean(this.upgradeTargetVersion)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The major an upgrade lands on. It follows from the instance's upgrade image, so the UI reads
|
|
84
|
+
// it instead of asking for one.
|
|
85
|
+
get upgradeTargetVersion() {
|
|
86
|
+
const cloneUpgrade = this.instance?.state?.cloneUpgrade
|
|
87
|
+
|
|
88
|
+
return cloneUpgrade?.available ? cloneUpgrade.targetVersion : undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
get isCloneStable() {
|
|
73
92
|
if (!this.clone) return false
|
|
74
93
|
return checkIsCloneStable(this.clone)
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
load = async (instanceId: string, cloneId: string) => {
|
|
97
|
+
this.poller.start()
|
|
98
|
+
|
|
78
99
|
const [isInstanceOk, isCloneOk, isSnapshotsLoaded] = await Promise.all([
|
|
79
100
|
this.loadInstance(instanceId),
|
|
80
101
|
this.loadClone(instanceId, cloneId),
|
|
@@ -92,6 +113,8 @@ export class MainStore {
|
|
|
92
113
|
return isSuccess
|
|
93
114
|
}
|
|
94
115
|
|
|
116
|
+
stopPolling = () => this.poller.stop()
|
|
117
|
+
|
|
95
118
|
private loadInstance = async (instanceId: string) => {
|
|
96
119
|
const { response, error } = await this.api.getInstance({
|
|
97
120
|
instanceId,
|
|
@@ -116,7 +139,9 @@ export class MainStore {
|
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
private loadClone = async (instanceId: string, cloneId: string) => {
|
|
119
|
-
|
|
142
|
+
if (this.poller.isStopped) return false
|
|
143
|
+
|
|
144
|
+
this.poller.cancel()
|
|
120
145
|
|
|
121
146
|
const { response, error } = await this.api.getClone({ instanceId, cloneId })
|
|
122
147
|
|
|
@@ -124,10 +149,7 @@ export class MainStore {
|
|
|
124
149
|
this.clone = response
|
|
125
150
|
|
|
126
151
|
if (!this.isCloneStable)
|
|
127
|
-
this.
|
|
128
|
-
() => this.loadClone(instanceId, cloneId),
|
|
129
|
-
UNSTABLE_CLONE_UPDATE_TIMEOUT,
|
|
130
|
-
)
|
|
152
|
+
this.poller.scheduleNext(() => this.loadClone(instanceId, cloneId))
|
|
131
153
|
}
|
|
132
154
|
|
|
133
155
|
if (error) {
|
|
@@ -166,6 +188,27 @@ export class MainStore {
|
|
|
166
188
|
return Boolean(response)
|
|
167
189
|
}
|
|
168
190
|
|
|
191
|
+
upgradeClone = async (dockerImage?: string) => {
|
|
192
|
+
if (!this.instance || !this.clone || !this.api.upgradeClone) return false
|
|
193
|
+
|
|
194
|
+
this.isUpgradingClone = true
|
|
195
|
+
this.upgradeCloneError = null
|
|
196
|
+
|
|
197
|
+
const { response, error } = await this.api.upgradeClone({
|
|
198
|
+
instanceId: this.instance.id,
|
|
199
|
+
cloneId: this.clone.id,
|
|
200
|
+
dockerImage,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (response) await this.loadClone(this.instance.id, this.clone.id)
|
|
204
|
+
|
|
205
|
+
if (error) this.upgradeCloneError = await getTextFromUnknownApiError(error)
|
|
206
|
+
|
|
207
|
+
this.isUpgradingClone = false
|
|
208
|
+
|
|
209
|
+
return Boolean(response)
|
|
210
|
+
}
|
|
211
|
+
|
|
169
212
|
destroyClone = async () => {
|
|
170
213
|
if (!this.instance || !this.clone) return false
|
|
171
214
|
|
|
@@ -166,6 +166,9 @@ export const CreateClone = observer((props: Props) => {
|
|
|
166
166
|
history.push(props.routes.clone(stores.main.clone.id))
|
|
167
167
|
}, [stores.main.clone, stores.main.isCloneStable])
|
|
168
168
|
|
|
169
|
+
// Stop clone status polling on unmount.
|
|
170
|
+
useEffect(() => () => stores.main.stopPolling(), [])
|
|
171
|
+
|
|
169
172
|
const headRendered = (
|
|
170
173
|
<>
|
|
171
174
|
{/* //TODO: make global reset styles. */}
|
|
@@ -13,12 +13,11 @@ import {
|
|
|
13
13
|
} from '@postgres.ai/shared/stores/Snapshots'
|
|
14
14
|
import { getTextFromUnknownApiError } from '@postgres.ai/shared/utils/api'
|
|
15
15
|
import { checkIsCloneStable } from '@postgres.ai/shared/utils/clone'
|
|
16
|
+
import { ClonePoller } from '@postgres.ai/shared/utils/clonePoller'
|
|
16
17
|
|
|
17
18
|
import { FormValues } from '../useForm'
|
|
18
19
|
import { InitWS } from '@postgres.ai/shared/types/api/endpoints/initWS'
|
|
19
20
|
|
|
20
|
-
const UNSTABLE_CLONE_UPDATE_TIMEOUT = 1000
|
|
21
|
-
|
|
22
21
|
export type MainStoreApi = SnapshotsApi & {
|
|
23
22
|
getInstance: GetInstance
|
|
24
23
|
createClone: CreateClone
|
|
@@ -37,7 +36,7 @@ export class MainStore {
|
|
|
37
36
|
clone: Clone | null = null
|
|
38
37
|
cloneError: string | null = null
|
|
39
38
|
|
|
40
|
-
private
|
|
39
|
+
private readonly poller = new ClonePoller()
|
|
41
40
|
|
|
42
41
|
private readonly api: MainStoreApi
|
|
43
42
|
|
|
@@ -73,6 +72,8 @@ export class MainStore {
|
|
|
73
72
|
createClone = async (data: FormValues) => {
|
|
74
73
|
if (!this.instance) return false
|
|
75
74
|
|
|
75
|
+
this.poller.start()
|
|
76
|
+
|
|
76
77
|
const { response, error } = await this.api.createClone({
|
|
77
78
|
...data,
|
|
78
79
|
instanceId: this.instance.id,
|
|
@@ -115,11 +116,15 @@ export class MainStore {
|
|
|
115
116
|
return response
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
stopPolling = () => this.poller.stop()
|
|
120
|
+
|
|
118
121
|
private updateCloneUntilStable = async (args: {
|
|
119
122
|
instanceId: string
|
|
120
123
|
cloneId: string
|
|
121
124
|
}) => {
|
|
122
|
-
|
|
125
|
+
if (this.poller.isStopped) return
|
|
126
|
+
|
|
127
|
+
this.poller.cancel()
|
|
123
128
|
|
|
124
129
|
const { response, error } = await this.api.getClone(args)
|
|
125
130
|
|
|
@@ -127,10 +132,7 @@ export class MainStore {
|
|
|
127
132
|
this.clone = response
|
|
128
133
|
|
|
129
134
|
if (!this.isCloneStable)
|
|
130
|
-
this.
|
|
131
|
-
() => this.updateCloneUntilStable(args),
|
|
132
|
-
UNSTABLE_CLONE_UPDATE_TIMEOUT,
|
|
133
|
-
)
|
|
135
|
+
this.poller.scheduleNext(() => this.updateCloneUntilStable(args))
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
if (error)
|