@postgres.ai/shared 4.0.2-pr-1148.1 → 4.0.2-pr-1200

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 (61) hide show
  1. package/components/UpgradeCloneModal/index.d.ts +11 -0
  2. package/components/UpgradeCloneModal/index.js +75 -0
  3. package/package.json +1 -1
  4. package/pages/Branches/Branch/index.js +27 -2
  5. package/pages/Branches/Branch/stores/Main.d.ts +5 -0
  6. package/pages/Branches/Branch/stores/Main.js +29 -0
  7. package/pages/Branches/components/BranchesTable/index.js +2 -2
  8. package/pages/Branches/index.js +10 -1
  9. package/pages/Clone/Status/index.js +3 -0
  10. package/pages/Clone/index.js +12 -4
  11. package/pages/Clone/stores/Main.d.ts +9 -1
  12. package/pages/Clone/stores/Main.js +40 -3
  13. package/pages/CreateBranch/stores/Main.d.ts +3 -0
  14. package/pages/CreateClone/index.js +2 -0
  15. package/pages/CreateClone/stores/Main.d.ts +5 -1
  16. package/pages/CreateClone/stores/Main.js +8 -3
  17. package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.d.ts +2 -0
  18. package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.js +8 -1
  19. package/pages/Instance/Configuration/SimpleMode/PreviewCard.d.ts +3 -1
  20. package/pages/Instance/Configuration/SimpleMode/PreviewCard.js +7 -7
  21. package/pages/Instance/Configuration/SimpleMode/index.d.ts +6 -1
  22. package/pages/Instance/Configuration/SimpleMode/index.js +81 -22
  23. package/pages/Instance/Configuration/configOptions.d.ts +10 -0
  24. package/pages/Instance/Configuration/configOptions.js +24 -1
  25. package/pages/Instance/Info/Snapshots/Calendar/Day/index.js +3 -3
  26. package/pages/Instance/Info/Snapshots/Calendar/utils.d.ts +3 -0
  27. package/pages/Instance/Info/Snapshots/utils.d.ts +6 -0
  28. package/pages/Instance/Snapshots/components/SnapshotsList/index.js +11 -1
  29. package/pages/Instance/Snapshots/index.js +11 -3
  30. package/pages/Instance/Snapshots/utils/index.d.ts +3 -0
  31. package/pages/Instance/Tabs/styles.js +6 -6
  32. package/pages/Instance/stores/Main.d.ts +2 -2
  33. package/pages/Instance/stores/Main.js +17 -9
  34. package/pages/Snapshots/Snapshot/index.js +28 -2
  35. package/pages/Snapshots/Snapshot/stores/Main.d.ts +5 -0
  36. package/pages/Snapshots/Snapshot/stores/Main.js +29 -0
  37. package/stores/Snapshots.d.ts +1 -1
  38. package/stores/Snapshots.js +12 -6
  39. package/types/api/endpoints/getBranches.d.ts +6 -0
  40. package/types/api/endpoints/testDbSource.d.ts +3 -1
  41. package/types/api/endpoints/testDbSource.js +6 -0
  42. package/types/api/endpoints/updateBranch.d.ts +11 -0
  43. package/types/api/endpoints/updateBranch.js +1 -0
  44. package/types/api/endpoints/updateSnapshot.d.ts +11 -0
  45. package/types/api/endpoints/updateSnapshot.js +1 -0
  46. package/types/api/endpoints/upgradeClone.d.ts +8 -0
  47. package/types/api/endpoints/upgradeClone.js +1 -0
  48. package/types/api/entities/branchSnapshots.d.ts +3 -0
  49. package/types/api/entities/clone.d.ts +9 -2
  50. package/types/api/entities/instance.d.ts +11 -1
  51. package/types/api/entities/instanceState.d.ts +16 -1
  52. package/types/api/entities/snapshot.d.ts +6 -0
  53. package/utils/api.d.ts +1 -1
  54. package/utils/api.js +6 -0
  55. package/utils/clone.d.ts +3 -1
  56. package/utils/clone.js +7 -1
  57. package/utils/clonePoller.d.ts +12 -0
  58. package/utils/clonePoller.js +41 -0
  59. package/utils/date.js +12 -12
  60. package/utils/protection.d.ts +5 -0
  61. package/utils/protection.js +23 -0
