@postgres.ai/shared 4.1.3 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.gitlab-ci.yml +7 -3
  2. package/components/UpgradeCloneModal/index.tsx +145 -0
  3. package/dist/components/UpgradeCloneModal/index.d.ts +11 -0
  4. package/dist/components/UpgradeCloneModal/index.js +75 -0
  5. package/dist/package.json +2 -2
  6. package/dist/pages/Branches/Branch/index.js +27 -2
  7. package/dist/pages/Branches/Branch/stores/Main.d.ts +5 -0
  8. package/dist/pages/Branches/Branch/stores/Main.js +29 -0
  9. package/dist/pages/Branches/components/BranchesTable/index.js +2 -2
  10. package/dist/pages/Branches/index.js +10 -1
  11. package/dist/pages/Clone/Status/index.js +3 -0
  12. package/dist/pages/Clone/index.js +12 -4
  13. package/dist/pages/Clone/stores/Main.d.ts +9 -1
  14. package/dist/pages/Clone/stores/Main.js +40 -3
  15. package/dist/pages/CreateBranch/stores/Main.d.ts +3 -0
  16. package/dist/pages/CreateClone/index.js +2 -0
  17. package/dist/pages/CreateClone/stores/Main.d.ts +5 -1
  18. package/dist/pages/CreateClone/stores/Main.js +8 -3
  19. package/dist/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.d.ts +11 -0
  20. package/dist/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.js +24 -0
  21. package/dist/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.d.ts +10 -0
  22. package/dist/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.js +29 -0
  23. package/dist/pages/Instance/Configuration/PhysicalMode/Sync/index.d.ts +9 -0
  24. package/dist/pages/Instance/Configuration/PhysicalMode/Sync/index.js +14 -0
  25. package/dist/pages/Instance/Configuration/PhysicalMode/Walg/index.d.ts +10 -0
  26. package/dist/pages/Instance/Configuration/PhysicalMode/Walg/index.js +21 -0
  27. package/dist/pages/Instance/Configuration/PhysicalMode/index.d.ts +10 -0
  28. package/dist/pages/Instance/Configuration/PhysicalMode/index.js +17 -0
  29. package/dist/pages/Instance/Configuration/SimpleMode/PreviewCard.d.ts +13 -0
  30. package/dist/pages/Instance/Configuration/SimpleMode/PreviewCard.js +14 -0
  31. package/dist/pages/Instance/Configuration/SimpleMode/index.d.ts +19 -0
  32. package/dist/pages/Instance/Configuration/SimpleMode/index.js +166 -0
  33. package/dist/pages/Instance/Configuration/configMode.d.ts +2 -0
  34. package/dist/pages/Instance/Configuration/configMode.js +7 -0
  35. package/dist/pages/Instance/Configuration/configOptions.d.ts +16 -0
  36. package/dist/pages/Instance/Configuration/configOptions.js +71 -0
  37. package/dist/pages/Instance/Configuration/connectionString.d.ts +20 -0
  38. package/dist/pages/Instance/Configuration/connectionString.js +129 -0
  39. package/dist/pages/Instance/Configuration/dockerCatalog.d.ts +2 -0
  40. package/dist/pages/Instance/Configuration/dockerCatalog.js +19 -0
  41. package/dist/pages/Instance/Configuration/index.d.ts +1 -2
  42. package/dist/pages/Instance/Configuration/index.js +115 -86
  43. package/dist/pages/Instance/Configuration/useForm.d.ts +20 -0
  44. package/dist/pages/Instance/Configuration/useForm.js +126 -5
  45. package/dist/pages/Instance/Configuration/utils/index.js +1 -17
  46. package/dist/pages/Instance/Info/Snapshots/Calendar/Day/index.js +3 -3
  47. package/dist/pages/Instance/Info/Snapshots/Calendar/utils.d.ts +3 -0
  48. package/dist/pages/Instance/Info/Snapshots/utils.d.ts +6 -0
  49. package/dist/pages/Instance/Snapshots/components/SnapshotsList/index.js +11 -1
  50. package/dist/pages/Instance/Snapshots/index.js +11 -3
  51. package/dist/pages/Instance/Snapshots/utils/index.d.ts +3 -0
  52. package/dist/pages/Instance/Tabs/styles.js +6 -6
  53. package/dist/pages/Instance/index.js +1 -3
  54. package/dist/pages/Instance/stores/Main.d.ts +22 -2
  55. package/dist/pages/Instance/stores/Main.js +26 -9
  56. package/dist/pages/Snapshots/Snapshot/index.js +28 -2
  57. package/dist/pages/Snapshots/Snapshot/stores/Main.d.ts +5 -0
  58. package/dist/pages/Snapshots/Snapshot/stores/Main.js +29 -0
  59. package/dist/stores/Snapshots.d.ts +1 -1
  60. package/dist/stores/Snapshots.js +12 -6
  61. package/dist/types/api/endpoints/getBranches.d.ts +6 -0
  62. package/dist/types/api/endpoints/probeSource.d.ts +32 -0
  63. package/dist/types/api/endpoints/probeSource.js +1 -0
  64. package/dist/types/api/endpoints/testDbSource.d.ts +3 -1
  65. package/dist/types/api/endpoints/testDbSource.js +6 -0
  66. package/dist/types/api/endpoints/updateBranch.d.ts +11 -0
  67. package/dist/types/api/endpoints/updateBranch.js +1 -0
  68. package/dist/types/api/endpoints/updateSnapshot.d.ts +11 -0
  69. package/dist/types/api/endpoints/updateSnapshot.js +1 -0
  70. package/dist/types/api/endpoints/upgradeClone.d.ts +8 -0
  71. package/dist/types/api/endpoints/upgradeClone.js +1 -0
  72. package/dist/types/api/entities/branchSnapshots.d.ts +3 -0
  73. package/dist/types/api/entities/clone.d.ts +9 -2
  74. package/dist/types/api/entities/config.d.ts +32 -0
  75. package/dist/types/api/entities/config.js +45 -18
  76. package/dist/types/api/entities/instance.d.ts +11 -1
  77. package/dist/types/api/entities/instanceState.d.ts +16 -1
  78. package/dist/types/api/entities/snapshot.d.ts +6 -0
  79. package/dist/utils/api.d.ts +1 -1
  80. package/dist/utils/api.js +6 -0
  81. package/dist/utils/clone.d.ts +3 -1
  82. package/dist/utils/clone.js +7 -1
  83. package/dist/utils/clonePoller.d.ts +12 -0
  84. package/dist/utils/clonePoller.js +41 -0
  85. package/dist/utils/date.js +12 -12
  86. package/dist/utils/protection.d.ts +5 -0
  87. package/dist/utils/protection.js +23 -0
  88. package/package.json +2 -2
  89. package/pages/Branches/Branch/index.tsx +70 -0
  90. package/pages/Branches/Branch/stores/Main.ts +40 -0
  91. package/pages/Branches/components/BranchesTable/index.tsx +2 -0
  92. package/pages/Branches/index.tsx +13 -1
  93. package/pages/Clone/Status/index.tsx +3 -0
  94. package/pages/Clone/index.tsx +58 -0
  95. package/pages/Clone/stores/Main.ts +51 -8
  96. package/pages/CreateClone/index.tsx +3 -0
  97. package/pages/CreateClone/stores/Main.ts +10 -8
  98. package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.tsx +129 -0
  99. package/pages/Instance/Configuration/PhysicalMode/PgBackRest/index.tsx +89 -0
  100. package/pages/Instance/Configuration/PhysicalMode/Sync/index.tsx +64 -0
  101. package/pages/Instance/Configuration/PhysicalMode/Walg/index.tsx +63 -0
  102. package/pages/Instance/Configuration/PhysicalMode/index.tsx +93 -0
  103. package/pages/Instance/Configuration/SimpleMode/PreviewCard.tsx +176 -0
  104. package/pages/Instance/Configuration/SimpleMode/index.tsx +303 -0
  105. package/pages/Instance/Configuration/configMode.ts +12 -0
  106. package/pages/Instance/Configuration/configOptions.ts +111 -0
  107. package/pages/Instance/Configuration/connectionString.ts +157 -0
  108. package/pages/Instance/Configuration/dockerCatalog.ts +21 -0
  109. package/pages/Instance/Configuration/index.tsx +151 -49
  110. package/pages/Instance/Configuration/useForm.ts +155 -5
  111. package/pages/Instance/Configuration/utils/index.ts +1 -17
  112. package/pages/Instance/Info/Snapshots/Calendar/Day/index.tsx +3 -3
  113. package/pages/Instance/Snapshots/components/SnapshotsList/index.tsx +21 -1
  114. package/pages/Instance/Snapshots/index.tsx +15 -5
  115. package/pages/Instance/Tabs/styles.ts +7 -7
  116. package/pages/Instance/index.tsx +0 -4
  117. package/pages/Instance/stores/Main.ts +29 -7
  118. package/pages/Snapshots/Snapshot/index.tsx +71 -0
  119. package/pages/Snapshots/Snapshot/stores/Main.ts +40 -0
  120. package/postgres.ai-shared-4.2.0.tgz +0 -0
  121. package/stores/Snapshots.ts +16 -7
  122. package/types/api/endpoints/getBranches.ts +3 -0
  123. package/types/api/endpoints/probeSource.ts +34 -0
  124. package/types/api/endpoints/testDbSource.ts +10 -1
  125. package/types/api/endpoints/updateBranch.ts +11 -0
  126. package/types/api/endpoints/updateSnapshot.ts +11 -0
  127. package/types/api/endpoints/upgradeClone.ts +8 -0
  128. package/types/api/entities/clone.ts +13 -1
  129. package/types/api/entities/config.ts +46 -0
  130. package/types/api/entities/instanceState.ts +6 -0
  131. package/types/api/entities/snapshot.ts +3 -0
  132. package/utils/api.ts +8 -0
  133. package/utils/clone.ts +7 -1
  134. package/utils/clonePoller.ts +47 -0
  135. package/utils/date.ts +19 -14
  136. package/utils/protection.ts +25 -0
  137. package/postgres.ai-shared-4.1.3.tgz +0 -0
