@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
@@ -1,6 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, IconButton, TextField, Typography } from '@material-ui/core';
3
3
  import Box from '@mui/material/Box';
4
+ // MASKED_ENV_VALUE is what the engine returns instead of a stored env value:
5
+ // envs carry WAL-G and pgBackRest credentials, so GET /admin/config lists
6
+ // the keys only. Posting the mask back for a key keeps the stored value.
7
+ export const MASKED_ENV_VALUE = '****';
8
+ export const isMaskedEnvValue = (value) => value === MASKED_ENV_VALUE;
9
+ const MASKED_HINT = 'Stored on the server; kept unless you change it';
4
10
  // EnvsEditor renders a rows-of-key/value editor with add/remove and a
5
11
  // click-to-add suggestion list. Engine consumes envs as a free-form map
6
12
  // (physical.go:76, CopyOptions.Envs map[string]string); the UI is a thin
@@ -20,5 +26,6 @@ export const EnvsEditor = ({ envs, onChange, suggestions = [], disabled, keyErro
20
26
  onChange([...envs, { key, value: '' }]);
21
27
  };
22
28
  const usedKeys = new Set(envs.map((e) => e.key));
23
- return (_jsxs(Box, { mt: 1, "data-testid": "envs-editor", children: [_jsx(Typography, { variant: "subtitle2", children: "Environment variables" }), envs.length === 0 ? (_jsx(Box, { mt: 1, mb: 1, children: _jsx(Typography, { variant: "caption", color: "textSecondary", children: "No environment variables set. Use suggestions below or click \"Add\"." }) })) : (envs.map((env, i) => (_jsxs(Box, { display: "flex", alignItems: "center", mt: 1, "data-testid": `envs-row-${i}`, children: [_jsx(TextField, { size: "small", label: "Name", value: env.key, disabled: disabled, error: Boolean(keyErrors[i]), helperText: keyErrors[i], onChange: (e) => updateRow(i, { key: e.target.value }), inputProps: { 'data-testid': `envs-key-${i}` } }), _jsx(Box, { mx: 1, children: _jsx(TextField, { size: "small", label: "Value", value: env.value, disabled: disabled, onChange: (e) => updateRow(i, { value: e.target.value }), inputProps: { 'data-testid': `envs-value-${i}` } }) }), _jsx(IconButton, { size: "small", "aria-label": "remove env", disabled: disabled, onClick: () => removeRow(i), "data-testid": `envs-remove-${i}`, children: "\u00D7" })] }, i)))), _jsx(Box, { mt: 1, children: _jsx(Button, { size: "small", variant: "outlined", disabled: disabled, onClick: () => addRow(), "data-testid": "envs-add", children: "+ Add" }) }), suggestions.length > 0 && (_jsxs(Box, { mt: 1, children: [_jsx(Typography, { variant: "caption", color: "textSecondary", children: "Suggestions:" }), _jsx(Box, { mt: 0.5, children: suggestions.map((s) => (_jsx(Button, { size: "small", variant: "text", disabled: disabled || usedKeys.has(s), onClick: () => addRow(s), "data-testid": `envs-suggest-${s}`, children: s }, s))) })] }))] }));
29
+ const hasMasked = envs.some((e) => isMaskedEnvValue(e.value));
30
+ return (_jsxs(Box, { mt: 1, "data-testid": "envs-editor", children: [_jsx(Typography, { variant: "subtitle2", children: "Environment variables" }), hasMasked && (_jsx(Box, { mt: 0.5, children: _jsxs(Typography, { variant: "caption", color: "textSecondary", "data-testid": "envs-masked-hint", children: ["Values shown as ", MASKED_ENV_VALUE, " are stored on the server and are kept unless you change them. Renaming such a variable requires entering its value again."] }) })), envs.length === 0 ? (_jsx(Box, { mt: 1, mb: 1, children: _jsx(Typography, { variant: "caption", color: "textSecondary", children: "No environment variables set. Use suggestions below or click \"Add\"." }) })) : (envs.map((env, i) => (_jsxs(Box, { display: "flex", alignItems: "center", mt: 1, "data-testid": `envs-row-${i}`, children: [_jsx(TextField, { size: "small", label: "Name", value: env.key, disabled: disabled, error: Boolean(keyErrors[i]), helperText: keyErrors[i], onChange: (e) => updateRow(i, { key: e.target.value }), inputProps: { 'data-testid': `envs-key-${i}` } }), _jsx(Box, { mx: 1, children: _jsx(TextField, { size: "small", label: "Value", value: env.value, disabled: disabled, helperText: isMaskedEnvValue(env.value) ? MASKED_HINT : undefined, onChange: (e) => updateRow(i, { value: e.target.value }), inputProps: { 'data-testid': `envs-value-${i}` } }) }), _jsx(IconButton, { size: "small", "aria-label": "remove env", disabled: disabled, onClick: () => removeRow(i), "data-testid": `envs-remove-${i}`, children: "\u00D7" })] }, i)))), _jsx(Box, { mt: 1, children: _jsx(Button, { size: "small", variant: "outlined", disabled: disabled, onClick: () => addRow(), "data-testid": "envs-add", children: "+ Add" }) }), suggestions.length > 0 && (_jsxs(Box, { mt: 1, children: [_jsx(Typography, { variant: "caption", color: "textSecondary", children: "Suggestions:" }), _jsx(Box, { mt: 0.5, children: suggestions.map((s) => (_jsx(Button, { size: "small", variant: "text", disabled: disabled || usedKeys.has(s), onClick: () => addRow(s), "data-testid": `envs-suggest-${s}`, children: s }, s))) })] }))] }));
24
31
  };
@@ -1,11 +1,13 @@
1
1
  /// <reference types="react" />
2
2
  import { ProposedConfig } from '@postgres.ai/shared/types/api/endpoints/probeSource';
