@postgres.ai/shared 4.1.0 → 4.1.1

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 CHANGED
@@ -1,6 +1,8 @@
1
1
  .only_ui_feature: &only_ui_feature
2
2
  rules:
3
3
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
4
+ changes:
5
+ - ui/**/*
4
6
 
5
7
  .only_ui_tag_release: &only_ui_tag_release
6
8
  rules:
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postgres.ai/shared",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.js",
6
6
  "pack": "node scripts/pack.js"
@@ -286,10 +286,10 @@ export const Clone = observer((props) => {
286
286
  style: styles.inputFieldLabel,
287
287
  }, FormHelperTextProps: {
288
288
  style: styles.inputFieldHelper,
289
- } }), _jsx(IconButton, { className: classes.copyButton, "aria-label": "Copy", onClick: () => copyToClipboard(jdbcConnectionStr), children: icons.copyIcon })] }), "\u00A0", _jsx(Tooltip, { content: _jsx(_Fragment, { children: "Used to connect to PostgreSQL using JDBC. Change DBNAME to the name of the database you want to connect to, and change DBPASSWORD to the password you used when creating the clone." }), children: _jsx("span", { className: classes.textFieldInfo, children: icons.infoIcon }) })] }))] })), _jsx("br", {}), _jsx("div", { className: classes.fieldBlock, children: _jsxs("span", { className: classes.remark, children: ["Password was set during clone creation. It\u2019s not being stored.", _jsx("br", {}), "You would need to recreate a clone if the password is lost."] }) }), _jsx("br", {}), _jsx("p", { children: _jsx("strong", { children: "Protection" }) }), _jsxs("div", { style: { marginTop: '8px', marginBottom: '16px' }, children: [_jsx(Select, { label: "Deletion protection", items: clone.protected && clone.protectedTillDate
289
+ } }), _jsx(IconButton, { className: classes.copyButton, "aria-label": "Copy", onClick: () => copyToClipboard(jdbcConnectionStr), children: icons.copyIcon })] }), "\u00A0", _jsx(Tooltip, { content: _jsx(_Fragment, { children: "Used to connect to PostgreSQL using JDBC. Change DBNAME to the name of the database you want to connect to, and change DBPASSWORD to the password you used when creating the clone." }), children: _jsx("span", { className: classes.textFieldInfo, children: icons.infoIcon }) })] }))] })), _jsx("br", {}), _jsx("div", { className: classes.fieldBlock, children: _jsxs("span", { className: classes.remark, children: ["Password was set during clone creation. It\u2019s not being stored.", _jsx("br", {}), "You would need to recreate a clone if the password is lost."] }) }), _jsx("br", {}), _jsx("p", { children: _jsx("strong", { children: "Protection" }) }), _jsxs("div", { style: { marginTop: '8px', marginBottom: '16px' }, children: [_jsx(Select, { label: "Deletion\u00a0protection", items: clone.protected && clone.protectedTillDate
290
290
  ? [
291
291
  { value: 'current', children: `Protected until ${clone.protectedTillDate.toLocaleString()}` },
292
292
  ...protectionOptions,
293
293
  ]
294
- : protectionOptions, value: getCurrentProtectionValue(), onChange: handleProtectionChange, disabled: isDisabledControls, style: { minWidth: 100 } }), isUpdatingClone && _jsx(Spinner, { size: "sm", className: classes.spinner })] }), _jsxs("span", { className: classes.remark, children: ["Select how long this clone should be protected from deletion. Protected clones cannot be deleted manually or automatically.", _jsx("br", {}), "Check disk space regularly and delete this clone once your work is done."] }), stores.main.updateCloneError && (_jsx(ErrorStub, { title: "Updating error", message: stores.main.updateCloneError, className: classes.errorStub }))] }), _jsxs("div", { className: classes.snippetContainer, children: [_jsx(SectionTitle, { tag: "h2", level: 2, text: 'Reset clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can reset the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliResetCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Delete clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can delete the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliDestroyCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Toggle deletion protection using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can toggle deletion protection using CLI for this clone using the following command:" }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(true) }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(false) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Create snapshot for this clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can create a snapshot for this clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCreateSnapshotCommand(props.cloneId) })] }), _jsxs(_Fragment, { children: [_jsx(DestroyCloneRestrictionModal, { isOpen: isOpenRestrictionModal, onClose: () => setIsOpenRestrictionModal(false), cloneId: clone.id }), _jsx(DestroyCloneModal, { isOpen: isOpenDestroyModal, onClose: () => setIsOpenDestroyModal(false), cloneId: clone.id, onDestroyClone: destroyClone }), _jsx(ResetCloneModal, { isOpen: isOpenResetModal, onClose: () => setIsOpenResetModal(false), clone: clone, snapshots: snapshots.data, onResetClone: resetClone, version: (_k = instance.state) === null || _k === void 0 ? void 0 : _k.engine.version })] })] })] }));
294
+ : protectionOptions, value: getCurrentProtectionValue(), onChange: handleProtectionChange, disabled: isDisabledControls, style: { minWidth: 200 } }), isUpdatingClone && _jsx(Spinner, { size: "sm", className: classes.spinner })] }), _jsxs("span", { className: classes.remark, children: ["Select how long this clone should be protected from deletion. Protected clones cannot be deleted manually or automatically.", _jsx("br", {}), "Check disk space regularly and delete this clone once your work is done."] }), stores.main.updateCloneError && (_jsx(ErrorStub, { title: "Updating error", message: stores.main.updateCloneError, className: classes.errorStub }))] }), _jsxs("div", { className: classes.snippetContainer, children: [_jsx(SectionTitle, { tag: "h2", level: 2, text: 'Reset clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can reset the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliResetCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Delete clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can delete the clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCliDestroyCloneCommand(props.cloneId) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Toggle deletion protection using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can toggle deletion protection using CLI for this clone using the following command:" }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(true) }), _jsx(SyntaxHighlight, { content: getCliProtectedCloneCommand(false) }), _jsx(SectionTitle, { className: classes.title, tag: "h2", level: 2, text: 'Create snapshot for this clone using CLI' }), _jsx("p", { className: classes.tooltip, children: "You can create a snapshot for this clone using CLI using the following command:" }), _jsx(SyntaxHighlight, { content: getCreateSnapshotCommand(props.cloneId) })] }), _jsxs(_Fragment, { children: [_jsx(DestroyCloneRestrictionModal, { isOpen: isOpenRestrictionModal, onClose: () => setIsOpenRestrictionModal(false), cloneId: clone.id }), _jsx(DestroyCloneModal, { isOpen: isOpenDestroyModal, onClose: () => setIsOpenDestroyModal(false), cloneId: clone.id, onDestroyClone: destroyClone }), _jsx(ResetCloneModal, { isOpen: isOpenResetModal, onClose: () => setIsOpenResetModal(false), clone: clone, snapshots: snapshots.data, onResetClone: resetClone, version: (_k = instance.state) === null || _k === void 0 ? void 0 : _k.engine.version })] })] })] }));
295
295
  });
@@ -166,7 +166,7 @@ export const CreateClone = observer((props) => {
166
166
  if (formik.errors.dbPassword) {
167
167
  formik.setFieldError('dbPassword', '');
168
168
  }
169
- }, isDisabled: isCreatingClone, children: "Generate" }), passwordGenerated && (_jsx("span", { className: styles.passwordHint, children: "New password created. Copy and save it securely." }))] }), _jsx("p", { className: cn(formik.errors.dbPassword && styles.error, styles.remark), children: formik.errors.dbPassword })] }), _jsxs("div", { className: styles.section, children: [_jsx("h2", { className: styles.title, children: "Clone protection" }), _jsx(Select, { label: "Deletion protection", items: (() => {
169
+ }, isDisabled: isCreatingClone, children: "Generate" }), passwordGenerated && (_jsx("span", { className: styles.passwordHint, children: "New password created. Copy and save it securely." }))] }), _jsx("p", { className: cn(formik.errors.dbPassword && styles.error, styles.remark), children: formik.errors.dbPassword })] }), _jsxs("div", { className: styles.section, children: [_jsx("h2", { className: styles.title, children: "Clone protection" }), _jsx(Select, { fullWidth: true, label: "Deletion protection", items: (() => {
170
170
  var _a, _b, _c, _d;
171
171
  const maxDurationMinutes = (_d = (_c = (_b = (_a = stores.main.instance) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.cloning) === null || _c === void 0 ? void 0 : _c.protectionMaxDurationMinutes) !== null && _d !== void 0 ? _d : 0;
172
172
  const allowForever = maxDurationMinutes === 0;
@@ -190,7 +190,7 @@ export const CreateClone = observer((props) => {
190
190
  .map(({ value, children }) => ({ value, children })),
191
191
  ...(allowForever ? [{ value: '0', children: 'Forever' }] : []),
192
192
  ];
193
- })(), value: formik.values.protectionDurationMinutes, onChange: (e) => formik.setFieldValue('protectionDurationMinutes', e.target.value), disabled: isCreatingClone, style: { minWidth: 100 } }), _jsxs("p", { className: styles.remark, children: ["Select how long this clone should be protected from deletion. Protected clones cannot be deleted manually or automatically.", _jsx("br", {}), "Check disk space regularly and delete this clone once the work is done."] })] }), _jsxs("div", { className: cn(styles.marginBottom, styles.section), children: [_jsxs(Paper, { className: styles.summary, children: [_jsx(InfoIcon, { className: styles.summaryIcon }), _jsxs("div", { className: styles.params, children: [_jsxs("p", { className: styles.param, children: [_jsx("span", { children: "Data size:" }), _jsx("strong", { children: ((_j = stores.main.instance.state) === null || _j === void 0 ? void 0 : _j.dataSize)
193
+ })(), value: formik.values.protectionDurationMinutes, onChange: (e) => formik.setFieldValue('protectionDurationMinutes', e.target.value), disabled: isCreatingClone }), _jsxs("p", { className: styles.remark, children: ["Select how long this clone should be protected from deletion. Protected clones cannot be deleted manually or automatically.", _jsx("br", {}), "Check disk space regularly and delete this clone once the work is done."] })] }), _jsxs("div", { className: cn(styles.marginBottom, styles.section), children: [_jsxs(Paper, { className: styles.summary, children: [_jsx(InfoIcon, { className: styles.summaryIcon }), _jsxs("div", { className: styles.params, children: [_jsxs("p", { className: styles.param, children: [_jsx("span", { children: "Data size:" }), _jsx("strong", { children: ((_j = stores.main.instance.state) === null || _j === void 0 ? void 0 : _j.dataSize)
194
194
  ? formatBytesIEC(stores.main.instance.state.dataSize)
195
195
  : '-' })] }), _jsxs("p", { className: styles.param, children: [_jsx("span", { children: "Expected cloning time:" }), _jsxs("strong", { children: [round((_k = stores.main.instance.state) === null || _k === void 0 ? void 0 : _k.cloning.expectedCloningTime, 2), ' ', "s"] })] })] })] }), stores.main.cloneError && (_jsx("div", { className: cn(styles.marginBottom, styles.section), children: _jsx(ErrorStub, { message: stores.main.cloneError }) })), _jsx("div", { className: styles.controls, children: _jsxs(Button, { onClick: formik.submitForm, variant: "primary", size: "medium", isDisabled: isCreatingClone, children: ["Create clone", isCreatingClone && (_jsx(Spinner, { size: "sm", className: styles.spinner }))] }) })] })] }), _jsx("div", { className: styles.form, children: _jsxs("div", { className: styles.snippetContainer, children: [_jsx(SectionTitle, { className: styles.title, tag: "h1", level: 1, text: "The same using CLI" }), _jsx("p", { className: styles.text, children: "Alternatively, you can create a new clone using CLI. Fill the form, copy the command below and paste it into your terminal." }), _jsx(SyntaxHighlight, { wrapLines: true, content: getCliCreateCloneCommand(formik.values, showPassword) }), _jsx(SectionTitle, { className: styles.title, tag: "h2", level: 2, text: "Check clone status" }), _jsx("p", { className: styles.text, children: "To check the status of your newly created clone, use the command below." }), _jsx(SyntaxHighlight, { content: getCliCloneStatus(formik.values.cloneId) })] }) })] })] }));