@@ -13,11 +13,15 @@ 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';
17
+ import { Spinner } from '@postgres.ai/shared/components/Spinner';
16
18
  import { DestroySnapshotModal } from '@postgres.ai/shared/pages/Snapshots/Snapshot/DestorySnapshotModal';
17
19
  import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer';
18
20
  import { Tooltip } from '@postgres.ai/shared/components/Tooltip';
19
21
  import { icons } from '@postgres.ai/shared/styles/icons';
20
22
  import { formatBytesIEC } from '@postgres.ai/shared/utils/units';
23
+ import { parseDate } from '@postgres.ai/shared/utils/date';
24
+ import { protectionOptions, getProtectionSelectValue, } from '@postgres.ai/shared/utils/protection';
21
25
  import { styles } from '@postgres.ai/shared/styles/styles';
22
26
  import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight';
23
27
  import { TableBodyCell, TableBodyCellMenu, TableHeaderCell, } from '@postgres.ai/shared/components/Table';
@@ -71,6 +75,13 @@ const useStyles = makeStyles(() => ({
71
75
  summary: {
72
76
  marginTop: 20,
73
77
  },
78
+ protectionControl: {
79
+ display: 'flex',
80
+ alignItems: 'center',
81
+ marginTop: '8px',
82
+ marginBottom: '8px',
83
+ maxWidth: 525,
84
+ },
74
85
  text: {
75
86
  marginTop: '4px',
76
87
  },
@@ -129,11 +140,18 @@ export const SnapshotPage = observer((props) => {
129
140
  const history = useHistory();
130
141
  const stores = useCreatedStores(props.api);
131
142
  const [isOpenDestroyModal, setIsOpenDestroyModal] = useState(false);
132
- const { snapshot, branchSnapshot, isSnapshotsLoading, snapshotError, branchSnapshotError, load, } = stores.main;
143
+ const { snapshot, branchSnapshot, isSnapshotsLoading, isUpdatingSnapshot, updateSnapshotProtection, updateSnapshotError, snapshotError, branchSnapshotError, load, } = stores.main;
133
144
  const afterSubmitClick = () => {
134
145
  history.push(props.routes.snapshots());
135
146
  load(props.snapshotId, props.instanceId);
136
147
  };
148
+ const protectedTillDate = (snapshot === null || snapshot === void 0 ? void 0 : snapshot.protectedTill)
149
+ ? parseDate(snapshot.protectedTill)
150
+ : null;
151
+ const handleProtectionChange = (e) => {
152
+ const value = e.target.value;
153
+ updateSnapshotProtection(props.snapshotId, props.instanceId, value === 'none' ? null : parseInt(value, 10));
154
+ };
137
155
  const headRendered = (_jsxs(_Fragment, { children: [_jsx("style", { children: 'p { margin: 0;}' }), props.elements.breadcrumbs, _jsx(SectionTitle, { className: classes.title, tag: "h1", level: 1, text: `Snapshot ${props.snapshotId}`, children: _jsx(InstanceTabs, { tab: TABS_INDEX.SNAPSHOTS, isPlatform: props.isPlatform, instanceId: props.instanceId, hasLogs: props.api.initWS !== undefined, hideInstanceTabs: props.hideBranchingFeatures }) })] }));
138
156
  useEffect(() => {
139
157
  load(props.snapshotId, props.instanceId);
@@ -147,7 +165,15 @@ export const SnapshotPage = observer((props) => {
147
165
  ? formatBytesIEC(snapshot.logicalSize)
148
166
  : '-'] }), _jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Physical data diff size:" }), (snapshot === null || snapshot === void 0 ? void 0 : snapshot.physicalSize)
149
167
  ? formatBytesIEC(snapshot.physicalSize)
150
- : '-'] }), (branchSnapshot === null || branchSnapshot === void 0 ? void 0 : branchSnapshot.message) && (_jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Message:" }), branchSnapshot.message] }))] }), _jsx("br", {}), _jsx("p", { children: _jsx("strong", { children: "Snapshot info" }) }), (snapshot === null || snapshot === void 0 ? void 0 : snapshot.pool) && (_jsxs("div", { className: classes.copyFieldContainer, children: [_jsx(TextField, { variant: "outlined", label: "snapshot pool", value: snapshot.pool, className: classes.textField, margin: "normal", fullWidth: true, InputLabelProps: {
168
+ : '-'] }), (branchSnapshot === null || branchSnapshot === void 0 ? void 0 : branchSnapshot.message) && (_jsxs("p", { className: classes.text, children: [_jsx("span", { className: classes.paramTitle, children: "Message:" }), branchSnapshot.message] }))] }), _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: (snapshot === null || snapshot === void 0 ? void 0 : snapshot.protected) && protectedTillDate
169
+ ? [
170
+ {
171
+ value: 'current',
172
+ children: `Protected until ${protectedTillDate.toLocaleString()}`,
173
+ },
174
+ ...protectionOptions,
175
+ ]
176
+ : protectionOptions, value: getProtectionSelectValue(Boolean(snapshot === null || snapshot === void 0 ? void 0 : snapshot.protected), protectedTillDate), onChange: handleProtectionChange, disabled: isUpdatingSnapshot, fullWidth: true }), isUpdatingSnapshot && (_jsx(Spinner, { size: "sm", className: classes.spinner }))] }), _jsx("p", { className: classes.text, children: "Protected snapshots cannot be deleted manually or automatically." }), updateSnapshotError && (_jsx(ErrorStub, { title: "Update error", message: updateSnapshotError, className: classes.marginTop }))] }), _jsx("br", {}), _jsx("p", { children: _jsx("strong", { children: "Snapshot info" }) }), (snapshot === null || snapshot === void 0 ? void 0 : snapshot.pool) && (_jsxs("div", { className: classes.copyFieldContainer, children: [_jsx(TextField, { variant: "outlined", label: "snapshot pool", value: snapshot.pool, className: classes.textField, margin: "normal", fullWidth: true, InputLabelProps: {
151
177
  shrink: true,
152
178
  style: styles.inputFieldLabel,
153
179
  }, FormHelperTextProps: {
@@ -1,5 +1,6 @@
1
1
  import { SnapshotsStore, SnapshotsApi } from '@postgres.ai/shared/stores/Snapshots';
2
2
  import { DestroySnapshot } from '@postgres.ai/shared/types/api/endpoints/destroySnapshot';
3
+ import { UpdateSnapshot } from '@postgres.ai/shared/types/api/endpoints/updateSnapshot';
3
4
  import { SnapshotDto } from '@postgres.ai/shared/types/api/entities/snapshot';
4
5
  import { GetBranchSnapshot } from '@postgres.ai/shared/types/api/endpoints/getBranchSnapshot';
5
6
  import { BranchSnapshotDto } from '@postgres.ai/shared/types/api/entities/branchSnapshot';
@@ -10,6 +11,7 @@ declare type Error = {
10
11
  };
11
12
  export declare type Api = SnapshotsApi & {
12
13
  destroySnapshot: DestroySnapshot;
14
+ updateSnapshot?: UpdateSnapshot;
13
15
  getBranchSnapshot?: GetBranchSnapshot;
14
16
  initWS?: InitWS;
15
17
  };
@@ -19,12 +21,15 @@ export declare class MainStore {
19
21
  snapshotError: Error | null;
20
22
  branchSnapshotError: Error | null;
21
23
  isSnapshotsLoading: boolean;
24
+ isUpdatingSnapshot: boolean;
25
+ updateSnapshotError: string | null;
22
26
  private readonly api;
23
27
  readonly snapshots: SnapshotsStore;
24
28
  constructor(api: Api);
25
29
  load: (snapshotId: string, instanceId: string) => Promise<void>;
26
30
  getSnapshot: (snapshotId: string, instanceId: string) => Promise<boolean | undefined>;
27
31
  getBranchSnapshot: (snapshotId: string, instanceId: string) => Promise<import("@postgres.ai/shared/types/api/entities/branchSnapshot").BranchSnapshotDTO | null | undefined>;
32
+ updateSnapshotProtection: (snapshotId: string, instanceId: string, durationMinutes: number | null) => Promise<void>;
28
33
  destroySnapshot: (snapshotId: string, forceDelete: boolean, instanceId: string) => Promise<{
29
34
  response: boolean | null;
30
35
  error: any;
@@ -7,6 +7,7 @@
7
7
  import { makeAutoObservable } from 'mobx';
8
8
  import { SnapshotsStore, } from '@postgres.ai/shared/stores/Snapshots';
9
9
  import { generateSnapshotPageId } from '@postgres.ai/shared/pages/Instance/Snapshots/utils';
10
+ import { getTextFromUnknownApiError } from '@postgres.ai/shared/utils/api';
10
11
  export class MainStore {
11
12
  constructor(api) {
12
13
  this.snapshot = null;
@@ -14,6 +15,8 @@ export class MainStore {
14
15
  this.snapshotError = null;
15
16
  this.branchSnapshotError = null;
16
17
  this.isSnapshotsLoading = false;
18
+ this.isUpdatingSnapshot = false;
19
+ this.updateSnapshotError = null;
17
20
  this.load = async (snapshotId, instanceId) => {
18
21
  if (!snapshotId)
19
22
  return;
@@ -55,6 +58,32 @@ export class MainStore {
55
58
  }
56
59
  return response;
57
60
  };
61
+ this.updateSnapshotProtection = async (snapshotId, instanceId, durationMinutes) => {
62
+ if (!this.api.updateSnapshot || !this.snapshot)
63
+ return;
64
+ this.isUpdatingSnapshot = true;
65
+ this.updateSnapshotError = null;
66
+ const prevProtected = this.snapshot.protected;
67
+ const isProtected = durationMinutes !== null;
68
+ this.snapshot.protected = isProtected;
69
+ const { response, error } = await this.api.updateSnapshot({
70
+ instanceId,
71
+ snapshotId: this.snapshot.id,
72
+ snapshot: {
73
+ isProtected,
74
+ protectionDurationMinutes: durationMinutes !== null && durationMinutes !== void 0 ? durationMinutes : undefined,
75
+ },
76
+ });
77
+ if (response) {
78
+ await this.load(snapshotId, instanceId);
79
+ }
80
+ else if (this.snapshot) {
81
+ this.snapshot.protected = prevProtected;
82
+ }
83
+ if (error)
84
+ this.updateSnapshotError = await getTextFromUnknownApiError(error);
85
+ this.isUpdatingSnapshot = false;
86
+ };
58
87
  this.destroySnapshot = async (snapshotId, forceDelete, instanceId) => {
59
88
  if (!this.api.destroySnapshot || !snapshotId)
60
89
  return;
@@ -18,7 +18,7 @@ export declare class SnapshotsStore {
18
18
  private readonly api;
19
19
  constructor(api: SnapshotsApi);
20
20
  load: (instanceId: string) => Promise<boolean | undefined>;
21
- reload: (instanceId: string, branchName?: string) => Promise<boolean | undefined>;
21
+ reload: (instanceId: string, branchName?: string, silent?: boolean) => Promise<boolean | undefined>;
22
22
  createSnapshot: (cloneId: string, message: string, instanceId: string) => Promise<import("../types/api/entities/createSnapshot").CreateSnapshotDTO | null | undefined>;
23
23
  private loadData;
24
24
  }
@@ -18,7 +18,7 @@ export class SnapshotsStore {
18
18
  this.error = null;
19
19
  return this.loadData(instanceId);
20
20
  };
21
- this.reload = (instanceId, branchName) => this.loadData(instanceId, branchName);
21
+ this.reload = (instanceId, branchName, silent) => this.loadData(instanceId, branchName, silent);
22
22
  this.createSnapshot = async (cloneId, message, instanceId) => {
23
23
  if (!this.api.createSnapshot || !cloneId)
24
24
  return;
@@ -35,18 +35,24 @@ export class SnapshotsStore {
35
35
  }
36
36
  return response;
37
37
  };
38
- this.loadData = async (instanceId, branchName) => {
38
+ this.loadData = async (instanceId, branchName, silent) => {
39
39
  if (!this.api.getSnapshots)
40
40
  return;
41
- this.isLoading = true;
41
+ if (!silent)
42
+ this.isLoading = true;
42
43
  const { response, error } = await this.api.getSnapshots({
43
44
  instanceId,
44
45
  branchName,
45
46
  });
46
- this.isLoading = false;
47
- if (response)
47
+ if (!silent)
48
+ this.isLoading = false;
49
+ if (response) {
48
50
  this.data = response;
49
- if (error) {
51
+ this.error = null;
52
+ }
53
+ // a silent background refresh keeps the existing list on a transient failure; errors
54
+ // surface only on a foreground load.
55
+ if (error && !silent) {
50
56
  this.error = await error
51
57
  .json()
52
58
  .then((error) => { var _a, _b; return (_b = (_a = error.details) === null || _a === void 0 ? void 0 : _a.split('"message": "')[1]) === null || _b === void 0 ? void 0 : _b.split('"')[0]; });
@@ -6,6 +6,9 @@ export interface Branch {
6
6
  snapshotID: string;
7
7
  dataset: string;
8
8
  numSnapshots: number;
9
+ protected: boolean;
10
+ protectedTill?: string;
11
+ deleteAt?: string;
9
12
  }
10
13
  export declare const formatBranchesDto: (dto: Branch[]) => {
11
14
  dataStateAt: string;
@@ -15,6 +18,9 @@ export declare const formatBranchesDto: (dto: Branch[]) => {
15
18
  snapshotID: string;
16
19
  dataset: string;
17
20
  numSnapshots: number;
21
+ protected: boolean;
22
+ protectedTill?: string | undefined;
23
+ deleteAt?: string | undefined;
18
24
  }[];
19
25
  export declare type GetBranches = (instanceId: string) => Promise<{
20
26
  response: Branch[] | null;
@@ -17,6 +17,8 @@ export declare type TestDbSource = (values: dbSource) => Promise<{
17
17
  export declare const formatTuningParams: (tuningParams: {
18
18
  [key: string]: string;
19
19
  } | undefined) => string;
20
- export declare const formatTuningParamsToObj: (tuningParams: string | undefined) => {
20
+ export declare const formatTuningParamsToObj: (tuningParams: string | {
21
+ [key: string]: string;
22
+ } | undefined) => {
21
23
  [key: string]: string;
22
24
  };
@@ -11,6 +11,12 @@ export const formatTuningParams = (tuningParams) => {
11
11
  return formattedTuningParams;
12
12
  };
13
13
  export const formatTuningParamsToObj = (tuningParams) => {
14
+ // Simple mode seeds tuningParams as an object (the proposed queryTuning map);
15
+ // Expert mode keeps it as the textarea string. Pass an already-parsed object
16
+ // through unchanged so callers never run .split on a non-string.
17
+ if (tuningParams && typeof tuningParams === 'object') {
18
+ return tuningParams;
19
+ }
14
20
  let formattedTuningParams = {};
15
21
  if (tuningParams) {
16
22
  const tuningParamsArr = tuningParams.split('\n');
@@ -0,0 +1,11 @@
1
+ export declare 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 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare 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 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare type UpgradeClone = (args: {
2
+ instanceId: string;
3
+ cloneId: string;
4
+ dockerImage?: string;
5
+ }) => Promise<{
6
+ response: true | null;
7
+ error: Response | null;
8
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -12,4 +12,7 @@ export declare const formatBranchSnapshotDto: (dto: SnapshotDto[]) => {
12
12
  logicalSize: number;
13
13
  message: string;
14
14
  branch: string;
15
+ protected: boolean;
16
+ protectedTill?: string | undefined;
17
+ deleteAt?: string | undefined;
15
18
  }[];
@@ -4,9 +4,11 @@ export declare type CloneDto = {
4
4
  id: string;
5
5
  branch: string;
6
6
  status: {
7
- code: 'OK' | 'CREATING' | 'DELETING' | 'RESETTING' | 'FATAL';
7
+ code: 'OK' | 'CREATING' | 'DELETING' | 'RESETTING' | 'UPGRADING' | 'WARNING' | 'FATAL';
8
8
  message: string;
9
9
  };
10
+ dockerImage?: string;
11
+ dbVersion?: string;
10
12
  protected: boolean;
11
13
  protectedTill?: string;
12
14
  metadata: {
@@ -39,13 +41,18 @@ export declare const formatCloneDto: (dto: CloneDto) => {
39
41
  logicalSize: number;
40
42
  message: string;
41
43
  branch: string;
44
+ protected: boolean;
45
+ protectedTill?: string | undefined;
46
+ deleteAt?: string | undefined;
42
47
  } | null;
43
48
  id: string;
44
49
  branch: string;
45
50
  status: {
46
- code: 'OK' | 'CREATING' | 'DELETING' | 'RESETTING' | 'FATAL';
51
+ code: 'OK' | 'CREATING' | 'DELETING' | 'RESETTING' | 'UPGRADING' | 'WARNING' | 'FATAL';
47
52
  message: string;
48
53
  };
54
+ dockerImage?: string | undefined;
55
+ dbVersion?: string | undefined;
49
56
  protected: boolean;
50
57
  protectedTill?: string | undefined;
51
58
  metadata: {
@@ -75,13 +75,18 @@ export declare const formatInstanceDto: (dto: InstanceDto) => {
75
75
  logicalSize: number;
76
76
  message: string;
77
77
  branch: string;
78
+ protected: boolean;
79
+ protectedTill?: string | undefined;
80
+ deleteAt?: string | undefined;
78
81
  } | null;
79
82
  id: string;
80
83
  branch: string;
81
84
  status: {
82
- code: "OK" | "CREATING" | "DELETING" | "RESETTING" | "FATAL";
85
+ code: "OK" | "CREATING" | "DELETING" | "RESETTING" | "UPGRADING" | "WARNING" | "FATAL";
83
86
  message: string;
84
87
  };
88
+ dockerImage?: string | undefined;
89
+ dbVersion?: string | undefined;
85
90
  protected: boolean;
86
91
  protectedTill?: string | undefined;
87
92
  metadata: {
@@ -106,6 +111,11 @@ export declare const formatInstanceDto: (dto: InstanceDto) => {
106
111
  code: "OK" | "WARNING" | "NO_RESPONSE";
107
112
  message: string;
108
113
  };
114
+ cloneUpgrade?: {
115
+ available: boolean;
116
+ targetVersion?: number | undefined;
117
+ reason?: string | undefined;
118
+ } | undefined;
109
119
  clones?: import("./clone").CloneDto[] | undefined;
110
120
  expectedCloningTime?: number | undefined;
111
121
  fileSystem?: {
@@ -12,6 +12,11 @@ export declare type InstanceStateDto = {
12
12
  code: 'OK' | 'WARNING' | 'NO_RESPONSE';
13
13
  message: string;
14
14
  };
15
+ cloneUpgrade?: {
16
+ available: boolean;
17
+ targetVersion?: number;
18
+ reason?: string;
19
+ };
15
20
  clones?: CloneDto[];
16
21
  expectedCloningTime?: number;
17
22
  pools?: PoolDto[];
@@ -80,13 +85,18 @@ export declare const formatInstanceStateDto: (dto: InstanceStateDto) => {
80
85
  logicalSize: number;
81
86
  message: string;
82
87
  branch: string;
88
+ protected: boolean;
89
+ protectedTill?: string | undefined;
90
+ deleteAt?: string | undefined;
83
91
  } | null;
84
92
  id: string;
85
93
  branch: string;
86
94
  status: {
87
- code: "OK" | "CREATING" | "DELETING" | "RESETTING" | "FATAL";
95
+ code: "OK" | "CREATING" | "DELETING" | "RESETTING" | "UPGRADING" | "WARNING" | "FATAL";
88
96
  message: string;
89
97
  };
98
+ dockerImage?: string | undefined;
99
+ dbVersion?: string | undefined;
90
100
  protected: boolean;
91
101
  protectedTill?: string | undefined;
92
102
  metadata: {
@@ -111,6 +121,11 @@ export declare const formatInstanceStateDto: (dto: InstanceStateDto) => {
111
121
  code: 'OK' | 'WARNING' | 'NO_RESPONSE';
112
122
  message: string;
113
123
  };
124
+ cloneUpgrade?: {
125
+ available: boolean;
126
+ targetVersion?: number | undefined;
127
+ reason?: string | undefined;
128
+ } | undefined;
114
129
  clones?: CloneDto[] | undefined;
115
130
  expectedCloningTime?: number | undefined;
116
131
  fileSystem?: {
@@ -9,6 +9,9 @@ export declare type SnapshotDto = {
9
9
  logicalSize: number;
10
10
  message: string;
11
11
  branch: string;
12
+ protected: boolean;
13
+ protectedTill?: string;
14
+ deleteAt?: string;
12
15
  };
13
16
  export declare const formatSnapshotDto: (dto: SnapshotDto) => {
14
17
  createdAtDate: Date;
@@ -23,5 +26,8 @@ export declare const formatSnapshotDto: (dto: SnapshotDto) => {
23
26
  logicalSize: number;
24
27
  message: string;
25
28
  branch: string;
29
+ protected: boolean;
30
+ protectedTill?: string | undefined;
31
+ deleteAt?: string | undefined;
26
32
  };
27
33
  export declare type Snapshot = ReturnType<typeof formatSnapshotDto>;
package/utils/api.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const getTextFromUnknownApiError: (error: Response) => Promise<string>;
1
+ export declare const getTextFromUnknownApiError: (error: Response) => Promise<any>;
package/utils/api.js CHANGED
@@ -9,6 +9,12 @@ export const getTextFromUnknownApiError = async (error) => {
9
9
  try {
10
10
  const result = await error.json();
11
11
  log(result);
12
+ // The engine answers with models.Error{code, message}. Showing the serialized object puts
13
+ // `{"code":"BAD_REQUEST","message":"..."}` in front of the user when the message alone is
14
+ // what they need; anything without a usable message still falls back to the raw body.
15
+ if (typeof (result === null || result === void 0 ? void 0 : result.message) === 'string' && result.message !== '') {
16
+ return result.message;
17
+ }
12
18
  return JSON.stringify(result);
13
19
  }
14
20
  catch (e) {
package/utils/clone.d.ts CHANGED
@@ -4,10 +4,12 @@ declare const STATUS_CODE_TO_TYPE: {
4
4
  CREATING: "waiting";
5
5
  DELETING: "waiting";
6
6
  RESETTING: "waiting";
7
+ UPGRADING: "waiting";
8
+ WARNING: "warning";
7
9
  FATAL: "error";
8
10
  };
9
11
  declare type StatusCode = keyof typeof STATUS_CODE_TO_TYPE;
10
- export declare const getCloneStatusType: (statusCode: StatusCode) => "error" | "waiting" | "ok";
12
+ export declare const getCloneStatusType: (statusCode: StatusCode) => "error" | "warning" | "waiting" | "ok";
11
13
  export declare const getCloneStatusText: (statusCode: StatusCode) => string;
12
14
  export declare const checkIsCloneStable: (clone: Clone) => boolean;
13
15
  export {};
package/utils/clone.js CHANGED
@@ -10,6 +10,8 @@ const STATUS_CODE_TO_TYPE = {
10
10
  CREATING: 'waiting',
11
11
  DELETING: 'waiting',
12
12
  RESETTING: 'waiting',
13
+ UPGRADING: 'waiting',
14
+ WARNING: 'warning',
13
15
  FATAL: 'error',
14
16
  };
15
17
  export const getCloneStatusType = (statusCode) => { var _a; return (_a = STATUS_CODE_TO_TYPE[statusCode]) !== null && _a !== void 0 ? _a : 'unknown'; };
@@ -18,4 +20,8 @@ export const getCloneStatusText = (statusCode) => {
18
20
  return statusCode;
19
21
  return capitalize(statusCode);
20
22
  };
21
- export const checkIsCloneStable = (clone) => clone.status.code === 'OK' || clone.status.code === 'FATAL';
23
+ // WARNING is terminal too: it means the clone is running, just not on the version that was
24
+ // requested. Leaving it out would make the clone page poll forever after a failed upgrade.
25
+ export const checkIsCloneStable = (clone) => clone.status.code === 'OK' ||
26
+ clone.status.code === 'WARNING' ||
27
+ clone.status.code === 'FATAL';
@@ -0,0 +1,12 @@
1
+ export declare const UNSTABLE_CLONE_UPDATE_TIMEOUT = 1000;
2
+ export declare class ClonePoller {
3
+ private readonly intervalMs;
4
+ private timeout?;
5
+ private stopped;
6
+ constructor(intervalMs?: number);
7
+ get isStopped(): boolean;
8
+ start: () => void;
9
+ stop: () => void;
10
+ cancel: () => void;
11
+ scheduleNext: (tick: () => void) => void;
12
+ }
@@ -0,0 +1,41 @@
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
+ // interval between clone status polls while a clone is still unstable.
8
+ export const UNSTABLE_CLONE_UPDATE_TIMEOUT = 1000;
9
+ // ClonePoller owns a single self-rescheduling timeout used to poll clone status
10
+ // until it becomes stable. It is reusable: start() re-arms a poller that was
11
+ // previously stopped, and stop() cancels any pending tick and blocks further
12
+ // scheduling — including a tick that an in-flight request is about to queue.
13
+ export class ClonePoller {
14
+ constructor(intervalMs = UNSTABLE_CLONE_UPDATE_TIMEOUT) {
15
+ this.intervalMs = intervalMs;
16
+ this.stopped = false;
17
+ // start re-arms the poller so a reused store polls again.
18
+ this.start = () => {
19
+ this.stopped = false;
20
+ };
21
+ // stop halts polling and cancels any pending tick.
22
+ this.stop = () => {
23
+ this.stopped = true;
24
+ clearTimeout(this.timeout);
25
+ };
26
+ // cancel drops a pending tick without blocking future scheduling.
27
+ this.cancel = () => {
28
+ clearTimeout(this.timeout);
29
+ };
30
+ // scheduleNext queues the next tick, unless the poller has been stopped.
31
+ this.scheduleNext = (tick) => {
32
+ clearTimeout(this.timeout);
33
+ if (this.stopped)
34
+ return;
35
+ this.timeout = setTimeout(tick, this.intervalMs);
36
+ };
37
+ }
38
+ get isStopped() {
39
+ return this.stopped;
40
+ }
41
+ }
package/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
- // Handle empty or invalid date strings
10
- if (!dateString || dateString.trim() === '') {
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
- try {
14
- const parsedDate = parse(dateString, 'yyyyMMddHHmmss', new Date());
15
- // Additional validation of parsed date
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,5 @@
1
+ export declare const protectionOptions: {
2
+ value: string;
3
+ children: string;
4
+ }[];
5
+ export declare const getProtectionSelectValue: (isProtected: boolean, protectedTillDate: Date | null) => string;
@@ -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
+ };