@@ -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 {
1319import { Layout as DashboardLayout } from "../../../layouts/index.js" ;
1420import CippPageCard from "../../../components/CippCards/CippPageCard" ;
1521import { ApiGetCall , ApiPostCall } from "../../../api/ApiCall" ;
22+ import { CippApiResults } from "../../../components/CippComponents/CippApiResults" ;
1623import { CippInfoBar } from "../../../components/CippCards/CippInfoBar" ;
1724import {
1825 ArrowCircleRight ,
@@ -30,10 +37,87 @@ import { CippDataTable } from "../../../components/CippTable/CippDataTable";
3037import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog" ;
3138import { CippRestoreWizard } from "../../../components/CippComponents/CippRestoreWizard" ;
3239import { BackupValidator } from "../../../utils/backupValidation" ;
33- import { useState } from "react" ;
40+ import { useEffect , useState } from "react" ;
3441import { 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+
36115const 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