3
+ import { ResolvedImage } from '../configOptions';
3
4
  declare type Props = {
4
5
  proposed: ProposedConfig;
6
+ resolved: ResolvedImage;
5
7
  applying: boolean;
6
8
  applyError: string | null;
7
9
  onApply: () => void;
8
10
  onEdit: () => void;
9
11
  };
10
- export declare const PreviewCard: ({ proposed, applying, applyError, onApply, onEdit, }: Props) => JSX.Element;
12
+ export declare const PreviewCard: ({ proposed, resolved, applying, applyError, onApply, onEdit, }: Props) => JSX.Element;
11
13
  export {};
@@ -3,12 +3,12 @@ import { Button, Link, Typography } from '@material-ui/core';
3
3
  import Box from '@mui/material/Box';
4
4
  import { Spinner } from '@postgres.ai/shared/components/Spinner';
5
5
  import { providerKeyToImage } from '../configOptions';
6
- const Field = ({ label, value }) => (_jsxs(Box, { display: "flex", mb: 0.5, children: [_jsx(Box, { minWidth: 220, fontWeight: 600, children: label }), _jsx(Box, { style: { wordBreak: 'break-all' }, children: value })] }));
7
- const Callout = ({ children }) => (_jsx(Box, { mt: 1, p: 1, bgcolor: "#fff8e1", borderLeft: "4px solid #f5a623", fontSize: 13, children: children }));
8
- export const PreviewCard = ({ proposed, applying, applyError, onApply, onEdit, }) => {
9
- var _a, _b;
6
+ const Field = ({ label, value }) => (_jsxs(Box, { display: "flex", mb: 0.5, children: [_jsx(Box, { minWidth: 220, fontWeight: 600, children: label }), _jsx(Box, { minWidth: 0, style: { overflowWrap: 'anywhere' }, children: value })] }));
7
+ const Callout = ({ children }) => (_jsx(Box, { mt: 1, p: 1, bgcolor: "#fff8e1", borderLeft: "4px solid #f5a623", fontSize: 13, style: { overflowWrap: 'anywhere' }, children: children }));
8
+ export const PreviewCard = ({ proposed, resolved, applying, applyError, onApply, onEdit, }) => {
9
+ var _a, _b, _c;
10
10
  const mapping = providerKeyToImage(proposed.dockerImage, proposed.pgMajorVersion);
11
- const resolvedTag = proposed.dockerTag || mapping.defaultTag || '(latest)';
12
- const tuningEntries = Object.entries((_a = proposed.queryTuning) !== null && _a !== void 0 ? _a : {});
13
- return (_jsxs(Box, { mt: 2, p: 2, border: "1px solid #e0e0e0", borderRadius: 4, "data-testid": "preview-card", children: [_jsx(Typography, { variant: "h6", children: "Proposed configuration" }), _jsxs(Box, { mt: 2, children: [_jsx(Field, { label: "Detected provider", value: proposed.detectedProvider || 'unknown' }), _jsx(Field, { label: "Docker image", value: mapping.imageType }), _jsx(Field, { label: "Docker tag", value: resolvedTag }), _jsx(Field, { label: "Postgres major version", value: String(proposed.pgMajorVersion || 'unknown') }), _jsx(Field, { label: "Databases", value: ((_b = proposed.databases) === null || _b === void 0 ? void 0 : _b.join(', ')) || '(none)' }), _jsx(Field, { label: "shared_buffers", value: proposed.sharedBuffers || '' }), _jsx(Field, { label: "shared_preload_libraries", value: proposed.sharedPreloadLibraries || '' })] }), tuningEntries.length > 0 && (_jsxs(Box, { mt: 2, children: [_jsx(Typography, { variant: "subtitle2", children: "Query tuning" }), _jsx(Box, { component: "table", mt: 1, style: { borderCollapse: 'collapse' }, children: _jsx("tbody", { children: tuningEntries.map(([k, v]) => (_jsxs("tr", { children: [_jsx("td", { style: { padding: '2px 16px 2px 0', fontWeight: 600 }, children: k }), _jsx("td", { style: { padding: '2px 0' }, children: v })] }, k))) }) })] })), _jsxs(Box, { mt: 2, children: [(mapping.fallback || proposed.detectedProvider === 'generic') && (_jsx(Callout, { children: "Could not detect a managed cloud provider; using the generic Postgres image. Switch to Expert mode if your source runs on a managed service and we missed it." })), !proposed.memoryProbed && (_jsxs(Callout, { children: ["Could not detect host memory; ", _jsx("code", { children: "shared_buffers" }), " is set to a 1\u00A0GB safe default. Adjust in Expert mode if your host has more RAM."] })), _jsx(Callout, { children: "Query tuning is copied from your source. If you use the RDS refresh tool, these values may not match production \u2014 review in Expert mode after the first retrieval run." }), _jsxs(Callout, { children: ["We'll ship ", _jsx("code", { children: proposed.sharedPreloadLibraries }), ". If the chosen image does not bundle one of these libraries, the clone container will fail to start with a \"could not load library\" error \u2014 check ", _jsx("code", { children: "docker logs dblab_server" }), " after Apply."] })] }), _jsxs(Box, { mt: 2, display: "flex", alignItems: "center", children: [_jsxs(Button, { variant: "contained", color: "secondary", onClick: onApply, disabled: applying, "data-testid": "preview-apply", children: ["Apply & start retrieval", applying && _jsx(Spinner, { size: "sm" })] }), _jsx(Box, { ml: 2, children: _jsx(Link, { component: "button", type: "button", onClick: onEdit, "data-testid": "preview-edit", children: "Edit before applying" }) })] }), applyError && (_jsx(Box, { mt: 1, color: "#d32f2f", fontSize: 13, "data-testid": "apply-error", children: applyError }))] }));
11
+ const preloadLibraries = (_a = resolved.sharedPreloadLibraries) !== null && _a !== void 0 ? _a : proposed.sharedPreloadLibraries;
12
+ const tuningEntries = Object.entries((_b = proposed.queryTuning) !== null && _b !== void 0 ? _b : {});
13
+ return (_jsxs(Box, { mt: 2, p: 2, border: "1px solid #e0e0e0", borderRadius: 4, "data-testid": "preview-card", children: [_jsx(Typography, { variant: "h6", children: "Proposed configuration" }), _jsxs(Box, { mt: 2, children: [_jsx(Field, { label: "Detected provider", value: proposed.detectedProvider || 'unknown' }), _jsx(Field, { label: "Docker image", value: resolved.dockerPath || mapping.imageType }), _jsx(Field, { label: "Postgres major version", value: String(proposed.pgMajorVersion || 'unknown') }), _jsx(Field, { label: "Databases", value: ((_c = proposed.databases) === null || _c === void 0 ? void 0 : _c.join(', ')) || '(none)' }), _jsx(Field, { label: "shared_buffers", value: proposed.sharedBuffers || '' }), _jsx(Field, { label: "shared_preload_libraries", value: preloadLibraries || '' })] }), tuningEntries.length > 0 && (_jsxs(Box, { mt: 2, children: [_jsx(Typography, { variant: "subtitle2", children: "Query tuning" }), _jsx(Box, { component: "table", mt: 1, style: { borderCollapse: 'collapse' }, children: _jsx("tbody", { children: tuningEntries.map(([k, v]) => (_jsxs("tr", { children: [_jsx("td", { style: { padding: '2px 16px 2px 0', fontWeight: 600 }, children: k }), _jsx("td", { style: { padding: '2px 0' }, children: v })] }, k))) }) })] })), _jsxs(Box, { mt: 2, children: [(mapping.fallback || proposed.detectedProvider === 'generic') && (_jsx(Callout, { children: "Could not detect a managed cloud provider; using the generic Postgres image. Switch to Expert mode if your source runs on a managed service and we missed it." })), !proposed.memoryProbed && (_jsxs(Callout, { children: ["Could not detect host memory; ", _jsx("code", { children: "shared_buffers" }), " is set to a 1\u00A0GB safe default. Adjust in Expert mode if your host has more RAM."] })), _jsx(Callout, { children: "Query tuning is copied from your source. If you use the RDS refresh tool, these values may not match production \u2014 review in Expert mode after the first retrieval run." }), resolved.isSe ? (_jsxs(Callout, { children: ["Shipping the ", _jsx("code", { children: resolved.dockerPath }), " image with its curated ", _jsx("code", { children: "shared_preload_libraries" }), " preset, so the detected extensions are bundled."] })) : (_jsxs(Callout, { children: ["We'll ship ", _jsx("code", { children: preloadLibraries }), ". If the chosen image does not bundle one of these libraries, the clone container will fail to start with a \"could not load library\" error \u2014 check", ' ', _jsx("code", { children: "docker logs dblab_server" }), " after Apply."] }))] }), _jsxs(Box, { mt: 2, display: "flex", alignItems: "center", children: [_jsxs(Button, { variant: "contained", color: "secondary", onClick: onApply, disabled: applying, "data-testid": "preview-apply", children: ["Apply & start retrieval", applying && _jsx(Spinner, { size: "sm" })] }), _jsx(Box, { ml: 2, children: _jsx(Link, { component: "button", type: "button", onClick: onEdit, "data-testid": "preview-edit", children: "Edit before applying" }) })] }), applyError && (_jsx(Box, { mt: 1, color: "#d32f2f", fontSize: 13, "data-testid": "apply-error", children: applyError }))] }));
14
14
  };
@@ -1,5 +1,7 @@
1
1
  /// <reference types="react" />
2
+ import { SeImages } from '@postgres.ai/shared/types/api/endpoints/getSeImages';
2
3
  import { ProposedConfig } from '@postgres.ai/shared/types/api/endpoints/probeSource';
4
+ import { ResolvedImage } from '../configOptions';
3
5
  import { FormValues } from '../useForm';
4
6
  declare type Props = {
5
7
  instanceId: string;
@@ -7,7 +9,10 @@ declare type Props = {
7
9
  onApplied?: () => void;
8
10
  onEdit?: (proposed: ProposedConfig, password: string) => void;
9
11
  };
10
- export declare const buildProjectionFromProposed: (proposed: ProposedConfig, password: string) => FormValues;
12
+ export declare const buildProjectionFromProposed: (proposed: ProposedConfig, password: string, resolved?: ResolvedImage) => FormValues;
13
+ export declare const resolveProbeImage: (proposed: ProposedConfig, getSeImages: (args: {
14
+ packageGroup: string;
15
+ }) => Promise<SeImages[] | null | undefined>) => Promise<ResolvedImage>;
11
16
  export declare const SimpleMode: (({ instanceId, disabled, onApplied, onEdit }: Props) => JSX.Element) & {
12
17
  displayName: string;
13
18
  };
@@ -5,28 +5,41 @@ import { Button, TextField, Typography } from '@material-ui/core';
5
5
  import Box from '@mui/material/Box';
6
6
  import { Spinner } from '@postgres.ai/shared/components/Spinner';
7
7
  import { useStores } from '@postgres.ai/shared/pages/Instance/context';
8
- import { providerKeyToImage } from '../configOptions';
9
- import { genericImagePrefix } from '../dockerCatalog';
8
+ import { providerKeyToImage, resolveDockerImagePath, selectSeImage, } from '../configOptions';
10
9
  import { PreviewCard } from './PreviewCard';
10
+ // genericFallbackImage resolves the generic CE image for a probed source. Used
11
+ // when no SE image is available (CE, or a managed provider with no SE image for
12
+ // the detected major version).
13
+ const genericFallbackImage = (proposed) => {
14
+ const mapping = providerKeyToImage(proposed.dockerImage, proposed.pgMajorVersion);
15
+ const isGeneric = mapping.imageType === 'Generic Postgres';
16
+ return {
17
+ dockerPath: resolveDockerImagePath(proposed.dockerImage, proposed.pgMajorVersion, proposed.dockerTag),
18
+ dockerTag: isGeneric ? proposed.dockerTag || mapping.defaultTag || '' : '',
19
+ sharedPreloadLibraries: proposed.sharedPreloadLibraries,
20
+ isSe: false,
21
+ };
22
+ };
11
23
  // Translates a ProposedConfig from POST /admin/probe-source into the
12
24
  // FormValues shape the Expert form uses. Both Apply (→ updateConfig) and
13
25
  // Edit (→ formik.setValues) consume it, so the engine receives the same
14
- // projection regardless of which flow the user picks.
15
- export const buildProjectionFromProposed = (proposed, password) => {
26
+ // projection regardless of which flow the user picks. The resolved image (SE
27
+ // or generic) is passed in so the preview and the applied config never diverge.
28
+ export const buildProjectionFromProposed = (proposed, password, resolved) => {
29
+ var _a;
16
30
  const mapping = providerKeyToImage(proposed.dockerImage, proposed.pgMajorVersion);
17
- const tag = proposed.dockerTag || mapping.defaultTag || '';
18
31
  const isGeneric = mapping.imageType === 'Generic Postgres';
19
- const dockerPath = isGeneric ? `${genericImagePrefix}:${tag}` : '';
32
+ const image = resolved !== null && resolved !== void 0 ? resolved : genericFallbackImage(proposed);
20
33
  return {
21
34
  debug: false,
22
35
  dockerImage: isGeneric
23
36
  ? String(proposed.pgMajorVersion)
24
37
  : mapping.imageType,
25
38
  dockerImageType: mapping.imageType,
26
- dockerPath,
27
- dockerTag: tag,
39
+ dockerPath: image.dockerPath,
40
+ dockerTag: image.dockerTag,
28
41
  sharedBuffers: proposed.sharedBuffers,
29
- sharedPreloadLibraries: proposed.sharedPreloadLibraries,
42
+ sharedPreloadLibraries: (_a = image.sharedPreloadLibraries) !== null && _a !== void 0 ? _a : proposed.sharedPreloadLibraries,
30
43
  // tuningParams is typed as string on FormValues but updateConfig.ts
31
44
  // spreads it as a key-value object; cast matches the Expert form's
32
45
  // formatTuningParamsToObj(...) as unknown as string pattern.
@@ -41,10 +54,14 @@ export const buildProjectionFromProposed = (proposed, password) => {
41
54
  dumpParallelJobs: '',
42
55
  dumpIgnoreErrors: false,
43
56
  restoreParallelJobs: '',
44
- restoreIgnoreErrors: false,
57
+ // managed sources reference cloud-only extensions/objects (e.g. supabase_vault)
58
+ // that are absent from the clone image; best-effort restore skips them.
59
+ restoreIgnoreErrors: true,
45
60
  restoreConfigs: '',
46
61
  pgDumpCustomOptions: '',
47
- pgRestoreCustomOptions: '',
62
+ // skip ownership and privileges on restore: managed sources (Supabase, RDS)
63
+ // reference roles that do not exist in the clone, which would abort pg_restore.
64
+ pgRestoreCustomOptions: '--no-owner --no-privileges --no-tablespaces',
48
65
  retrievalMode: 'logical',
49
66
  physicalTool: '',
50
67
  physicalDockerImage: '',
@@ -55,6 +72,27 @@ export const buildProjectionFromProposed = (proposed, password) => {
55
72
  physicalEnvs: [],
56
73
  };
57
74
  };
75
+ // resolveProbeImage selects the platform SE image for a managed provider when
76
+ // the SE catalog is reachable — SE/Enterprise instances expose it (platformUrl
77
+ // set), CE returns undefined. Falls back to the generic image for CE and for
78
+ // managed providers with no SE image at the detected major version.
79
+ export const resolveProbeImage = async (proposed, getSeImages) => {
80
+ var _a;
81
+ const mapping = providerKeyToImage(proposed.dockerImage, proposed.pgMajorVersion);
82
+ if (mapping.imageType !== 'Generic Postgres') {
83
+ const seImages = await getSeImages({ packageGroup: mapping.imageType });
84
+ const se = selectSeImage(seImages, proposed.pgMajorVersion);
85
+ if (se) {
86
+ return {
87
+ dockerPath: se.location,
88
+ dockerTag: se.tag,
89
+ sharedPreloadLibraries: (_a = se.pg_config_presets) === null || _a === void 0 ? void 0 : _a.shared_preload_libraries,
90
+ isSe: true,
91
+ };
92
+ }
93
+ }
94
+ return genericFallbackImage(proposed);
95
+ };
58
96
  export const SimpleMode = observer(({ instanceId, disabled, onApplied, onEdit }) => {
59
97
  const stores = useStores();
60
98
  const main = stores.main;
@@ -63,45 +101,66 @@ export const SimpleMode = observer(({ instanceId, disabled, onApplied, onEdit })
63
101
  const [probing, setProbing] = useState(false);
64
102
  const [probeError, setProbeError] = useState(null);
65
103
  const [proposed, setProposed] = useState(null);
104
+ const [resolved, setResolved] = useState(null);
66
105
  const [applying, setApplying] = useState(false);
67
106
  const [applyError, setApplyError] = useState(null);
68
107
  const onDetect = async () => {
69
108
  setProbing(true);
70
109
  setProbeError(null);
71
110
  setProposed(null);
111
+ setResolved(null);
72
112
  setApplyError(null);
73
113
  const result = await main.probeSource({ url, password });
74
- setProbing(false);
75
114
  if (!result) {
115
+ setProbing(false);
76
116
  setProbeError('Probe is not available on this instance.');
77
117
  return;
78
118
  }
79
119
  if (result.error) {
120
+ setProbing(false);
80
121
  setProbeError(result.error.message);
81
122
  return;
82
123
  }
83
- if (result.response)
124
+ if (result.response) {
125
+ const image = await resolveProbeImage(result.response, main.getSeImages);
126
+ setResolved(image);
84
127
  setProposed(result.response);
128
+ }
129
+ setProbing(false);
85
130
  };
86
131
  const onApply = async () => {
87
132
  var _a;
88
- if (!proposed)
133
+ if (!proposed || !resolved)
89
134
  return;
90
135
  setApplying(true);
91
136
  setApplyError(null);
92
- const projection = buildProjectionFromProposed(proposed, password);
93
- const response = await main.updateConfig(projection, instanceId);
94
- setApplying(false);
95
- if (!response) {
96
- setApplyError((_a = main.configError) !== null && _a !== void 0 ? _a : 'Could not apply the proposed configuration.');
97
- return;
137
+ try {
138
+ const projection = buildProjectionFromProposed(proposed, password, resolved);
139
+ const response = await main.updateConfig(projection, instanceId);
140
+ if (!response) {
141
+ setApplyError((_a = main.configError) !== null && _a !== void 0 ? _a : 'Could not apply the proposed configuration.');
142
+ return;
143
+ }
144
+ const refresh = await main.fullRefresh(instanceId);
145
+ if (refresh === null || refresh === void 0 ? void 0 : refresh.error) {
146
+ setApplyError(`Configuration applied, but starting data retrieval failed: ${refresh.error.message}`);
147
+ return;
148
+ }
149
+ onApplied === null || onApplied === void 0 ? void 0 : onApplied();
150
+ }
151
+ catch (err) {
152
+ setApplyError(err instanceof Error
153
+ ? err.message
154
+ : 'Could not apply the proposed configuration.');
155
+ }
156
+ finally {
157
+ setApplying(false);
98
158
  }
99
- onApplied === null || onApplied === void 0 ? void 0 : onApplied();
100
159
  };
101
160
  const handleEdit = () => {
102
161
  if (proposed)
103
162
  onEdit === null || onEdit === void 0 ? void 0 : onEdit(proposed, password);
104
163
  };
105
164
  const canDetect = !probing && url.trim().length > 0 && password.length > 0 && !disabled;
106
- return (_jsxs(Box, { mt: 2, mb: 2, "data-testid": "simple-mode", children: [!proposed && (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", children: "Simple configuration" }), _jsx(Typography, { variant: "body2", children: "Paste your source connection string and password. We'll probe the source, propose a configuration, and let you review before starting retrieval." }), _jsx(Box, { mt: 2, children: _jsx(TextField, { label: "Connection string", placeholder: "postgres://user@host:5432/dbname", value: url, onChange: (e) => setUrl(e.target.value), fullWidth: true, disabled: probing || disabled, inputProps: { 'data-testid': 'simple-url' } }) }), _jsx(Box, { mt: 2, children: _jsx(TextField, { label: "Password", type: "password", value: password, onChange: (e) => setPassword(e.target.value), fullWidth: true, disabled: probing || disabled, inputProps: { 'data-testid': 'simple-password' } }) }), _jsx(Box, { mt: 2, children: _jsxs(Button, { variant: "contained", color: "secondary", onClick: onDetect, disabled: !canDetect, "data-testid": "simple-detect", children: ["Detect & preview", probing && _jsx(Spinner, { size: "sm" })] }) }), probeError && (_jsx(Box, { mt: 1, color: "#d32f2f", fontSize: 13, "data-testid": "probe-error", children: probeError }))] })), proposed && (_jsx(PreviewCard, { proposed: proposed, applying: applying, applyError: applyError, onApply: onApply, onEdit: handleEdit }))] }));
165
+ return (_jsxs(Box, { mt: 2, mb: 2, "data-testid": "simple-mode", children: [!proposed && (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", children: "Simple configuration" }), _jsx(Typography, { variant: "body2", children: "Paste your source connection string and password. We'll probe the source, propose a configuration, and let you review before starting retrieval." }), _jsx(Box, { mt: 2, children: _jsx(TextField, { label: "Connection string", placeholder: "postgres://user@host:5432/dbname", value: url, onChange: (e) => setUrl(e.target.value), fullWidth: true, disabled: probing || disabled, inputProps: { 'data-testid': 'simple-url' } }) }), _jsx(Box, { mt: 2, children: _jsx(TextField, { label: "Password", type: "password", value: password, onChange: (e) => setPassword(e.target.value), fullWidth: true, disabled: probing || disabled, inputProps: { 'data-testid': 'simple-password' } }) }), _jsx(Box, { mt: 2, children: _jsxs(Button, { variant: "contained", color: "secondary", onClick: onDetect, disabled: !canDetect, "data-testid": "simple-detect", children: ["Detect & preview", probing && _jsx(Spinner, { size: "sm" })] }) }), probeError && (_jsx(Box, { mt: 1, color: "#d32f2f", fontSize: 13, "data-testid": "probe-error", children: probeError }))] })), proposed && resolved && (_jsx(PreviewCard, { proposed: proposed, resolved: resolved, applying: applying, applyError: applyError, onApply: onApply, onEdit: handleEdit }))] }));
107
166
  });
@@ -1,8 +1,18 @@
1
+ import { SeImages } from '@postgres.ai/shared/types/api/endpoints/getSeImages';
1
2
  export declare type ProviderImageMapping = {
2
3
  imageType: string;
3
4
  defaultTag?: string;
4
5
  fallback: boolean;
5
6
  };
7
+ export declare const genericImagePathForVersion: (pgMajorVersion: number) => string;
8
+ export declare type ResolvedImage = {
9
+ dockerPath: string;
10
+ dockerTag: string;
11
+ sharedPreloadLibraries?: string;
12
+ isSe: boolean;
13
+ };
14
+ export declare const selectSeImage: (seImages: SeImages[] | null | undefined, pgMajorVersion: number) => SeImages | undefined;
15
+ export declare const resolveDockerImagePath: (providerKey: string, pgMajorVersion: number, dockerTag?: string) => string;
6
16
  export declare const providerKeyToImage: (providerKey: string, pgMajorVersion: number) => ProviderImageMapping;
7
17
  export declare const dockerImageOptions: {
8
18
  name: string;
@@ -1,4 +1,4 @@
1
- import { dockerImagesConfig } from './dockerCatalog';
1
+ import { dockerImagesConfig, genericImagePrefix } from './dockerCatalog';
2
2
  // Mapping from the engine probe's provider key (probe.Provider) to a
3
3
  // dockerImageOptions.type value the form already understands. Keys must
4
4
  // match the values defined in engine/internal/retrieval/probe/provider.go
@@ -24,6 +24,29 @@ const genericDefaultTag = (pgMajorVersion) => {
24
24
  return undefined;
25
25
  return `${version}-${tags[0]}`;
26
26
  };
27
+ // genericImagePathForVersion returns a pullable generic extended-postgres
28
+ // reference for a PG major version, or '' when the version is unknown. Simple
29
+ // mode uses it as the CE fallback for managed providers, whose SE images are
30
+ // only resolvable through the platform-only getSeImages call.
31
+ export const genericImagePathForVersion = (pgMajorVersion) => {
32
+ const tag = genericDefaultTag(pgMajorVersion);
33
+ return tag ? `${genericImagePrefix}:${tag}` : '';
34
+ };
35
+ // selectSeImage picks the SE catalog entry matching the detected major version.
36
+ // Returns undefined when the catalog is empty (CE) or has no matching version,
37
+ // which signals the caller to fall back to the generic image.
38
+ export const selectSeImage = (seImages, pgMajorVersion) => seImages === null || seImages === void 0 ? void 0 : seImages.find((image) => image.pg_major_version === String(pgMajorVersion));
39
+ // resolveDockerImagePath returns the concrete image reference Simple mode ships
40
+ // for a probed source. Managed providers fall back to the generic image in CE,
41
+ // so both the Apply payload and the preview show the same value.
42
+ export const resolveDockerImagePath = (providerKey, pgMajorVersion, dockerTag) => {
43
+ const mapping = providerKeyToImage(providerKey, pgMajorVersion);
44
+ const tag = dockerTag || mapping.defaultTag || '';
45
+ if (mapping.imageType === 'Generic Postgres') {
46
+ return tag ? `${genericImagePrefix}:${tag}` : '';
47
+ }
48
+ return genericImagePathForVersion(pgMajorVersion);
49
+ };
27
50
  // Resolves a probe provider key to a concrete docker image type the
28
51
  // Configuration form can write into the projection. Unknown keys (including
29
52
  // "azure", which has no matching SE image today) fall back to the generic
@@ -16,7 +16,7 @@ const useStyles = makeStyles({
16
16
  position: 'relative',
17
17
  cursor: 'default',
18
18
  flex: `0 0 ${CELL_SIZE}px`,
19
- background: '#f4f4f4',
19
+ background: 'rgba(128, 128, 128, 0.1)',
20
20
  height: `${CELL_SIZE}px`,
21
21
  display: 'flex',
22
22
  borderRadius: `${CELL_SIZE / 2}px`,
@@ -26,7 +26,7 @@ const useStyles = makeStyles({
26
26
  fontSize: '12px',
27
27
  },
28
28
  rootHasSnapshots: {
29
- background: colors.secondary2.lightLight,
29
+ background: 'rgba(15, 135, 157, 0.32)',
30
30
  cursor: 'pointer',
31
31
  },
32
32
  rootCurrent: {
@@ -41,7 +41,7 @@ const useStyles = makeStyles({
41
41
  right: '-6px',
42
42
  position: 'absolute',
43
43
  fontSize: '8px',
44
- backgroundColor: colors.white,
44
+ backgroundColor: 'inherit',
45
45
  border: `1px solid ${colors.secondary2.lightLight}`,
46
46
  borderRadius: '8px',
47
47
  height: '16px',
@@ -17,6 +17,9 @@ export declare const getCalendar: (monthStartDate: Date, snapshots: Snapshot[])
17
17
  logicalSize: number;
18
18
  message: string;
19
19
  branch: string;
20
+ protected: boolean;
21
+ protectedTill?: string | undefined;
22
+ deleteAt?: string | undefined;
20
23
  }[];
21
24
  isBreak: boolean;
22
25
  isDisabled: boolean;
@@ -16,6 +16,9 @@ export declare const getEdgeSnapshots: (snapshots: Snapshot[]) => {
16
16
  logicalSize: number;
17
17
  message: string;
18
18
  branch: string;
19
+ protected: boolean;
20
+ protectedTill?: string | undefined;
21
+ deleteAt?: string | undefined;
19
22
  };
20
23
  lastSnapshot: {
21
24
  createdAtDate: Date;
@@ -30,5 +33,8 @@ export declare const getEdgeSnapshots: (snapshots: Snapshot[]) => {
30
33
  logicalSize: number;
31
34
  message: string;
32
35
  branch: string;
36
+ protected: boolean;
37
+ protectedTill?: string | undefined;
38
+ deleteAt?: string | undefined;
33
39
  };
34
40
  };
@@ -41,6 +41,16 @@ const useStyles = makeStyles({
41
41
  header: {
42
42
  fontWeight: 500,
43
43
  },
44
+ protectedBadge: {
45
+ marginLeft: '8px',
46
+ padding: '1px 6px',
47
+ fontSize: '11px',
48
+ fontWeight: 400,
49
+ color: '#1a7f37',
50
+ border: '1px solid #1a7f37',
51
+ borderRadius: '4px',
52
+ whiteSpace: 'nowrap',
53
+ },
44
54
  infoContent: {
45
55
  fontSize: '12px',
46
56
  color: '#808080',
@@ -113,7 +123,7 @@ const SnapshotListItem = ({ snapshot, setSnapshotModal, openClonesModal, }) => {
113
123
  const timeAgo = formatDistanceSafe(snapshot.createdAtDate);
114
124
  const history = useHistory();
115
125
  const host = useHost();
116
- return (_jsx("div", { className: classes.commitItem, children: _jsxs("div", { className: classes.gridContainer, children: [_jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: snapshot.message || '-' }), _jsx("div", { className: classes.infoContent, title: snapshot.dataStateAt, children: timeAgo })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Pool" }), _jsx("div", { className: classes.infoContent, children: (_a = snapshot.pool) !== null && _a !== void 0 ? _a : '-' })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Number of clones" }), _jsx("div", { className: classes.infoContent, children: (_b = snapshot.numClones) !== null && _b !== void 0 ? _b : '-' })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Logical Size" }), _jsx("div", { className: classes.infoContent, children: snapshot.logicalSize ? formatBytesIEC(snapshot.logicalSize) : '-' })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Physical Size" }), _jsx("div", { className: classes.infoContent, children: snapshot.physicalSize
126
+ return (_jsx("div", { className: classes.commitItem, children: _jsxs("div", { className: classes.gridContainer, children: [_jsxs("div", { className: classes.infoBlock, children: [_jsxs("div", { className: classes.header, children: [snapshot.message || '-', snapshot.protected && (_jsx("span", { className: classes.protectedBadge, title: "Protected from deletion", children: "Protected" }))] }), _jsx("div", { className: classes.infoContent, title: snapshot.dataStateAt, children: timeAgo })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Pool" }), _jsx("div", { className: classes.infoContent, children: (_a = snapshot.pool) !== null && _a !== void 0 ? _a : '-' })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Number of clones" }), _jsx("div", { className: classes.infoContent, children: (_b = snapshot.numClones) !== null && _b !== void 0 ? _b : '-' })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Logical Size" }), _jsx("div", { className: classes.infoContent, children: snapshot.logicalSize ? formatBytesIEC(snapshot.logicalSize) : '-' })] }), _jsxs("div", { className: classes.infoBlock, children: [_jsx("div", { className: classes.header, children: "Physical Size" }), _jsx("div", { className: classes.infoContent, children: snapshot.physicalSize
117
127
  ? formatBytesIEC(snapshot.physicalSize)
118
128
  : '-' })] }), _jsxs("div", { className: classes.actionsContainer, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { className: classes.snapshotId, children: snapshot.id }), _jsx("div", { className: classes.copyButtonContainer, title: "Copy snapshot ID", children: _jsx(IconButton, { className: classes.copyButton, onClick: (e) => {
119
129
  e.stopPropagation();
@@ -37,6 +37,9 @@ const useStyles = makeStyles({
37
37
  transform: 'translate(-50%, -50%)',
38
38
  },
39
39
  }, { index: 1 });
40
+ // listRefreshIntervalMs is how often the snapshot list is silently refreshed so background
41
+ // auto-deletion does not leave stale rows in the list.
42
+ const listRefreshIntervalMs = 30000;
40
43
  export const Snapshots = observer(({ instanceId }) => {
41
44
  var _a;
42
45
  const host = useHost();
@@ -80,9 +83,14 @@ export const Snapshots = observer(({ instanceId }) => {
80
83
  fetchInitialData();
81
84
  }, []);
82
85
  useEffect(() => {
83
- if (selectedBranch) {
84
- stores.main.reloadSnapshots(selectedBranch === 'All branches' ? '' : selectedBranch);
85
- }
86
+ if (!selectedBranch)
87
+ return;
88
+ const branchName = selectedBranch === 'All branches' ? '' : selectedBranch;
89
+ stores.main.reloadSnapshots(branchName);
90
+ const intervalId = setInterval(() => {
91
+ stores.main.reloadSnapshots(branchName, true);
92
+ }, listRefreshIntervalMs);
93
+ return () => clearInterval(intervalId);
86
94
  }, [selectedBranch]);
87
95
  if (!instance && !snapshots.isLoading)
88
96
  return _jsx(_Fragment, {});
@@ -13,4 +13,7 @@ export declare const groupSnapshotsByCreatedAtDate: (snapshots: Snapshot[]) => {
13
13
  logicalSize: number;
14
14
  message: string;
15
15
  branch: string;
16
+ protected: boolean;
17
+ protectedTill?: string | undefined;
18
+ deleteAt?: string | undefined;
16
19
  }[][];
@@ -1,6 +1,5 @@
1
1
  import { makeStyles } from '@material-ui/core';
2
- import { colors } from '@postgres.ai/shared/styles/colors';
3
- export const useTabsStyles = makeStyles({
2
+ export const useTabsStyles = makeStyles((theme) => ({
4
3
  tabsRoot: {
5
4
  minHeight: 0,
6
5
  marginTop: '-8px',
@@ -21,7 +20,7 @@ export const useTabsStyles = makeStyles({
21
20
  height: '18px',
22
21
  },
23
22
  '& a': {
24
- color: colors.black,
23
+ color: theme.palette.text.primary,
25
24
  textDecoration: 'none',
26
25
  '@media (max-width: 700px)': {
27
26
  display: 'flex',
@@ -39,18 +38,19 @@ export const useTabsStyles = makeStyles({
39
38
  height: '3px',
40
39
  },
41
40
  tabRoot: {
41
+ color: theme.palette.text.primary,
42
42
  fontWeight: 400,
43
43
  minWidth: 0,
44
44
  minHeight: 0,
45
45
  width: '100%',
46
46
  padding: '6px 16px',
47
- borderBottom: `3px solid ${colors.consoleStroke}`,
47
+ borderBottom: `3px solid ${theme.palette.divider}`,
48
48
  '& + $tabRoot': {
49
49
  marginLeft: '10px',
50
50
  },
51
51
  '&.Mui-disabled': {
52
52
  opacity: 1,
53
- color: colors.pgaiDarkGray,
53
+ color: theme.palette.text.disabled,
54
54
  },
55
55
  '@media (max-width: 700px)': {
56
56
  width: 'max-content',
@@ -59,4 +59,4 @@ export const useTabsStyles = makeStyles({
59
59
  tabHidden: {
60
60
  display: 'none',
61
61
  },
62
- }, { index: 1 });
62
+ }), { index: 1 });
@@ -80,7 +80,7 @@ export declare class MainStore {
80
80
  get isDisabledInstance(): boolean;
81
81
  load: (instanceId: string, isPlatform?: boolean) => void;
82
82
  reload: (instanceId: string) => void;
83
- reloadSnapshots: (branchName?: string) => Promise<void>;
83
+ reloadSnapshots: (branchName?: string, silent?: boolean) => Promise<void>;
84
84
  reloadInstanceRetrieval: () => Promise<void>;
85
85
  private loadInstanceRetrieval;
86
86
  private loadInstance;
@@ -151,7 +151,7 @@ export declare class MainStore {
151
151
  destroyClone: (cloneId: string) => Promise<boolean | undefined>;
152
152
  private liveUpdateInstance;
153
153
  reloadClones: () => Promise<void>;
154
- getBranches: (instanceId: string) => Promise<import("@postgres.ai/shared/types/api/endpoints/getBranches").Branch[] | null | undefined>;
154
+ getBranches: (instanceId: string, silent?: boolean) => Promise<import("@postgres.ai/shared/types/api/endpoints/getBranches").Branch[] | null | undefined>;
155
155
  deleteBranch: (branchName: string, instanceId: string) => Promise<{
156
156
  response: Response | null;
157
157
  error: globalThis.Error | null;
@@ -80,10 +80,10 @@ export class MainStore {
80
80
  }
81
81
  });
82
82
  };
83
- this.reloadSnapshots = async (branchName) => {
83
+ this.reloadSnapshots = async (branchName, silent) => {
84
84
  if (!this.instance)
85
85
  return;
86
- await this.snapshots.reload(this.instance.id, branchName);
86
+ await this.snapshots.reload(this.instance.id, branchName, silent);
87
87
  };
88
88
  this.reloadInstanceRetrieval = async () => {
89
89
  if (!this.instance)
@@ -164,15 +164,17 @@ export class MainStore {
164
164
  return response;
165
165
  };
166
166
  this.getFullConfig = async (instanceId) => {
167
- var _a, _b, _c, _d, _e;
167
+ var _a, _b, _c, _d, _e, _f;
168
168
  if (!this.api.getFullConfig)
169
169
  return;
170
170
  const { response, error } = await this.api.getFullConfig(instanceId);
171
171
  if (response) {
172
172
  this.fullConfig = response;
173
173
  const splitYML = this.fullConfig.split('---');
174
- this.platformUrl = (_b = (_a = splitYML[0]) === null || _a === void 0 ? void 0 : _a.split('url: ')[1]) === null || _b === void 0 ? void 0 : _b.split('\n')[0];
175
- this.uiVersion = (_e = (_d = (_c = splitYML[0]) === null || _c === void 0 ? void 0 : _c.split('dockerImage: "postgresai/ce-ui:')[2]) === null || _d === void 0 ? void 0 : _d.split('\n')[0]) === null || _e === void 0 ? void 0 : _e.replace(/['"]+/g, '');
174
+ const platformSection = (_b = (_a = splitYML[0]) === null || _a === void 0 ? void 0 : _a.split('\nplatform:')[1]) !== null && _b !== void 0 ? _b : '';
175
+ const rawPlatformUrl = (_c = platformSection.split('url: ')[1]) === null || _c === void 0 ? void 0 : _c.split('\n')[0];
176
+ this.platformUrl = rawPlatformUrl === null || rawPlatformUrl === void 0 ? void 0 : rawPlatformUrl.replace(/['"]+/g, '').trim().split(/\s+/)[0];
177
+ this.uiVersion = (_f = (_e = (_d = splitYML[0]) === null || _d === void 0 ? void 0 : _d.split('dockerImage: "postgresai/ce-ui:')[2]) === null || _e === void 0 ? void 0 : _e.split('\n')[0]) === null || _f === void 0 ? void 0 : _f.replace(/['"]+/g, '');
176
178
  }
177
179
  if (error)
178
180
  this.getFullConfigError = await error
@@ -272,13 +274,19 @@ export class MainStore {
272
274
  await this.loadInstanceRetrieval(this.instance.id);
273
275
  this.isReloadingClones = false;
274
276
  };
275
- this.getBranches = async (instanceId) => {
277
+ this.getBranches = async (instanceId, silent) => {
276
278
  if (!this.api.getBranches)
277
279
  return;
278
- this.isBranchesLoading = true;
280
+ if (!silent)
281
+ this.isBranchesLoading = true;
279
282
  const { response, error } = await this.api.getBranches(instanceId);
280
- this.isBranchesLoading = false;
281
- if (error)
283
+ if (!silent)
284
+ this.isBranchesLoading = false;
285
+ if (response)
286
+ this.getBranchesError = null;
287
+ // a silent background refresh must not replace the rendered list with an error stub on a
288
+ // transient failure; errors surface only on a foreground load.
289
+ if (error && !silent)
282
290
  this.getBranchesError = await error.json().then((err) => err);
283
291
  return response;
284
292
  };