@@ -1,8 +1,7 @@
1
- import { makeStyles } from '@material-ui/core'
2
- import { colors } from '@postgres.ai/shared/styles/colors'
1
+ import { makeStyles, Theme } from '@material-ui/core'
3
2
 
4
3
  export const useTabsStyles = makeStyles(
5
- {
4
+ (theme: Theme) => ({
6
5
  tabsRoot: {
7
6
  minHeight: 0,
8
7
  marginTop: '-8px',
@@ -24,7 +23,7 @@ export const useTabsStyles = makeStyles(
24
23
  height: '18px',
25
24
  },
26
25
  '& a': {
27
- color: colors.black,
26
+ color: theme.palette.text.primary,
28
27
  textDecoration: 'none',
29
28
 
30
29
  '@media (max-width: 700px)': {
@@ -43,18 +42,19 @@ export const useTabsStyles = makeStyles(
43
42
  height: '3px',
44
43
  },
45
44
  tabRoot: {
45
+ color: theme.palette.text.primary,
46
46
  fontWeight: 400,
47
47
  minWidth: 0,
48
48
  minHeight: 0,
49
49
  width: '100%',
50
50
  padding: '6px 16px',
51
- borderBottom: `3px solid ${colors.consoleStroke}`,
51
+ borderBottom: `3px solid ${theme.palette.divider}`,
52
52
  '& + $tabRoot': {
53
53
  marginLeft: '10px',
54
54
  },
55
55
  '&.Mui-disabled': {
56
56
  opacity: 1,
57
- color: colors.pgaiDarkGray,
57
+ color: theme.palette.text.disabled,
58
58
  },
59
59
  '@media (max-width: 700px)': {
60
60
  width: 'max-content',
@@ -63,6 +63,6 @@ export const useTabsStyles = makeStyles(
63
63
  tabHidden: {
64
64
  display: 'none',
65
65
  },
66
- },
66
+ }),
67
67
  { index: 1 },
68
68
  )
@@ -96,8 +96,6 @@ export const Instance = observer((props: Props) => {
96
96
  instanceRetrieval ||
97
97
  (!isLoadingInstance && !isLoadingInstanceRetrieval && instance && instance?.url && !instanceError)
98
98
 
99
- const isConfigurationActive = instanceRetrieval?.mode !== 'physical'
100
-
101
99
  useEffect(() => {
102
100
  load(instanceId, isPlatform)
103
101
  }, [instanceId])
@@ -111,7 +109,6 @@ export const Instance = observer((props: Props) => {
111
109
  if (
112
110
  instance &&
113
111
  instance.state?.retrieving?.status === 'pending' &&
114
- isConfigurationActive &&
115
112
  !hasBeenRedirected
116
113
  ) {
117
114
  setActiveTab(TABS_INDEX.CONFIGURATION)
@@ -193,7 +190,6 @@ export const Instance = observer((props: Props) => {
193
190
  <Configuration
194
191
  instanceId={instanceId}
195
192
  switchActiveTab={switchTab}
196
- isConfigurationActive={isConfigurationActive}
197
193
  reload={() => load(props.instanceId)}
198
194
  disableConfigModification={
199
195
  instance?.state?.engine.disableConfigModification
@@ -13,6 +13,7 @@ import { Config } from '@postgres.ai/shared/types/api/entities/config'
13
13
  import { GetConfig } from '@postgres.ai/shared/types/api/endpoints/getConfig'
14
14
  import { UpdateConfig } from '@postgres.ai/shared/types/api/endpoints/updateConfig'
15
15
  import { TestDbSource } from '@postgres.ai/shared/types/api/endpoints/testDbSource'
16
+ import { ProbeSource } from '@postgres.ai/shared/types/api/endpoints/probeSource'
16
17
  import { RefreshInstance } from '@postgres.ai/shared/types/api/endpoints/refreshInstance'
17
18
  import { DestroyClone } from '@postgres.ai/shared/types/api/endpoints/destroyClone'
18
19
  import { ResetClone } from '@postgres.ai/shared/types/api/endpoints/resetClone'
@@ -47,6 +48,7 @@ export type Api = {
47
48
  getConfig?: GetConfig
48
49
  updateConfig?: UpdateConfig
49
50
  testDbSource?: TestDbSource
51
+ probeSource?: ProbeSource
50
52
  getFullConfig?: GetFullConfig
51
53
  getSeImages?: GetSeImages
52
54
  getEngine?: GetEngine
@@ -169,9 +171,9 @@ export class MainStore {
169
171
  })
170
172
  }
171
173
 
172
- reloadSnapshots = async (branchName?: string) => {
174
+ reloadSnapshots = async (branchName?: string, silent?: boolean) => {
173
175
  if (!this.instance) return
174
- await this.snapshots.reload(this.instance.id, branchName)
176
+ await this.snapshots.reload(this.instance.id, branchName, silent)
175
177
  }
176
178
 
177
179
  reloadInstanceRetrieval = async () => {
@@ -291,7 +293,12 @@ export class MainStore {
291
293
  this.fullConfig = response
292
294
 
293
295
  const splitYML = this.fullConfig.split('---')
294
- this.platformUrl = splitYML[0]?.split('url: ')[1]?.split('\n')[0]
296
+ const platformSection = splitYML[0]?.split('\nplatform:')[1] ?? ''
297
+ const rawPlatformUrl = platformSection.split('url: ')[1]?.split('\n')[0]
298
+ this.platformUrl = rawPlatformUrl
299
+ ?.replace(/['"]+/g, '')
300
+ .trim()
301
+ .split(/\s+/)[0]
295
302
  this.uiVersion = splitYML[0]
296
303
  ?.split('dockerImage: "postgresai/ce-ui:')[2]
297
304
  ?.split('\n')[0]
@@ -351,6 +358,17 @@ export class MainStore {
351
358
  }
352
359
  }
353
360
 
361
+ probeSource = async (values: { url: string; password: string }) => {
362
+ if (!this.api.probeSource) return
363
+
364
+ const { response, error } = await this.api.probeSource(values)
365
+
366
+ return {
367
+ response,
368
+ error,
369
+ }
370
+ }
371
+
354
372
  resetClone = async (cloneId: string, snapshotId: string) => {
355
373
  if (!this.instance || !this.api.resetClone) return
356
374
 
@@ -402,15 +420,19 @@ export class MainStore {
402
420
  this.isReloadingClones = false
403
421
  }
404
422
 
405
- getBranches = async (instanceId: string) => {
423
+ getBranches = async (instanceId: string, silent?: boolean) => {
406
424
  if (!this.api.getBranches) return
407
- this.isBranchesLoading = true
425
+ if (!silent) this.isBranchesLoading = true
408
426
 
409
427
  const { response, error } = await this.api.getBranches(instanceId)
410
428
 
411
- this.isBranchesLoading = false
429
+ if (!silent) this.isBranchesLoading = false
430
+
431
+ if (response) this.getBranchesError = null
412
432
 
413
- if (error) this.getBranchesError = await error.json().then((err) => err)
433
+ // a silent background refresh must not replace the rendered list with an error stub on a
434
+ // transient failure; errors surface only on a foreground load.
435
+ if (error && !silent) this.getBranchesError = await error.json().then((err) => err)
414
436
 
415
437
  return response
416
438
  }
@@ -23,11 +23,18 @@ 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'
27
+ import { Spinner } from '@postgres.ai/shared/components/Spinner'
26
28
  import { DestroySnapshotModal } from '@postgres.ai/shared/pages/Snapshots/Snapshot/DestorySnapshotModal'
27
29
  import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer'
28
30
  import { Tooltip } from '@postgres.ai/shared/components/Tooltip'
29
31
  import { icons } from '@postgres.ai/shared/styles/icons'
30
32
  import { formatBytesIEC } from '@postgres.ai/shared/utils/units'
33
+ import { parseDate } from '@postgres.ai/shared/utils/date'
34
+ import {
35
+ protectionOptions,
36
+ getProtectionSelectValue,
37
+ } from '@postgres.ai/shared/utils/protection'
31
38
  import { styles } from '@postgres.ai/shared/styles/styles'
32
39
  import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight'
33
40
  import {
@@ -94,6 +101,13 @@ const useStyles = makeStyles(
94
101
  summary: {
95
102
  marginTop: 20,
96
103
  },
104
+ protectionControl: {
105
+ display: 'flex',
106
+ alignItems: 'center',
107
+ marginTop: '8px',
108
+ marginBottom: '8px',
109
+ maxWidth: 525,
110
+ },
97
111
  text: {
98
112
  marginTop: '4px',
99
113
  },
@@ -160,6 +174,9 @@ export const SnapshotPage = observer((props: Props) => {
160
174
  snapshot,
161
175
  branchSnapshot,
162
176
  isSnapshotsLoading,
177
+ isUpdatingSnapshot,
178
+ updateSnapshotProtection,
179
+ updateSnapshotError,
163
180
  snapshotError,
164
181
  branchSnapshotError,
165
182
  load,
@@ -170,6 +187,19 @@ export const SnapshotPage = observer((props: Props) => {
170
187
  load(props.snapshotId, props.instanceId)
171
188
  }
172
189
 
190
+ const protectedTillDate = snapshot?.protectedTill
191
+ ? parseDate(snapshot.protectedTill)
192
+ : null
193
+
194
+ const handleProtectionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
195
+ const value = e.target.value
196
+ updateSnapshotProtection(
197
+ props.snapshotId,
198
+ props.instanceId,
199
+ value === 'none' ? null : parseInt(value, 10),
200
+ )
201
+ }
202
+
173
203
  const headRendered = (
174
204
  <>
175
205
  <style>{'p { margin: 0;}'}</style>
@@ -291,6 +321,47 @@ export const SnapshotPage = observer((props: Props) => {
291
321
  </p>
292
322
  )}
293
323
  </div>
324
+ <div className={classes.summary}>
325
+ <p>
326
+ <strong>Deletion protection</strong>
327
+ </p>
328
+ <div className={classes.protectionControl}>
329
+ <Select
330
+ label="Deletion protection"
331
+ items={
332
+ snapshot?.protected && protectedTillDate
333
+ ? [
334
+ {
335
+ value: 'current',
336
+ children: `Protected until ${protectedTillDate.toLocaleString()}`,
337
+ },
338
+ ...protectionOptions,
339
+ ]
340
+ : protectionOptions
341
+ }
342
+ value={getProtectionSelectValue(
343
+ Boolean(snapshot?.protected),
344
+ protectedTillDate,
345
+ )}
346
+ onChange={handleProtectionChange}
347
+ disabled={isUpdatingSnapshot}
348
+ fullWidth
349
+ />
350
+ {isUpdatingSnapshot && (
351
+ <Spinner size="sm" className={classes.spinner} />
352
+ )}
353
+ </div>
354
+ <p className={classes.text}>
355
+ Protected snapshots cannot be deleted manually or automatically.
356
+ </p>
357
+ {updateSnapshotError && (
358
+ <ErrorStub
359
+ title="Update error"
360
+ message={updateSnapshotError}
361
+ className={classes.marginTop}
362
+ />
363
+ )}
364
+ </div>
294
365
  <br />
295
366
  <p>
296
367
  <strong>Snapshot info</strong>
@@ -12,11 +12,13 @@ import {
12
12
  SnapshotsApi,
13
13
  } from '@postgres.ai/shared/stores/Snapshots'
14
14
  import { DestroySnapshot } from '@postgres.ai/shared/types/api/endpoints/destroySnapshot'
15
+ import { UpdateSnapshot } from '@postgres.ai/shared/types/api/endpoints/updateSnapshot'
15
16
  import { SnapshotDto } from '@postgres.ai/shared/types/api/entities/snapshot'
16
17
  import { GetBranchSnapshot } from '@postgres.ai/shared/types/api/endpoints/getBranchSnapshot'
17
18
  import { BranchSnapshotDto } from '@postgres.ai/shared/types/api/entities/branchSnapshot'
18
19
  import { generateSnapshotPageId } from '@postgres.ai/shared/pages/Instance/Snapshots/utils'
19
20
  import { InitWS } from '@postgres.ai/shared/types/api/endpoints/initWS'
21
+ import { getTextFromUnknownApiError } from '@postgres.ai/shared/utils/api'
20
22
 
21
23
  type Error = {
22
24
  title?: string
@@ -25,6 +27,7 @@ type Error = {
25
27
 
26
28
  export type Api = SnapshotsApi & {
27
29
  destroySnapshot: DestroySnapshot
30
+ updateSnapshot?: UpdateSnapshot
28
31
  getBranchSnapshot?: GetBranchSnapshot
29
32
  initWS?: InitWS
30
33
  }
@@ -37,6 +40,8 @@ export class MainStore {
37
40
  branchSnapshotError: Error | null = null
38
41
 
39
42
  isSnapshotsLoading = false
43
+ isUpdatingSnapshot = false
44
+ updateSnapshotError: string | null = null
40
45
 
41
46
  private readonly api: Api
42
47
  readonly snapshots: SnapshotsStore
@@ -99,6 +104,41 @@ export class MainStore {
99
104
  return response
100
105
  }
101
106
 
107
+ updateSnapshotProtection = async (
108
+ snapshotId: string,
109
+ instanceId: string,
110
+ durationMinutes: number | null,
111
+ ) => {
112
+ if (!this.api.updateSnapshot || !this.snapshot) return
113
+
114
+ this.isUpdatingSnapshot = true
115
+ this.updateSnapshotError = null
116
+
117
+ const prevProtected = this.snapshot.protected
118
+ const isProtected = durationMinutes !== null
119
+
120
+ this.snapshot.protected = isProtected
121
+
122
+ const { response, error } = await this.api.updateSnapshot({
123
+ instanceId,
124
+ snapshotId: this.snapshot.id,
125
+ snapshot: {
126
+ isProtected,
127
+ protectionDurationMinutes: durationMinutes ?? undefined,
128
+ },
129
+ })
130
+
131
+ if (response) {
132
+ await this.load(snapshotId, instanceId)
133
+ } else if (this.snapshot) {
134
+ this.snapshot.protected = prevProtected
135
+ }
136
+
137
+ if (error) this.updateSnapshotError = await getTextFromUnknownApiError(error)
138
+
139
+ this.isUpdatingSnapshot = false
140
+ }
141
+
102
142
  destroySnapshot = async (
103
143
  snapshotId: string,
104
144
  forceDelete: boolean,
Binary file
@@ -40,8 +40,8 @@ export class SnapshotsStore {
40
40
  return this.loadData(instanceId)
41
41
  }
42
42
 
43
- reload = (instanceId: string, branchName?: string) =>
44
- this.loadData(instanceId, branchName)
43
+ reload = (instanceId: string, branchName?: string, silent?: boolean) =>
44
+ this.loadData(instanceId, branchName, silent)
45
45
 
46
46
  createSnapshot = async (
47
47
  cloneId: string,
@@ -72,21 +72,30 @@ export class SnapshotsStore {
72
72
  return response
73
73
  }
74
74
 
75
- private loadData = async (instanceId: string, branchName?: string) => {
75
+ private loadData = async (
76
+ instanceId: string,
77
+ branchName?: string,
78
+ silent?: boolean,
79
+ ) => {
76
80
  if (!this.api.getSnapshots) return
77
81
 
78
- this.isLoading = true
82
+ if (!silent) this.isLoading = true
79
83
 
80
84
  const { response, error } = await this.api.getSnapshots({
81
85
  instanceId,
82
86
  branchName,
83
87
  })
84
88
 
85
- this.isLoading = false
89
+ if (!silent) this.isLoading = false
86
90
 
87
- if (response) this.data = response
91
+ if (response) {
92
+ this.data = response
93
+ this.error = null
94
+ }
88
95
 
89
- if (error) {
96
+ // a silent background refresh keeps the existing list on a transient failure; errors
97
+ // surface only on a foreground load.
98
+ if (error && !silent) {
90
99
  this.error = await error
91
100
  .json()
92
101
  .then((error) => error.details?.split('"message": "')[1]?.split('"')[0])
@@ -8,6 +8,9 @@ export interface Branch {
8
8
  snapshotID: string
9
9
  dataset: string
10
10
  numSnapshots: number
11
+ protected: boolean
12
+ protectedTill?: string
13
+ deleteAt?: string
11
14
  }
12
15
 
13
16
  export const formatBranchesDto = (dto: Branch[]) =>
@@ -0,0 +1,34 @@
1
+ export type ProbeSourceRequest = {
2
+ url: string
3
+ password: string
4
+ }
5
+
6
+ export type SourceConnection = {
7
+ host: string
8
+ port: number
9
+ username: string
10
+ dbname: string
11
+ }
12
+
13
+ export type ProposedConfig = {
14
+ source: SourceConnection
15
+ detectedProvider: string
16
+ dockerImage: string
17
+ dockerTag: string
18
+ pgMajorVersion: number
19
+ databases: string[]
20
+ sharedBuffers: string
21
+ memoryProbed: boolean
22
+ sharedPreloadLibraries: string
23
+ queryTuning: { [key: string]: string }
24
+ }
25
+
26
+ export type ProbeSourceError = {
27
+ status: number
28
+ message: string
29
+ }
30
+
31
+ export type ProbeSource = (req: ProbeSourceRequest) => Promise<{
32
+ response: ProposedConfig | null
33
+ error: ProbeSourceError | null
34
+ }>
@@ -33,7 +33,16 @@ export const formatTuningParams = (
33
33
  return formattedTuningParams
34
34
  }
35
35
 
36
- export const formatTuningParamsToObj = (tuningParams: string | undefined) => {
36
+ export const formatTuningParamsToObj = (
37
+ tuningParams: string | { [key: string]: string } | undefined,
38
+ ) => {
39
+ // Simple mode seeds tuningParams as an object (the proposed queryTuning map);
40
+ // Expert mode keeps it as the textarea string. Pass an already-parsed object
41
+ // through unchanged so callers never run .split on a non-string.
42
+ if (tuningParams && typeof tuningParams === 'object') {
43
+ return tuningParams
44
+ }
45
+
37
46
  let formattedTuningParams: { [key: string]: string } = {}
38
47
 
39
48
  if (tuningParams) {
@@ -0,0 +1,11 @@
1
+ export type UpdateBranch = (args: {
2
+ instanceId: string
3
+ branchName: string
4
+ branch: {
5
+ isProtected: boolean
6
+ protectionDurationMinutes?: number
7
+ }
8
+ }) => Promise<{
9
+ response: true | null
10
+ error: Response | null
11
+ }>
@@ -0,0 +1,11 @@
1
+ export type UpdateSnapshot = (args: {
2
+ instanceId: string
3
+ snapshotId: string
4
+ snapshot: {
5
+ isProtected: boolean
6
+ protectionDurationMinutes?: number
7
+ }
8
+ }) => Promise<{
9
+ response: true | null
10
+ error: Response | null
11
+ }>
@@ -0,0 +1,8 @@
1
+ export type UpgradeClone = (args: {
2
+ instanceId: string
3
+ cloneId: string
4
+ dockerImage?: string
5
+ }) => Promise<{
6
+ response: true | null
7
+ error: Response | null
8
+ }>
@@ -16,9 +16,21 @@ export type CloneDto = {
16
16
  id: string
17
17
  branch: string
18
18
  status: {
19
- code: 'OK' | 'CREATING' | 'DELETING' | 'RESETTING' | 'FATAL'
19
+ code:
20
+ | 'OK'
21
+ | 'CREATING'
22
+ | 'DELETING'
23
+ | 'RESETTING'
24
+ | 'UPGRADING'
25
+ | 'WARNING'
26
+ | 'FATAL'
20
27
  message: string
21
28
  }
29
+ // Set once a clone has been upgraded to another major version; empty means the clone runs
30
+ // the engine-wide image.
31
+ dockerImage?: string
32
+ // PostgreSQL major version the clone is currently running.
33
+ dbVersion?: string
22
34
  protected: boolean
23
35
  protectedTill?: string
24
36
  metadata: {
@@ -11,10 +11,13 @@ export interface DatabaseType {
11
11
  [name: string]: string | Object
12
12
  }
13
13
 
14
+ export type RetrievalMode = 'logical' | 'physical' | 'unknown' | ''
15
+
14
16
  export type configTypes = {
15
17
  global?: {
16
18
  debug?: boolean
17
19
  }
20
+ retrievalMode?: RetrievalMode
18
21
  databaseContainer?: {
19
22
  dockerImage?: string
20
23
  dockerPath?: string
@@ -54,14 +57,57 @@ export type configTypes = {
54
57
  configs?: { [key: string]: string }
55
58
  }
56
59
  }
60
+ physicalRestore?: {
61
+ options?: {
62
+ tool?: string
63
+ dockerImage?: string
64
+ sync?: {
65
+ enabled?: boolean
66
+ }
67
+ walg?: {
68
+ backupName?: string
69
+ }
70
+ pgbackrest?: {
71
+ stanza?: string
72
+ delta?: boolean
73
+ }
74
+ envs?: { [key: string]: string | number | boolean }
75
+ }
76
+ }
57
77
  }
58
78
  }
59
79
  }
60
80
 
81
+ const formatPhysicalEnvs = (
82
+ envs?: { [key: string]: string | number | boolean } | null,
83
+ ): Array<{ key: string; value: string }> => {
84
+ if (!envs) return []
85
+ return Object.entries(envs).map(([key, value]) => ({
86
+ key,
87
+ value: typeof value === 'string' ? value : String(value),
88
+ }))
89
+ }
90
+
91
+ const inferRetrievalMode = (config: configTypes): RetrievalMode => {
92
+ if (config.retrievalMode) return config.retrievalMode
93
+ if (config.retrieval?.spec?.physicalRestore) return 'physical'
94
+ if (config.retrieval?.spec?.logicalDump) return 'logical'
95
+ return ''
96
+ }
97
+
61
98
  export const formatConfig = (config: configTypes) => {
62
99
  const dockerImage = config.databaseContainer?.dockerImage
100
+ const physical = config.retrieval?.spec?.physicalRestore?.options
63
101
  return {
64
102
  debug: config.global?.debug,
103
+ retrievalMode: inferRetrievalMode(config),
104
+ physicalTool: physical?.tool ?? '',
105
+ physicalDockerImage: physical?.dockerImage ?? '',
106
+ physicalSyncEnabled: physical?.sync?.enabled ?? false,
107
+ physicalWalgBackupName: physical?.walg?.backupName ?? '',
108
+ physicalPgbackrestStanza: physical?.pgbackrest?.stanza ?? '',
109
+ physicalPgbackrestDelta: physical?.pgbackrest?.delta ?? false,
110
+ physicalEnvs: formatPhysicalEnvs(physical?.envs),
65
111
  dockerImage: isSeDockerImage(dockerImage)
66
112
  ? getImageMajorVersion(dockerImage)
67
113
  : dockerImage && getImageType(dockerImage) === 'Generic Postgres'
@@ -19,6 +19,12 @@ export type InstanceStateDto = {
19
19
  code: 'OK' | 'WARNING' | 'NO_RESPONSE'
20
20
  message: string
21
21
  }
22
+ // Absent on engines that do not report it, which is the same as an upgrade being unavailable.
23
+ cloneUpgrade?: {
24
+ available: boolean
25
+ targetVersion?: number
26
+ reason?: string
27
+ }
22
28
  // Fallback for capability with old API.
23
29
  clones?: CloneDto[]
24
30
  // Fallback for capability with old API.
@@ -11,6 +11,9 @@ export type SnapshotDto = {
11
11
  logicalSize: number
12
12
  message: string
13
13
  branch: string
14
+ protected: boolean
15
+ protectedTill?: string
16
+ deleteAt?: string
14
17
  }
15
18
 
16
19
  export const formatSnapshotDto = (dto: SnapshotDto) => ({
package/utils/api.ts CHANGED
@@ -11,6 +11,14 @@ export const getTextFromUnknownApiError = async (error: Response) => {
11
11
  try {
12
12
  const result = await error.json()
13
13
  log(result)
14
+
15
+ // The engine answers with models.Error{code, message}. Showing the serialized object puts
16
+ // `{"code":"BAD_REQUEST","message":"..."}` in front of the user when the message alone is
17
+ // what they need; anything without a usable message still falls back to the raw body.
18
+ if (typeof result?.message === 'string' && result.message !== '') {
19
+ return result.message
20
+ }
21
+
14
22
  return JSON.stringify(result)
15
23
  } catch (e) {
16
24
  // not a json
package/utils/clone.ts CHANGED
@@ -13,6 +13,8 @@ const STATUS_CODE_TO_TYPE = {
13
13
  CREATING: 'waiting' as const,
14
14
  DELETING: 'waiting' as const,
15
15
  RESETTING: 'waiting' as const,
16
+ UPGRADING: 'waiting' as const,
17
+ WARNING: 'warning' as const,
16
18
  FATAL: 'error' as const,
17
19
  }
18
20
 
@@ -27,5 +29,9 @@ export const getCloneStatusText = (statusCode: StatusCode) => {
27
29
  return capitalize(statusCode)
28
30
  }
29
31
 
32
+ // WARNING is terminal too: it means the clone is running, just not on the version that was
33
+ // requested. Leaving it out would make the clone page poll forever after a failed upgrade.
30
34
  export const checkIsCloneStable = (clone: Clone) =>
31
- clone.status.code === 'OK' || clone.status.code === 'FATAL'
35
+ clone.status.code === 'OK' ||
36
+ clone.status.code === 'WARNING' ||
37
+ clone.status.code === 'FATAL'