@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.
- package/components/UpgradeCloneModal/index.d.ts +11 -0
- package/components/UpgradeCloneModal/index.js +75 -0
- package/package.json +1 -1
- package/pages/Branches/Branch/index.js +27 -2
- package/pages/Branches/Branch/stores/Main.d.ts +5 -0
- package/pages/Branches/Branch/stores/Main.js +29 -0
- package/pages/Branches/components/BranchesTable/index.js +2 -2
- package/pages/Branches/index.js +10 -1
- package/pages/Clone/Status/index.js +3 -0
- package/pages/Clone/index.js +12 -4
- package/pages/Clone/stores/Main.d.ts +9 -1
- package/pages/Clone/stores/Main.js +40 -3
- package/pages/CreateBranch/stores/Main.d.ts +3 -0
- package/pages/CreateClone/index.js +2 -0
- package/pages/CreateClone/stores/Main.d.ts +5 -1
- package/pages/CreateClone/stores/Main.js +8 -3
- package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.d.ts +2 -0
- package/pages/Instance/Configuration/PhysicalMode/EnvsEditor/index.js +8 -1
- package/pages/Instance/Configuration/SimpleMode/PreviewCard.d.ts +3 -1
- package/pages/Instance/Configuration/SimpleMode/PreviewCard.js +7 -7
- package/pages/Instance/Configuration/SimpleMode/index.d.ts +6 -1
- package/pages/Instance/Configuration/SimpleMode/index.js +81 -22
- package/pages/Instance/Configuration/configOptions.d.ts +10 -0
- package/pages/Instance/Configuration/configOptions.js +24 -1
- package/pages/Instance/Info/Snapshots/Calendar/Day/index.js +3 -3
- package/pages/Instance/Info/Snapshots/Calendar/utils.d.ts +3 -0
- package/pages/Instance/Info/Snapshots/utils.d.ts +6 -0
- package/pages/Instance/Snapshots/components/SnapshotsList/index.js +11 -1
- package/pages/Instance/Snapshots/index.js +11 -3
- package/pages/Instance/Snapshots/utils/index.d.ts +3 -0
- package/pages/Instance/Tabs/styles.js +6 -6
- package/pages/Instance/stores/Main.d.ts +2 -2
- package/pages/Instance/stores/Main.js +17 -9
- package/pages/Snapshots/Snapshot/index.js +28 -2
- package/pages/Snapshots/Snapshot/stores/Main.d.ts +5 -0
- package/pages/Snapshots/Snapshot/stores/Main.js +29 -0
- package/stores/Snapshots.d.ts +1 -1
- package/stores/Snapshots.js +12 -6
- package/types/api/endpoints/getBranches.d.ts +6 -0
- package/types/api/endpoints/testDbSource.d.ts +3 -1
- package/types/api/endpoints/testDbSource.js +6 -0
- package/types/api/endpoints/updateBranch.d.ts +11 -0
- package/types/api/endpoints/updateBranch.js +1 -0
- package/types/api/endpoints/updateSnapshot.d.ts +11 -0
- package/types/api/endpoints/updateSnapshot.js +1 -0
- package/types/api/endpoints/upgradeClone.d.ts +8 -0
- package/types/api/endpoints/upgradeClone.js +1 -0
- package/types/api/entities/branchSnapshots.d.ts +3 -0
- package/types/api/entities/clone.d.ts +9 -2
- package/types/api/entities/instance.d.ts +11 -1
- package/types/api/entities/instanceState.d.ts +16 -1
- package/types/api/entities/snapshot.d.ts +6 -0
- package/utils/api.d.ts +1 -1
- package/utils/api.js +6 -0
- package/utils/clone.d.ts +3 -1
- package/utils/clone.js +7 -1
- package/utils/clonePoller.d.ts +12 -0
- package/utils/clonePoller.js +41 -0
- package/utils/date.js +12 -12
- package/utils/protection.d.ts +5 -0
- 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
|
-
|
|
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: {
|
|
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
|
|
12
|
-
const tuningEntries = Object.entries((
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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: '
|
|
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:
|
|
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:
|
|
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: [
|
|
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
|
-
|
|
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, {});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { makeStyles } from '@material-ui/core';
|
|
2
|
-
|
|
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:
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
280
|
+
if (!silent)
|
|
281
|
+
this.isBranchesLoading = true;
|
|
279
282
|
const { response, error } = await this.api.getBranches(instanceId);
|
|
280
|
-
|
|
281
|
-
|
|
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
|
};
|