Skip to content

Commit 2389de8

Browse files
authored
fix(topics): clamp default replication factor to broker count (#2420)
* fix(topics): clamp default replication factor to broker count CreateTopic defaulted replicationFactor to 3 in both the topic-create modal and the MCP inspector's "create new topic" flow. On single-broker clusters (local-byoc dev environments, etc.) that rejected the request with "not enough replicas". Clamp the default to min(3, brokersOnline) and fall back to 3 while KafkaInfo is loading. * fix(rp-connect): clamp onboarding topic RF to broker count The RPCN onboarding Add Topic step spread TOPIC_FORM_DEFAULTS (replicationFactor: 3) into the form and renders the RF field as readOnly in AdvancedTopicSettings. On single-broker clusters (local-byoc dev environments) that meant the user saw RF=3 with no way to edit, and CreateTopic failed with "not enough replicas". Override replicationFactor at form init with min(default, brokersOnline) via useGetKafkaInfoQuery so the readOnly value matches what the broker can actually satisfy. * revert(mcp): drop RF clamp from MCP inspector Scope back to the user-visible topic-create paths only. The MCP inspector's "create new topic" flow is out of scope for this PR. * fix(rp-connect): react to late-arriving KafkaInfo via rhf `values` useForm captures defaultValues at mount; when the KafkaInfo query resolves after mount the RF field stays at its initial 3. Pass defaultValues through both `defaultValues` and `values` with `resetOptions: { keepDirtyValues: true }` so the form reactively picks up the clamped replicationFactor once brokersOnline is known, without clobbering fields the user has already edited. * fix(topics): isolate keepDirtyValues to RPCN form watch only Address PR review feedback on #2420. The form-level `resetOptions: { keepDirtyValues: true }` (added so dirty fields survive the late `kafkaInfo` re-init) was leaking into the existing-topic-selected reset, where dirty fields should be overwritten by the selected topic's actual config. Also tighten the RF clamp comment in `create-topic-modal.tsx` to cover the all-brokers-offline case, not just the loading case. Refs UX-1208.
1 parent 357c64a commit 2389de8

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

frontend/src/components/pages/rp-connect/onboarding/add-topic-step.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ListTopicsRequestSchema } from 'protogen/redpanda/api/dataplane/v1/topi
2525
import { listTopics } from 'protogen/redpanda/api/dataplane/v1/topic-TopicService_connectquery';
2626
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react';
2727
import { useForm, useWatch } from 'react-hook-form';
28+
import { useGetKafkaInfoQuery } from 'react-query/api/cluster-status';
2829
import { useLegacyListTopicsQuery } from 'react-query/api/topic';
2930
import { LONG_LIVED_CACHE_STALE_TIME } from 'react-query/react-query.utils';
3031
import { isFalsy } from 'utils/falsy';
@@ -104,18 +105,36 @@ export const AddTopicStep = forwardRef<BaseStepRef<AddTopicFormData>, AddTopicSt
104105

105106
const isPending = createTopicMutation.isPending;
106107

108+
// The RF field is readOnly in advanced-topic-settings, so the default is
109+
// also the final value. Clamp to broker count so single-broker clusters
110+
// (e.g. local-byoc) don't hit "not enough replicas" on CreateTopic.
111+
const { data: kafkaInfo } = useGetKafkaInfoQuery();
112+
const brokersOnline = kafkaInfo?.brokersOnline ?? 0;
113+
const defaultReplicationFactor =
114+
brokersOnline > 0
115+
? Math.min(TOPIC_FORM_DEFAULTS.replicationFactor, brokersOnline)
116+
: TOPIC_FORM_DEFAULTS.replicationFactor;
117+
107118
const defaultValues = useMemo(
108119
() => ({
109120
...TOPIC_FORM_DEFAULTS,
121+
replicationFactor: defaultReplicationFactor,
110122
topicName: defaultTopicName || TOPIC_FORM_DEFAULTS.topicName,
111123
}),
112-
[defaultTopicName]
124+
[defaultTopicName, defaultReplicationFactor]
113125
);
114126

127+
// Pass defaultValues AND values so react-hook-form reactively updates
128+
// replicationFactor when the KafkaInfo query resolves after mount. The
129+
// `values` prop is rhf's built-in mechanism for external reactive state;
130+
// `keepDirtyValues` prevents fields the user has already touched from
131+
// being overwritten when kafkaInfo lands late.
115132
const form = useForm<AddTopicFormData>({
116133
resolver: zodResolver(addTopicFormSchema),
117134
mode: 'onChange',
118135
defaultValues,
136+
values: defaultValues,
137+
resetOptions: { keepDirtyValues: true },
119138
});
120139

121140
const watchedTopicName = useWatch({
@@ -147,7 +166,10 @@ export const AddTopicStep = forwardRef<BaseStepRef<AddTopicFormData>, AddTopicSt
147166
}
148167
if (topicConfig && !topicConfig.error) {
149168
const allTopicValues = parseTopicConfigFromExisting(existingTopicSelected, topicConfig);
150-
form.reset(allTopicValues, { keepDefaultValues: false });
169+
// Override the form-level `keepDirtyValues: true` default — when a user
170+
// selects an existing topic, its config must fully replace any partial
171+
// input they've made.
172+
form.reset(allTopicValues, { keepDefaultValues: false, keepDirtyValues: false });
151173
} else {
152174
form.setValue('topicName', existingTopicSelected.topicName, {
153175
shouldDirty: false,

frontend/src/components/pages/topics/create-topic-modal.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
CreateTopicRequestSchema,
2121
ListTopicsRequestSchema,
2222
} from 'protogen/redpanda/api/dataplane/v1/topic_pb';
23+
import { useGetKafkaInfoQuery } from 'react-query/api/cluster-status';
2324
import { useCreateTopicMutation, useLegacyListTopicsQuery } from 'react-query/api/topic';
2425
import { z } from 'zod';
2526

@@ -44,6 +45,14 @@ export const CreateTopicModal = ({ isOpen, onClose }: CreateTopicModalProps) =>
4445
hideInternalTopics: true,
4546
});
4647
const { mutateAsync: createTopic, isPending: isCreateTopicPending } = useCreateTopicMutation();
48+
// Clamp RF to broker count so single-broker clusters (e.g. local-byoc) don't
49+
// fail CreateTopic with "not enough replicas". When `brokersOnline` is 0
50+
// (KafkaInfo still loading, or every broker is offline) keep the default —
51+
// a degraded cluster will reject CreateTopic regardless of what we send.
52+
const { data: kafkaInfo } = useGetKafkaInfoQuery();
53+
const brokersOnline = kafkaInfo?.brokersOnline ?? 0;
54+
const replicationFactor =
55+
brokersOnline > 0 ? Math.min(DEFAULT_TOPIC_REPLICATION_FACTOR, brokersOnline) : DEFAULT_TOPIC_REPLICATION_FACTOR;
4756

4857
const formOpts = formOptions({
4958
defaultValues: {
@@ -57,7 +66,7 @@ export const CreateTopicModal = ({ isOpen, onClose }: CreateTopicModalProps) =>
5766
topic: create(CreateTopicRequest_TopicSchema, {
5867
name: value.name,
5968
partitionCount: DEFAULT_TOPIC_PARTITION_COUNT,
60-
replicationFactor: DEFAULT_TOPIC_REPLICATION_FACTOR,
69+
replicationFactor,
6170
configs: [
6271
create(CreateTopicRequest_Topic_ConfigSchema, {
6372
name: 'cleanup.policy',

0 commit comments

Comments
 (0)