196
196
  });
@@ -16,6 +16,7 @@
16
16
 
17
17
  .form {
18
18
  flex: 1 1 0;
19
+ min-width: 0;
19
20
  }
20
21
 
21
22
  .snippetContainer {
@@ -571,7 +571,7 @@ export const Configuration = observer(({ instanceId, switchActiveTab, reload, is
571
571
  testConnectionState.fetchTuning.message
572
572
  ? 'error'
573
573
  : '', message: testConnectionState.fetchTuning.error ||
574
- testConnectionState.fetchTuning.message.message })) : null] }), _jsxs(Box, { children: [_jsxs(Box, { children: [_jsx(Typography, { className: styles.subsection, children: "Subsection \"retrieval.spec.logicalRestore\"" }), _jsx("span", { className: classes.grayText, children: "Restoring options." })] }), _jsx(InputWithTooltip, { label: "pg_restore jobs", value: formik.values.restoreParallelJobs, tooltipText: tooltipText.restoreParallelJobs, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('restoreParallelJobs', e.target.value) }), _jsx(InputWithChip, { value: formik.values.pgRestoreCustomOptions, label: "pg_restore customOptions", id: "pgRestoreCustomOptions", tooltipText: tooltipText.pgRestoreCustomOptions, handleDeleteChip: handleDeleteChip, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('pgRestoreCustomOptions', e.target.value) }), _jsx(FormControlLabel, { style: { maxWidth: 'max-content' }, control: _jsx(Checkbox, { name: "restoreIgnoreErrors", checked: formik.values.restoreIgnoreErrors, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('restoreIgnoreErrors', e.target.checked), classes: {
574
+ testConnectionState.fetchTuning.message.message })) : null] }), _jsxs(Box, { children: [_jsxs(Box, { children: [_jsx(Typography, { className: styles.subsection, children: "Subsection \"retrieval.spec.logicalRestore\"" }), _jsx("span", { className: classes.grayText, children: "Restoring options." })] }), _jsx(InputWithTooltip, { label: "pg_restore jobs", value: formik.values.restoreParallelJobs, tooltipText: tooltipText.restoreParallelJobs, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('restoreParallelJobs', e.target.value) }), _jsx(InputWithChip, { value: formik.values.pgRestoreCustomOptions, label: "pg_restore customOptions", id: "pgRestoreCustomOptions", tooltipText: tooltipText.pgRestoreCustomOptions, handleDeleteChip: handleDeleteChip, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('pgRestoreCustomOptions', e.target.value) }), _jsx(InputWithTooltip, { type: "textarea", label: "Restore PostgreSQL configs", value: formik.values.restoreConfigs, tooltipText: tooltipText.restoreConfigs, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('restoreConfigs', e.target.value) }), _jsx(FormControlLabel, { style: { maxWidth: 'max-content' }, control: _jsx(Checkbox, { name: "restoreIgnoreErrors", checked: formik.values.restoreIgnoreErrors, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('restoreIgnoreErrors', e.target.checked), classes: {
575
575
  root: classes.checkboxRoot,
576
576
  } }), label: 'Ignore errors during logical data restore' })] }), _jsx(Box, { mt: 1, children: _jsx(Typography, { className: styles.subsection, children: "Subsection \"retrieval.refresh\"" }) }), _jsxs("span", { className: classes.grayText, children: ["Define full data refresh on schedule. The process requires at least one additional filesystem mount point. The schedule is to be specified using", ' ', _jsxs("a", { href: "https://en.wikipedia.org/wiki/Cron#Overview", target: "_blank", className: styles.externalLink, children: ["crontab format", _jsx(ExternalIcon, { className: styles.externalIcon })] }), "."] }), _jsx(InputWithTooltip, { label: "timetable", value: formik.values.timetable, tooltipText: tooltipText.timetable, disabled: isConfigurationDisabled, onChange: (e) => formik.setFieldValue('timetable', e.target.value) })] }), _jsxs(Box, { mt: 2, mb: 2, sx: {
577
577
  display: 'flex',
@@ -15,6 +15,7 @@ export declare const tooltipText: {
15
15
  pgDumpCustomOptions: () => JSX.Element;
16
16
  restoreParallelJobs: () => JSX.Element;
17
17
  pgRestoreCustomOptions: () => JSX.Element;
18
+ restoreConfigs: () => JSX.Element;
18
19
  timetable: () => JSX.Element;
19
20
  tuningParams: () => JSX.Element;
20
21
  };
@@ -16,6 +16,7 @@ export const tooltipText = {
16
16
  pgDumpCustomOptions: () => (_jsx("div", { children: "pg_dump options to be used to create a database dump, for example: '--exclude-schema=repack --exclude-schema=\"camelStyleSchemaName\"'. Note that due to security reasons, the current implementation supports only letters, numbers, hyphen, underscore, equal sign, and double quotes." })),
17
17
  restoreParallelJobs: () => (_jsx("div", { children: "Number of parallel workers used to restore databases from dump to PostgreSQL managed by DBLab. For initial data retrieval (the first data refresh), it is recommended to match the number of available vCPUs on the machine running DBLab. This yields faster restore times but can increase CPU and disk I/O usage on that machine (up to temporary resource saturation). For subsequent refreshes, if DBLab is in continuous use, it is recommended to reduce this value by 50% to reserve capacity for normal DBLab operations (such as working with clones)." })),
18
18
  pgRestoreCustomOptions: () => (_jsx("div", { children: "pg_restore options to be used to restore from a database dump, for example: '--exclude-schema=repack --exclude-schema=\"camelStyleSchemaName\"'. Note that due to security reasons, the current implementation supports only letters, numbers, hyphen, underscore, equal sign, and double quotes." })),
19
+ restoreConfigs: () => (_jsxs("div", { children: ["PostgreSQL configuration parameters applied during logical restore (one", ' ', _jsx("span", { className: styles.firaCodeFont, children: "parameter=value" }), " per line). These settings are written to", ' ', _jsx("span", { className: styles.firaCodeFont, children: "postgresql.conf" }), " before restore starts and do not affect clones. Useful for tuning restore performance, for example:", _jsx("br", {}), _jsx("span", { className: styles.firaCodeFont, children: "maintenance_work_mem=8GB" }), _jsx("br", {}), _jsx("span", { className: styles.firaCodeFont, children: "max_parallel_maintenance_workers=7" }), _jsx("br", {}), _jsx("span", { className: styles.firaCodeFont, children: "shared_preload_libraries=" }), _jsx("br", {}), _jsx("span", { className: styles.firaCodeFont, children: "fsync=off" })] })),
19
20
  timetable: () => (_jsxs("div", { children: ["Schedule for full data refreshes, in", ' ', _jsx("a", { target: '_blank', href: 'https://en.wikipedia.org/wiki/Cron#Overview', className: styles.externalLink, children: "crontab format" }), "."] })),
20
21
  tuningParams: () => (_jsxs("div", { children: ["Query tuning parameters. These are essential to ensure that cloned PostgreSQL instances generate the same plans as the source (specifically, they are crucial for query performance troubleshooting and optimization, including working with EXPLAIN plans). For details, see the", ' ', _jsx("a", { target: '_blank', href: 'https://postgres.ai/docs/how-to-guides/administration/postgresql-configuration#postgresql-configuration-in-clones', className: styles.externalLink, children: "docs" }), "."] })),
21
22
  };
@@ -19,6 +19,7 @@ export declare type FormValues = {
19
19
  dumpIgnoreErrors: boolean;
20
20
  restoreParallelJobs: string;
21
21
  restoreIgnoreErrors: boolean;
22
+ restoreConfigs: string;
22
23
  pgDumpCustomOptions: string;
23
24
  pgRestoreCustomOptions: string;
24
25
  };
@@ -33,6 +33,7 @@ export const useForm = (onSubmit) => {
33
33
  databases: '',
34
34
  dumpParallelJobs: '',
35
35
  restoreParallelJobs: '',
36
+ restoreConfigs: '',
36
37
  pgDumpCustomOptions: '',
37
38
  pgRestoreCustomOptions: '',
38
39
  dumpIgnoreErrors: false,
@@ -98,6 +98,7 @@ export declare class MainStore {
98
98
  dumpIgnoreErrors: boolean | undefined;
99
99
  restoreParallelJobs: string | number | undefined;
100
100
  restoreIgnoreErrors: boolean | undefined;
101
+ restoreConfigs: string;
101
102
  pgDumpCustomOptions: string;
102
103
  pgRestoreCustomOptions: string;
103
104
  dockerTag?: string | undefined;
@@ -15,8 +15,10 @@ export const formatTuningParamsToObj = (tuningParams) => {
15
15
  if (tuningParams) {
16
16
  const tuningParamsArr = tuningParams.split('\n');
17
17
  tuningParamsArr.forEach((param) => {
18
- const paramArr = param.split('=');
19
- formattedTuningParams[paramArr[0]] = paramArr[1];
18
+ const eqIndex = param.indexOf('=');
19
+ if (eqIndex !== -1) {
20
+ formattedTuningParams[param.substring(0, eqIndex)] = param.substring(eqIndex + 1);
21
+ }
20
22
  });
21
23
  }
22
24
  return formattedTuningParams;
@@ -41,6 +41,9 @@ export declare type configTypes = {
41
41
  customOptions?: string[];
42
42
  parallelJobs?: string | number;
43
43
  ignoreErrors?: boolean;
44
+ configs?: {
45
+ [key: string]: string;
46
+ };
44
47
  };
45
48
  };
46
49
  };
@@ -62,6 +65,7 @@ export declare const formatConfig: (config: configTypes) => {
62
65
  dumpIgnoreErrors: boolean | undefined;
63
66
  restoreParallelJobs: string | number | undefined;
64
67
  restoreIgnoreErrors: boolean | undefined;
68
+ restoreConfigs: string;
65
69
  pgDumpCustomOptions: string;
66
70
  pgRestoreCustomOptions: string;
67
71
  dockerTag?: string | undefined;
@@ -32,6 +32,15 @@ export const formatConfig = (config) => {
32
32
  dumpIgnoreErrors: (_27 = (_26 = (_25 = (_24 = config.retrieval) === null || _24 === void 0 ? void 0 : _24.spec) === null || _25 === void 0 ? void 0 : _25.logicalDump) === null || _26 === void 0 ? void 0 : _26.options) === null || _27 === void 0 ? void 0 : _27.ignoreErrors,
33
33
  restoreParallelJobs: (_31 = (_30 = (_29 = (_28 = config.retrieval) === null || _28 === void 0 ? void 0 : _28.spec) === null || _29 === void 0 ? void 0 : _29.logicalRestore) === null || _30 === void 0 ? void 0 : _30.options) === null || _31 === void 0 ? void 0 : _31.parallelJobs,
34
34
  restoreIgnoreErrors: (_35 = (_34 = (_33 = (_32 = config.retrieval) === null || _32 === void 0 ? void 0 : _32.spec) === null || _33 === void 0 ? void 0 : _33.logicalRestore) === null || _34 === void 0 ? void 0 : _34.options) === null || _35 === void 0 ? void 0 : _35.ignoreErrors,
35
+ restoreConfigs: (() => {
36
+ var _a, _b, _c, _d;
37
+ const configs = (_d = (_c = (_b = (_a = config.retrieval) === null || _a === void 0 ? void 0 : _a.spec) === null || _b === void 0 ? void 0 : _b.logicalRestore) === null || _c === void 0 ? void 0 : _c.options) === null || _d === void 0 ? void 0 : _d.configs;
38
+ if (!configs || Object.keys(configs).length === 0)
39
+ return '';
40
+ return Object.entries(configs)
41
+ .map(([k, v]) => `${k}=${v}`)
42
+ .join('\n');
43
+ })(),
35
44
  pgDumpCustomOptions: formatDumpCustomOptions((_40 = (_39 = (_38 = (_37 = (_36 = config.retrieval) === null || _36 === void 0 ? void 0 : _36.spec) === null || _37 === void 0 ? void 0 : _37.logicalDump) === null || _38 === void 0 ? void 0 : _38.options) === null || _39 === void 0 ? void 0 : _39.customOptions) !== null && _40 !== void 0 ? _40 : null),
36
45
  pgRestoreCustomOptions: formatDumpCustomOptions((_45 = (_44 = (_43 = (_42 = (_41 = config.retrieval) === null || _41 === void 0 ? void 0 : _41.spec) === null || _42 === void 0 ? void 0 : _42.logicalRestore) === null || _43 === void 0 ? void 0 : _43.options) === null || _44 === void 0 ? void 0 : _44.customOptions) !== null && _45 !== void 0 ? _45 : null),
37
46
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postgres.ai/shared",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "scripts": {
5
5
  "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.js",
6
6
  "pack": "node scripts/pack.js"
@@ -651,7 +651,7 @@ export const Clone = observer((props: Props) => {
651
651
  </p>
652
652
  <div style={{ marginTop: '8px', marginBottom: '16px' }}>
653
653
  <Select
654
- label="Deletion protection"
654
+ label={"Deletion\u00a0protection"}
655
655
  items={
656
656
  clone.protected && clone.protectedTillDate
657
657
  ? [
@@ -663,7 +663,7 @@ export const Clone = observer((props: Props) => {
663
663
  value={getCurrentProtectionValue()}
664
664
  onChange={handleProtectionChange}
665
665
  disabled={isDisabledControls}
666
- style={{ minWidth: 100 }}
666
+ style={{ minWidth: 200 }}
667
667
  />
668
668
  {isUpdatingClone && <Spinner size="sm" className={classes.spinner} />}
669
669
  </div>
@@ -393,6 +393,7 @@ export const CreateClone = observer((props: Props) => {
393
393
  <h2 className={styles.title}>Clone protection</h2>
394
394
 
395
395
  <Select
396
+ fullWidth
396
397
  label="Deletion protection"
397
398
  items={(() => {
398
399
  const maxDurationMinutes = stores.main.instance?.state?.cloning?.protectionMaxDurationMinutes ?? 0
@@ -423,7 +424,6 @@ export const CreateClone = observer((props: Props) => {
423
424
  formik.setFieldValue('protectionDurationMinutes', e.target.value)
424
425
  }
425
426
  disabled={isCreatingClone}
426
- style={{ minWidth: 100 }}
427
427
  />
428
428
 
429
429
  <p className={styles.remark}>
@@ -16,6 +16,7 @@
16
16
 
17
17
  .form {
18
18
  flex: 1 1 0;
19
+ min-width: 0;
19
20
  }
20
21
 
21
22
  .snippetContainer {
@@ -1120,6 +1120,16 @@ export const Configuration = observer(
1120
1120
  )
1121
1121
  }
1122
1122
  />
1123
+ <InputWithTooltip
1124
+ type="textarea"
1125
+ label="Restore PostgreSQL configs"
1126
+ value={formik.values.restoreConfigs}
1127
+ tooltipText={tooltipText.restoreConfigs}
1128
+ disabled={isConfigurationDisabled}
1129
+ onChange={(e) =>
1130
+ formik.setFieldValue('restoreConfigs', e.target.value)
1131
+ }
1132
+ />
1123
1133
  <FormControlLabel
1124
1134
  style={{ maxWidth: 'max-content' }}
1125
1135
  control={
@@ -138,6 +138,26 @@ export const tooltipText = {
138
138
  hyphen, underscore, equal sign, and double quotes.
139
139
  </div>
140
140
  ),
141
+ restoreConfigs: () => (
142
+ <div>
143
+ PostgreSQL configuration parameters applied during logical restore (one{' '}
144
+ <span className={styles.firaCodeFont}>parameter=value</span> per line).
145
+ These settings are written to{' '}
146
+ <span className={styles.firaCodeFont}>postgresql.conf</span> before
147
+ restore starts and do not affect clones. Useful for tuning restore
148
+ performance, for example:
149
+ <br />
150
+ <span className={styles.firaCodeFont}>maintenance_work_mem=8GB</span>
151
+ <br />
152
+ <span className={styles.firaCodeFont}>
153
+ max_parallel_maintenance_workers=7
154
+ </span>
155
+ <br />
156
+ <span className={styles.firaCodeFont}>shared_preload_libraries=</span>
157
+ <br />
158
+ <span className={styles.firaCodeFont}>fsync=off</span>
159
+ </div>
160
+ ),
141
161
  timetable: () => (
142
162
  <div>
143
163
  Schedule for full data refreshes, in{' '}
@@ -28,6 +28,7 @@ export type FormValues = {
28
28
  dumpIgnoreErrors: boolean
29
29
  restoreParallelJobs: string
30
30
  restoreIgnoreErrors: boolean
31
+ restoreConfigs: string
31
32
  pgDumpCustomOptions: string
32
33
  pgRestoreCustomOptions: string
33
34
  }
@@ -60,6 +61,7 @@ export const useForm = (onSubmit: (values: FormValues) => void) => {
60
61
  databases: '',
61
62
  dumpParallelJobs: '',
62
63
  restoreParallelJobs: '',
64
+ restoreConfigs: '',
63
65
  pgDumpCustomOptions: '',
64
66
  pgRestoreCustomOptions: '',
65
67
  dumpIgnoreErrors: false,
Binary file
@@ -39,8 +39,10 @@ export const formatTuningParamsToObj = (tuningParams: string | undefined) => {
39
39
  if (tuningParams) {
40
40
  const tuningParamsArr = tuningParams.split('\n')
41
41
  tuningParamsArr.forEach((param) => {
42
- const paramArr = param.split('=')
43
- formattedTuningParams[paramArr[0]] = paramArr[1]
42
+ const eqIndex = param.indexOf('=')
43
+ if (eqIndex !== -1) {
44
+ formattedTuningParams[param.substring(0, eqIndex)] = param.substring(eqIndex + 1)
45
+ }
44
46
  })
45
47
  }
46
48
 
@@ -51,6 +51,7 @@ export type configTypes = {
51
51
  customOptions?: string[]
52
52
  parallelJobs?: string | number
53
53
  ignoreErrors?: boolean
54
+ configs?: { [key: string]: string }
54
55
  }
55
56
  }
56
57
  }
@@ -103,6 +104,14 @@ export const formatConfig = (config: configTypes) => {
103
104
  config.retrieval?.spec?.logicalRestore?.options?.parallelJobs,
104
105
  restoreIgnoreErrors:
105
106
  config.retrieval?.spec?.logicalRestore?.options?.ignoreErrors,
107
+ restoreConfigs: (() => {
108
+ const configs =
109
+ config.retrieval?.spec?.logicalRestore?.options?.configs
110
+ if (!configs || Object.keys(configs).length === 0) return ''
111
+ return Object.entries(configs)
112
+ .map(([k, v]) => `${k}=${v}`)
113
+ .join('\n')
114
+ })(),
106
115
  pgDumpCustomOptions: formatDumpCustomOptions(
107
116
  (config.retrieval?.spec?.logicalDump?.options
108
117
  ?.customOptions as string[] | undefined) ?? null,
Binary file