Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions frontend/src/components/pages/admin/admin-debug-bundle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type SCRAMAuth,
SCRAMAuth_Mechanism,
} from '../../../protogen/redpanda/api/console/v1alpha1/debug_bundle_pb';
import queryClient from '../../../query-client';
import { appGlobal } from '../../../state/app-global';
import { api, useApiStoreHook } from '../../../state/backend-api';
import type { BrokerWithConfigAndStorage } from '../../../state/rest-interfaces';
Expand Down Expand Up @@ -216,9 +217,7 @@ const NewDebugBundleForm: FC<{

useEffect(() => {
api.refreshBrokers(true);
api.refreshPartitions('all', true).catch(() => {
// Error handling managed by API layer
});
queryClient.invalidateQueries({ queryKey: ['topicPartitionsAll'] });

@malinskibeniamin malinskibeniamin Mar 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can rely on a useQueryClient hook to grab the singleton instance too.

}, []);

const fieldViolationsMap = error?.details
Expand Down
19 changes: 9 additions & 10 deletions frontend/src/components/pages/connect/connector-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import React, { useEffect, useRef, useState } from 'react';

import { ConfigPage } from './dynamic-ui/components';
import { useTopicsQuery } from '../../../react-query/api/topic';
import { appGlobal } from '../../../state/app-global';
import { api, createMessageSearch, type MessageSearch, type MessageSearchRequest } from '../../../state/backend-api';
import { ConnectClusterStore } from '../../../state/connect/state';
Expand Down Expand Up @@ -87,7 +88,8 @@ const KafkaConnectorMain = ({
}) => {
const [connectClusterStore] = useState(() => ConnectClusterStore.getInstance(clusterName));

const logsTopic = api.topics?.first((x) => x.topicName === LOGS_TOPIC_NAME);
const { data: topicsData } = useTopicsQuery();
const logsTopic = topicsData?.topics?.first((x) => x.topicName === LOGS_TOPIC_NAME);

useEffect(() => {
const init = async () => {
Expand Down Expand Up @@ -478,7 +480,8 @@ const ConnectorErrorModal = (p: { error: ConnectorError }) => {

const errorType = p.error.type === 'ERROR' ? 'error' : 'warning';

const hasConnectorLogs = api.topics?.any((x) => x.topicName === LOGS_TOPIC_NAME);
const { data: connectorTopicsData } = useTopicsQuery();
const hasConnectorLogs = connectorTopicsData?.topics?.any((x) => x.topicName === LOGS_TOPIC_NAME);

return (
<>
Expand Down Expand Up @@ -536,13 +539,11 @@ class KafkaConnectorDetails extends PageComponent<{ clusterName: string; connect
appGlobal.onRefresh = () => this.refreshData(true).catch(console.error);
}

async refreshData(force: boolean): Promise<void> {
async refreshData(_force: boolean): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't use the _force, should we drop it

ConnectClusterStore.connectClusters.clear();
await api.refreshConnectClusters();

// refresh topics so we know whether or not we can show the "go to error logs topic" button in the connector details error popup
// and show the logs tab
api.refreshTopics(force);
// React Query handles topics fetching via useTopicsQuery hooks in child components
}

render() {
Expand All @@ -553,9 +554,6 @@ class KafkaConnectorDetails extends PageComponent<{ clusterName: string; connect
return <NotConfigured />;
}

// Touch observables so PageComponent's Reaction tracks them for re-renders.
void api.topics;

return (
<PageContent>
<KafkaConnectorMain clusterName={clusterName} connectorName={connectorName} refreshData={this.refreshData} />
Expand Down Expand Up @@ -658,7 +656,8 @@ const LogsTab = (p: {
const { connector } = p;
const connectorName = connector.name;
const topicName = LOGS_TOPIC_NAME;
const topic = api.topics?.first((x) => x.topicName === topicName);
const { data: logsTopicsData } = useTopicsQuery();
const topic = logsTopicsData?.topics?.first((x) => x.topicName === topicName);

const [logState, setLogState] = useState<{ messages: TopicMessage[]; isComplete: boolean }>({
messages: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
isMultiValue,
Select,
} from '@redpanda-data/ui';
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';

import { api } from '../../../../../state/backend-api';
import { useTopicsQuery } from '../../../../../react-query/api/topic';
import type { Property } from '../../../../../state/connect/state';
import { ExpandableText } from '../../../../misc/expandable-text';

Expand All @@ -40,9 +40,7 @@ export const TopicInput = (p: { properties: Property[]; connectorType: 'sink' |

const [selected, setSelected] = useState(initialSelection);

useEffect(() => {
api.refreshTopics();
}, []);
const { data: topicsData } = useTopicsQuery();

const property = propsMap.get(selected);
const isRegex = selected === 'topics.regex';
Expand Down Expand Up @@ -97,7 +95,7 @@ export const TopicInput = (p: { properties: Property[]; connectorType: 'sink' |
setPropertyValue(property, v.map(({ value }) => value)?.join(',') ?? []);
}
}}
options={api.topics?.map((x) => ({ value: x.topicName, label: x.topicName })) ?? []}
options={topicsData?.topics?.map((x) => ({ value: x.topicName, label: x.topicName })) ?? []}
value={
property.value
? property.value
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/components/pages/consumers/modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
import { ChevronLeftIcon, ChevronRightIcon, SkipIcon, TrashIcon, WarningIcon } from 'components/icons';
import { Component } from 'react';

import queryClient from '../../../query-client';
import { getTopicOffsetsByTimestamp } from '../../../react-query/api/topic';
import { appGlobal } from '../../../state/app-global';
import { api } from '../../../state/backend-api';
import type {
Expand Down Expand Up @@ -496,7 +498,7 @@ export class EditOffsetsModal extends Component<{

let offsetsForTimestamp: TopicOffset[];
try {
offsetsForTimestamp = await api.getTopicOffsetsByTimestamp(requiredTopics, this.state.timestampUtcMs);
offsetsForTimestamp = await getTopicOffsetsByTimestamp(requiredTopics, this.state.timestampUtcMs);
toast.update(toastRef, {
status: 'success',
duration: 2000,
Expand Down Expand Up @@ -660,10 +662,9 @@ export class EditOffsetsModal extends Component<{
// need all groups for "other groups" dropdown
api.refreshConsumerGroups();

// need watermarks for all topics the group consumes
// in order to know earliest/latest offsets
// React Query handles partition data; invalidate to force refetch
const topics = this.props.group.topicOffsets.map((x) => x.topic).distinct();
api.refreshPartitions(topics, true);
queryClient.invalidateQueries({ queryKey: ['topicPartitionsAll', ...topics.slice().sort()] });

// reset settings
this.setState({ page: 0, selectedOption: 'startOffset' });
Expand Down Expand Up @@ -787,7 +788,10 @@ class ColAfter extends Component<{

// not found - no message after given timestamp
// use 'latest'
const partition = api.topicPartitions.get(record.topicName)?.first((p) => p.id === record.partitionId);
const partitionsAllData = queryClient.getQueryData<
Map<string, import('../../../state/rest-interfaces').Partition[] | null>
>(['topicPartitionsAll']);
const partition = partitionsAllData?.get(record.topicName)?.first((p) => p.id === record.partitionId);
return (
<div style={{ display: 'inline-flex', gap: '6px', alignItems: 'center' }}>
<InfoText
Expand Down Expand Up @@ -818,7 +822,10 @@ class ColAfter extends Component<{
}

// Get offset from current partition values
const partition = api.topicPartitions.get(record.topicName)?.first((p) => p.id === record.partitionId);
const partitionsAllData2 = queryClient.getQueryData<
Map<string, import('../../../state/rest-interfaces').Partition[] | null>
>(['topicPartitionsAll']);
const partition = partitionsAllData2?.get(record.topicName)?.first((p) => p.id === record.partitionId);

const content =
val === -2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import React, { Component, type FC, type JSX, useRef, useState } from 'react';

import { BandwidthSlider } from './bandwidth-slider';
import queryClient from '../../../../query-client';
import { api } from '../../../../state/backend-api';
import type { ConfigEntry } from '../../../../state/rest-interfaces';
import { QuickTable } from '../../../../utils/tsx-utils';
Expand Down Expand Up @@ -376,19 +377,17 @@ export class ReassignmentDetailsDialog extends Component<{ state: ReassignmentSt
// became visible or invisible
// force update of topic config, so isThrottle has up to date information
setTimeout(async () => {
api.topicConfig.delete(state.topicName);
await api.refreshTopicConfig(state.topicName, true);
await queryClient.invalidateQueries({ queryKey: ['topicConfig', state.topicName] });
this.setState({ shouldThrottle: this.isThrottled() });
});
}
this.wasVisible = visible;

const topicConfig = api.topicConfig.get(state.topicName);
if (!topicConfig) {
setTimeout(() => {
api.refreshTopicConfig(state.topicName);
});
}
const topicConfig =
queryClient.getQueryData<import('../../../../state/rest-interfaces').TopicDescription | null>([
'topicConfig',
state.topicName,
]) ?? null;

const replicas = state.partitions.flatMap((p) => p.replicas).distinct();
const addingReplicas = state.partitions.flatMap((p) => p.addingReplicas).distinct();
Expand Down Expand Up @@ -463,7 +462,11 @@ export class ReassignmentDetailsDialog extends Component<{ state: ReassignmentSt
if (!this.lastState) {
return false;
}
const config = api.topicConfig.get(this.lastState.topicName);
const config =
queryClient.getQueryData<import('../../../../state/rest-interfaces').TopicDescription | null>([
'topicConfig',
this.lastState.topicName,
]) ?? null;
if (!config) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// - manages timers for refreshing current reassignments
// - tracks progress history for each reassignment to estimate speed and ETA

import queryClient from '../../../../query-client';
import { api } from '../../../../state/backend-api';
import type { PartitionReassignments } from '../../../../state/rest-interfaces';
import { IsDev } from '../../../../utils/env';
Expand Down Expand Up @@ -114,7 +115,7 @@ export class ReassignmentTracker {
// Update relevant topic-partitions
const topics = liveReassignments.map((r) => r.topicName);
if (topics.length > 0) {
await api.refreshPartitions(topics, true);
queryClient.invalidateQueries({ queryKey: ['topicPartitionsAll'] });
}

// Add new reassignments
Expand Down Expand Up @@ -189,7 +190,10 @@ export class ReassignmentTracker {
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: legacy code
updateReassignmentState(state: ReassignmentState) {
// partition stats
const topicPartitions = api.topicPartitions.get(state.topicName);
const topicPartitionsAllData = queryClient.getQueryData<
Map<string, import('../../../../state/rest-interfaces').Partition[] | null>
>(['topicPartitionsAll']);
const topicPartitions = topicPartitionsAllData?.get(state.topicName);
for (const p of state.partitions) {
const logDirs = topicPartitions?.first((e) => e.id === p.partitionId)?.partitionLogDirs.filter((l) => !l.error);
if (!logDirs || logDirs.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { StepSelectPartitions } from './step1-partitions';
import { StepSelectBrokers } from './step2-brokers';
import { StepReview, type TopicWithMoves } from './step3-review';
import queryClient from '../../../query-client';
import { appGlobal } from '../../../state/app-global';
import { api, partialTopicConfigs } from '../../../state/backend-api';
import type {
Expand Down Expand Up @@ -185,8 +186,8 @@ class ReassignPartitions extends PageComponent {

refreshData(force: boolean) {
api.refreshCluster(force); // need to know brokers for reassignment calculation, will also refresh config
api.refreshTopics(force);
api.refreshPartitions('all', force);
queryClient.invalidateQueries({ queryKey: ['topics'] });
queryClient.invalidateQueries({ queryKey: ['topicPartitionsAll'] });
api.refreshPartitionReassignments(force);
}

Expand All @@ -200,16 +201,15 @@ class ReassignPartitions extends PageComponent {
if (!api.clusterInfo) {
return DefaultSkeleton;
}
if (!api.topics) {
return DefaultSkeleton;
}

if (api.partitionReassignments === undefined) {
return DefaultSkeleton;
}

const partitionCountLeaders = api.topics?.sum((t) => t.partitionCount);
const partitionCountOnlyReplicated = api.topics?.sum((t) => t.partitionCount * (t.replicationFactor - 1));
const cachedTopics = queryClient.getQueryData<import('../../../state/rest-interfaces').GetTopicsResponse>([
'topics',
])?.topics;
const partitionCountLeaders = cachedTopics?.sum((t) => t.partitionCount);
const partitionCountOnlyReplicated = cachedTopics?.sum((t) => t.partitionCount * (t.replicationFactor - 1));

const { currentStep, requestInProgress, partitionSelection, selectedBrokerIds, reassignmentRequest } = this.state;

Expand All @@ -236,7 +236,10 @@ class ReassignPartitions extends PageComponent {
<Statistic
title="Total Partitions"
value={
partitionCountLeaders !== null && partitionCountOnlyReplicated !== null
partitionCountLeaders !== null &&
partitionCountLeaders !== undefined &&
partitionCountOnlyReplicated !== null &&
partitionCountOnlyReplicated !== undefined
? partitionCountLeaders + partitionCountOnlyReplicated
: '...'
}
Expand Down Expand Up @@ -497,18 +500,24 @@ class ReassignPartitions extends PageComponent {
}

const apiTopicPartitions = new Map<string, Partition[]>();
for (const [topicName, partitions] of api.topicPartitions) {
const cachedTopicPartitions = queryClient.getQueryData<
Map<string, import('../../../state/rest-interfaces').Partition[] | null>
>(['topicPartitionsAll']);
for (const [topicName, partitions] of cachedTopicPartitions ??
new Map<string, import('../../../state/rest-interfaces').Partition[] | null>()) {
if (!partitions) {
continue;
}
const validOnly = partitions.filter((x) => !x.hasErrors);
apiTopicPartitions.set(topicName, validOnly);
}

const cachedTopicsData =
queryClient.getQueryData<import('../../../state/rest-interfaces').GetTopicsResponse>(['topics'])?.topics ?? [];
// error checking will happen inside computeReassignments
const apiData: ApiData = {
brokers: api.clusterInfo?.brokers ?? [],
topics: api.topics as Topic[],
topics: cachedTopicsData as Topic[],
topicPartitions: apiTopicPartitions,
};

Expand Down Expand Up @@ -634,7 +643,10 @@ class ReassignPartitions extends PageComponent {
const followerReplicas: { partitionId: number; brokerId: number }[] = [];
for (const p of t.partitions) {
const partitionId = p.partitionId;
const brokersOld = api.topicPartitions
const cachedTpForTraffic = queryClient.getQueryData<
Map<string, import('../../../state/rest-interfaces').Partition[] | null>
>(['topicPartitionsAll']);
const brokersOld = cachedTpForTraffic
?.get(t.topicName)
?.first((partition) => partition.id === partitionId)?.replicas;
const brokersNew = p.replicas;
Expand Down Expand Up @@ -773,8 +785,12 @@ class ReassignPartitions extends PageComponent {
}

get selectedTopicPartitions(): TopicPartitions[] | undefined {
const apiTopics = api.topics;
const apiPartitions = api.topicPartitions;
const apiTopics = queryClient.getQueryData<import('../../../state/rest-interfaces').GetTopicsResponse>([
'topics',
])?.topics;
const apiPartitions = queryClient.getQueryData<
Map<string, import('../../../state/rest-interfaces').Partition[] | null>
>(['topicPartitionsAll']);

if (!(apiTopics && apiPartitions)) {
// biome-ignore lint/suspicious/useGetterReturn: early return for undefined case
Expand All @@ -788,7 +804,10 @@ class ReassignPartitions extends PageComponent {
let maxRf = 0;
for (const topicName in this.state.partitionSelection) {
if (Object.hasOwn(this.state.partitionSelection, topicName)) {
const topic = api.topics?.first((x) => x.topicName === topicName);
const cachedTopicsForRf = queryClient.getQueryData<import('../../../state/rest-interfaces').GetTopicsResponse>([
'topics',
])?.topics;
const topic = cachedTopicsForRf?.first((x) => x.topicName === topicName);
if (topic && topic.replicationFactor > maxRf) {
maxRf = topic.replicationFactor;
}
Expand All @@ -801,14 +820,21 @@ class ReassignPartitions extends PageComponent {
if (this.state.reassignmentRequest === null) {
return [];
}
if (api.topics === null) {
const cachedTopicsForMoves = queryClient.getQueryData<import('../../../state/rest-interfaces').GetTopicsResponse>([
'topics',
])?.topics;
if (!cachedTopicsForMoves) {
return [];
}
const cachedTpForMoves =
queryClient.getQueryData<Map<string, import('../../../state/rest-interfaces').Partition[] | null>>([
'topicPartitionsAll',
]) ?? new Map<string, import('../../../state/rest-interfaces').Partition[] | null>();
Comment on lines +830 to +832

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this type of dynamic import? I would just import it, I guess react doctor might complain about it being a barrel file of some sort? I think in this case it would make it worse to have this import everywhere, just hoist it to the top of the file.

return computeMovedReplicas(
this.state.partitionSelection,
this.state.reassignmentRequest,
api.topics,
api.topicPartitions
cachedTopicsForMoves,
cachedTpForMoves
);
}

Expand Down
Loading
Loading