Skip to content

Commit aae08c6

Browse files
Merge pull request #2058 from redpanda-data/front-end/dr/support-shadowing-sr
Front end/dr/support shadowing sr
2 parents f6635fb + a052d2c commit aae08c6

15 files changed

+288
-6
lines changed

frontend/src/components/pages/shadowlinks/create/configuration/acls-step.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const AclsStep = () => {
108108
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
109109
<Card className="gap-0" size="full">
110110
<CardHeader>
111-
<CardTitle>ACLs</CardTitle>
111+
<CardTitle>Shadow ACLs</CardTitle>
112112
<CardAction>
113113
<CollapsibleTrigger asChild>
114114
<Button className="w-fit p-0" data-testid="acls-toggle-button" size="sm" variant="ghost">

frontend/src/components/pages/shadowlinks/create/configuration/configuration-step.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111

1212
import { AclsStep } from './acls-step';
1313
import { ConsumerOffsetStep } from './consumer-offset-step';
14+
import { SchemaRegistryStep } from './schema-registry-step';
1415
import { TopicsStep } from './topics-step';
1516

1617
export const ConfigurationStep = () => (
1718
<div className="space-y-4">
1819
<TopicsStep />
1920
<AclsStep />
2021
<ConsumerOffsetStep />
22+
<SchemaRegistryStep />
2123
</div>
2224
);

frontend/src/components/pages/shadowlinks/create/configuration/consumer-offset-step.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const ConsumerOffsetStep = () => {
6363
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
6464
<Card className="gap-0" size="full">
6565
<CardHeader>
66-
<CardTitle>Consumer groups</CardTitle>
66+
<CardTitle>Shadow consumer groups</CardTitle>
6767
<CardAction>
6868
<CollapsibleTrigger asChild>
6969
<Button className="w-fit p-0" data-testid="consumers-toggle-button" size="sm" variant="ghost">
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright 2025 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { zodResolver } from '@hookform/resolvers/zod';
13+
import { Form } from 'components/redpanda-ui/components/form';
14+
import { useForm } from 'react-hook-form';
15+
import { fireEvent, render, screen, waitFor } from 'test-utils';
16+
17+
import { SchemaRegistryStep } from './schema-registry-step';
18+
import { FormSchema, type FormValues, initialValues } from '../model';
19+
20+
const TestWrapper = ({
21+
defaultValues = initialValues,
22+
onFormChange,
23+
}: {
24+
defaultValues?: FormValues;
25+
onFormChange?: (values: FormValues) => void;
26+
}) => {
27+
const form = useForm<FormValues>({
28+
resolver: zodResolver(FormSchema),
29+
defaultValues,
30+
});
31+
32+
if (onFormChange) {
33+
form.watch((values) => {
34+
onFormChange(values as FormValues);
35+
});
36+
}
37+
38+
return (
39+
<Form {...form}>
40+
<form>
41+
<SchemaRegistryStep />
42+
</form>
43+
</Form>
44+
);
45+
};
46+
47+
describe('SchemaRegistryStep', () => {
48+
describe('Toggle switch', () => {
49+
test('should toggle enableSchemaRegistrySync value when switch is clicked', async () => {
50+
let formValues: FormValues | undefined;
51+
52+
render(
53+
<TestWrapper
54+
onFormChange={(values) => {
55+
formValues = values;
56+
}}
57+
/>
58+
);
59+
60+
const switchElement = screen.getByTestId('sr-enable-switch');
61+
62+
expect(switchElement).toHaveAttribute('data-state', 'unchecked');
63+
64+
fireEvent.click(switchElement);
65+
66+
await waitFor(() => {
67+
expect(switchElement).toHaveAttribute('data-state', 'checked');
68+
expect(formValues?.enableSchemaRegistrySync).toBe(true);
69+
});
70+
71+
fireEvent.click(switchElement);
72+
73+
await waitFor(() => {
74+
expect(switchElement).toHaveAttribute('data-state', 'unchecked');
75+
expect(formValues?.enableSchemaRegistrySync).toBe(false);
76+
});
77+
});
78+
});
79+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright 2025 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with the Business Source License, use of this software will be governed
8+
* by the Apache License, Version 2.0
9+
*/
10+
11+
import { Card, CardContent, CardHeader, CardTitle } from 'components/redpanda-ui/components/card';
12+
import { FormControl, FormDescription, FormField, FormItem } from 'components/redpanda-ui/components/form';
13+
import { Switch } from 'components/redpanda-ui/components/switch';
14+
import { useFormContext } from 'react-hook-form';
15+
16+
import type { FormValues } from '../model';
17+
18+
export const SchemaRegistryStep = () => {
19+
const { control } = useFormContext<FormValues>();
20+
21+
return (
22+
<Card size="full">
23+
<CardHeader>
24+
<CardTitle>Shadow Schema Registry</CardTitle>
25+
</CardHeader>
26+
<CardContent>
27+
<FormField
28+
control={control}
29+
name="enableSchemaRegistrySync"
30+
render={({ field }) => (
31+
<FormItem className="flex items-center justify-between">
32+
<div className="space-y-1">
33+
<FormDescription>
34+
Replicate the source cluster's _schema topic, which replaces the shadow cluster's Schema Registry.
35+
</FormDescription>
36+
</div>
37+
<FormControl>
38+
<Switch checked={field.value} onCheckedChange={field.onChange} testId="sr-enable-switch" />
39+
</FormControl>
40+
</FormItem>
41+
)}
42+
/>
43+
</CardContent>
44+
</Card>
45+
);
46+
};

frontend/src/components/pages/shadowlinks/create/configuration/topics-step.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const TopicsStep = () => {
6363
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
6464
<Card className="gap-0" size="full">
6565
<CardHeader>
66-
<CardTitle>Topic shadowing</CardTitle>
66+
<CardTitle>Shadow topics</CardTitle>
6767
<CardAction>
6868
<CollapsibleTrigger asChild>
6969
<Button className="w-fit p-0" data-testid="topics-toggle-button" size="sm" variant="ghost">

frontend/src/components/pages/shadowlinks/create/model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ export const FormSchema = z
129129
})
130130
)
131131
.optional(),
132+
133+
// Schema Registry sync
134+
enableSchemaRegistrySync: z.boolean(),
132135
})
133136
.superRefine((data, ctx) => {
134137
// If SCRAM is enabled, username and password are required
@@ -213,4 +216,5 @@ export const initialValues: FormValues = {
213216
consumers: [],
214217
aclsMode: 'all',
215218
aclFilters: [], // No default filter - user adds as needed
219+
enableSchemaRegistrySync: false,
216220
};

frontend/src/components/pages/shadowlinks/create/shadowlink-create-page.test.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ type CreateAction =
9494
| { type: 'navigateToConfiguration' }
9595
| { type: 'addTopicFilterCreate'; name: string; options?: { patternType?: PatternType; filterType?: FilterType } }
9696
| { type: 'addConsumerFilterCreate'; name: string }
97-
| { type: 'addACLFilterCreate'; principal: string };
97+
| { type: 'addACLFilterCreate'; principal: string }
98+
| { type: 'enableSchemaRegistrySync' };
9899

99100
/**
100101
* Perform action for create form
@@ -217,6 +218,11 @@ const performCreateAction = async (
217218
case 'addACLFilterCreate':
218219
await addACLFilterCreate(user, scr, action.principal);
219220
break;
221+
case 'enableSchemaRegistrySync': {
222+
const schemaRegistrySwitch = scr.getByTestId('sr-enable-switch');
223+
await user.click(schemaRegistrySwitch);
224+
break;
225+
}
220226
default:
221227
throw new Error(`Unknown action type: ${JSON.stringify(action)}`);
222228
}
@@ -370,6 +376,24 @@ const testCases: CreateTestCase[] = [
370376
);
371377
},
372378
},
379+
{
380+
description: 'creates shadow link with schema registry sync enabled',
381+
actions: [
382+
{ type: 'fillName', value: 'test-shadow-link' },
383+
{ type: 'fillBootstrapServer', index: 0, value: 'server1.example.com:9092' },
384+
{ type: 'fillScramUsername', value: 'admin' },
385+
{ type: 'fillScramPassword', value: 'admin-secret' },
386+
{ type: 'navigateToConfiguration' },
387+
{ type: 'enableSchemaRegistrySync' },
388+
],
389+
verify: (createRequest, exp) => {
390+
exp(createRequest.shadowLink.name).toBe('test-shadow-link');
391+
exp(createRequest.shadowLink.configurations.schemaRegistrySyncOptions).toBeDefined();
392+
exp(createRequest.shadowLink.configurations.schemaRegistrySyncOptions.schemaRegistryShadowingMode.case).toBe(
393+
'shadowSchemaRegistryTopic'
394+
);
395+
},
396+
},
373397
];
374398

375399
describe('ShadowLinkCreatePage', () => {

frontend/src/components/pages/shadowlinks/create/shadowlink-create-page.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
FilterType,
2626
NameFilterSchema,
2727
PatternType,
28+
SchemaRegistrySyncOptions_ShadowSchemaRegistryTopicSchema,
29+
SchemaRegistrySyncOptionsSchema,
2830
ScramConfigSchema,
2931
SecuritySettingsSyncOptionsSchema,
3032
ShadowLinkClientOptionsSchema,
@@ -185,12 +187,23 @@ const buildCreateShadowLinkRequest = (values: FormValues) => {
185187
),
186188
});
187189

190+
// Build schema registry sync options (only set if enabled)
191+
const schemaRegistrySyncOptions = values.enableSchemaRegistrySync
192+
? create(SchemaRegistrySyncOptionsSchema, {
193+
schemaRegistryShadowingMode: {
194+
case: 'shadowSchemaRegistryTopic',
195+
value: create(SchemaRegistrySyncOptions_ShadowSchemaRegistryTopicSchema, {}),
196+
},
197+
})
198+
: undefined;
199+
188200
// Build configurations
189201
const configurations = create(ShadowLinkConfigurationsSchema, {
190202
clientOptions,
191203
topicMetadataSyncOptions,
192204
consumerOffsetSyncOptions,
193205
securitySyncOptions,
206+
schemaRegistrySyncOptions,
194207
});
195208

196209
// Build shadow link
@@ -250,7 +263,8 @@ export const ShadowLinkCreatePage = () => {
250263
<div className="space-y-2">
251264
<Heading level={1}>Create shadow link</Heading>
252265
<Text variant="muted">
253-
Set up a shadow link to replicate topics from a source cluster for disaster recovery.
266+
Shadowing copies data at the byte level, ensuring shadow topics contain identical copies of source topics with
267+
preserved offsets and timestamps. Select the replicated content for this shadow link.
254268
</Text>
255269
</div>
256270

frontend/src/components/pages/shadowlinks/details/config/configuration-shadowing.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,38 @@ const ACLFilterSection = ({ filters }: { filters: ACLFilter[] }) => {
166166
);
167167
};
168168

169+
// Component to display Schema Registry sync status
170+
const SchemaRegistrySection = ({ isEnabled }: { isEnabled: boolean }) => (
171+
<Card size="full" testId="schema-registry-card">
172+
<CardHeader>
173+
<Heading level={3}>Schema Registry</Heading>
174+
</CardHeader>
175+
<CardContent className="flex flex-row justify-between">
176+
<Text className="mt-2 text-muted-foreground text-sm">
177+
Replicate the source cluster's _schema topic, which replaces the shadow cluster's Schema Registry.
178+
</Text>
179+
<Badge testId="schema-registry-status-badge" variant={isEnabled ? 'green' : 'gray'}>
180+
{isEnabled ? 'Enabled' : 'Disabled'}
181+
</Badge>
182+
</CardContent>
183+
</Card>
184+
);
185+
169186
export const ConfigurationShadowing = ({ shadowLink }: ConfigurationShadowingProps) => {
170187
const topicSyncOptions = shadowLink.configurations?.topicMetadataSyncOptions;
171188
const consumerSyncOptions = shadowLink.configurations?.consumerOffsetSyncOptions;
172189
const securitySyncOptions = shadowLink.configurations?.securitySyncOptions;
190+
const schemaRegistrySyncOptions = shadowLink.configurations?.schemaRegistrySyncOptions;
173191

174192
// Get filters
175193
const topicFilters = topicSyncOptions?.autoCreateShadowTopicFilters || [];
176194
const consumerFilters = consumerSyncOptions?.groupFilters || [];
177195
const aclFilters = securitySyncOptions?.aclFilters || [];
178196

197+
// Check if schema registry sync is enabled
198+
const isSchemaRegistrySyncEnabled =
199+
schemaRegistrySyncOptions?.schemaRegistryShadowingMode?.case === 'shadowSchemaRegistryTopic';
200+
179201
return (
180202
<div className="flex flex-col gap-6">
181203
<Heading level={2} testId="shadowing-title">
@@ -200,6 +222,9 @@ export const ConfigurationShadowing = ({ shadowLink }: ConfigurationShadowingPro
200222
testId="consumer-group-replication"
201223
title="Consumer group replication"
202224
/>
225+
226+
{/* Schema Registry Section */}
227+
<SchemaRegistrySection isEnabled={isSchemaRegistrySyncEnabled} />
203228
</div>
204229
);
205230
};

0 commit comments

Comments
 (0)