Skip to content

Commit 9d85d14

Browse files
committed
External Storage Account Backup Replication
1 parent e8daa7f commit 9d85d14

1 file changed

Lines changed: 122 additions & 1 deletion

File tree

src/pages/cipp/settings/backup.js

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import {
22
Alert,
33
Box,
44
Button,
5+
Card,
56
CardContent,
7+
CardHeader,
8+
Divider,
9+
FormControlLabel,
610
Stack,
11+
Switch,
12+
TextField,
713
Typography,
814
Skeleton,
915
Input,
@@ -13,6 +19,7 @@ import {
1319
import { Layout as DashboardLayout } from "../../../layouts/index.js";
1420
import CippPageCard from "../../../components/CippCards/CippPageCard";
1521
import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall";
22+
import { CippApiResults } from "../../../components/CippComponents/CippApiResults";
1623
import { CippInfoBar } from "../../../components/CippCards/CippInfoBar";
1724
import {
1825
ArrowCircleRight,
@@ -30,10 +37,87 @@ import { CippDataTable } from "../../../components/CippTable/CippDataTable";
3037
import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog";
3138
import { CippRestoreWizard } from "../../../components/CippComponents/CippRestoreWizard";
3239
import { BackupValidator } from "../../../utils/backupValidation";
33-
import { useState } from "react";
40+
import { useEffect, useState } from "react";
3441
import { useDialog } from "../../../hooks/use-dialog";
3542

43+
const ReplicationScopeCard = ({ scope, label, description, config }) => {
44+
const save = ApiPostCall({ relatedQueryKeys: "BackupReplicationConfig" });
45+
const [sasUrl, setSasUrl] = useState("");
46+
const [enabled, setEnabled] = useState(false);
47+
48+
useEffect(() => {
49+
setEnabled(Boolean(config?.Enabled));
50+
setSasUrl("");
51+
}, [config?.Enabled, config?.IsSet]);
52+
53+
const isSet = Boolean(config?.IsSet);
54+
55+
const handleSave = () => {
56+
const data = { BackupType: scope, Enabled: enabled };
57+
if (sasUrl) {
58+
data.SASUrl = sasUrl;
59+
} else if (isSet) {
60+
// Keep the existing stored SAS URL untouched.
61+
data.SASUrl = "SentToKeyVault";
62+
}
63+
save.mutate({ url: "/api/ExecBackupReplicationConfig", data });
64+
};
65+
66+
return (
67+
<Card variant="outlined">
68+
<CardContent>
69+
<Stack spacing={2}>
70+
<Box>
71+
<Typography variant="subtitle2">{label}</Typography>
72+
<Typography variant="caption" color="text.secondary">
73+
{description}
74+
</Typography>
75+
</Box>
76+
<FormControlLabel
77+
control={
78+
<Switch
79+
size="small"
80+
checked={enabled}
81+
onChange={(e) => setEnabled(e.target.checked)}
82+
/>
83+
}
84+
label={<Typography variant="body2">Enable replication</Typography>}
85+
/>
86+
<TextField
87+
label="Container SAS URL"
88+
value={sasUrl}
89+
onChange={(e) => setSasUrl(e.target.value)}
90+
size="small"
91+
fullWidth
92+
type="password"
93+
placeholder={
94+
isSet ? "•••••••••• (stored — leave blank to keep)" : "https://account.blob.core.windows.net/container?sv=..."
95+
}
96+
helperText="Container-level SAS URL with write and create permissions."
97+
/>
98+
<Box>
99+
<Button
100+
variant="contained"
101+
color="primary"
102+
onClick={handleSave}
103+
disabled={save.isPending}
104+
>
105+
{save.isPending ? "Saving..." : "Save"}
106+
</Button>
107+
</Box>
108+
<CippApiResults apiObject={save} />
109+
</Stack>
110+
</CardContent>
111+
</Card>
112+
);
113+
};
114+
36115
const Page = () => {
116+
const replicationConfig = ApiGetCall({
117+
url: "/api/ExecBackupReplicationConfig",
118+
data: { List: true },
119+
queryKey: "BackupReplicationConfig",
120+
});
37121
const [validationResult, setValidationResult] = useState(null);
38122
const wizardDialog = useDialog();
39123
const runBackupDialog = useDialog();
@@ -317,6 +401,43 @@ const Page = () => {
317401
</CardContent>
318402
</CippPageCard>
319403

404+
<CippPageCard title="Backup Replication">
405+
<Box sx={{ mt: 3 }}>
406+
<Card>
407+
<CardHeader
408+
title="External Replication"
409+
subheader="Replicate new backups to an external Azure Storage account."
410+
/>
411+
<Divider />
412+
<CardContent>
413+
<Alert severity="info" sx={{ mb: 2 }}>
414+
When enabled, each new backup is also uploaded to the external container described by the
415+
SAS URL. The SAS URL is stored securely in Key Vault. This does not copy existing backups.
416+
This will continue to push backups to the container without any consideration for storage costs, so please monitor your external storage usage.
417+
</Alert>
418+
<Stack spacing={2} direction={{ xs: "column", md: "row" }}>
419+
<Box sx={{ flex: 1 }}>
420+
<ReplicationScopeCard
421+
scope="Core"
422+
label="CIPP Core Backups"
423+
description="Replicates CIPP configuration (Core) backups."
424+
config={replicationConfig.data?.Results?.Core}
425+
/>
426+
</Box>
427+
<Box sx={{ flex: 1 }}>
428+
<ReplicationScopeCard
429+
scope="Tenant"
430+
label="Tenant Backups"
431+
description="Replicates all scheduled tenant backups."
432+
config={replicationConfig.data?.Results?.Tenant}
433+
/>
434+
</Box>
435+
</Stack>
436+
</CardContent>
437+
</Card>
438+
</Box>
439+
</CippPageCard>
440+
320441
<CippApiDialog
321442
createDialog={runBackupDialog}
322443
title="Run Backup"

0 commit comments

Comments
 (0)