diff --git a/backend/pkg/api/connect/service/topic/v1/console.go b/backend/pkg/api/connect/service/topic/v1/console.go new file mode 100644 index 0000000000..0f49c942c2 --- /dev/null +++ b/backend/pkg/api/connect/service/topic/v1/console.go @@ -0,0 +1,103 @@ +// Copyright 2023 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package topic + +import ( + "context" + "fmt" + "log/slog" + "sort" + "strings" + + "connectrpc.com/connect" + "github.com/redpanda-data/common-go/api/pagination" + + apierrors "github.com/redpanda-data/console/backend/pkg/api/connect/errors" + "github.com/redpanda-data/console/backend/pkg/console" + v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/console/v1alpha1" + "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect" + v1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1" +) + +var _ consolev1alpha1connect.TopicServiceHandler = (*ConsoleService)(nil) + +// ConsoleService is the Console-facing implementation of the topic service. +// +// Unlike the other Console services it does not wrap the dataplane handler: the dataplane +// ListTopics deliberately returns only cheap metadata, whereas the Console topic list also renders +// cleanup policy and on-disk size. Those come from console.GetTopicsOverview, which fans out a +// DescribeLogDirs request to every broker — a cost the public dataplane API should not pay. +type ConsoleService struct { + logger *slog.Logger + consoleSvc console.Servicer + mapper consoleMapper + defaulter defaulter +} + +// NewConsoleService creates a new ConsoleService. +func NewConsoleService(logger *slog.Logger, consoleSvc console.Servicer) *ConsoleService { + return &ConsoleService{ + logger: logger, + consoleSvc: consoleSvc, + mapper: consoleMapper{}, + defaulter: defaulter{}, + } +} + +// ListTopics lists Kafka topics along with the metadata the Console topic list renders. +func (s *ConsoleService) ListTopics(ctx context.Context, req *connect.Request[v1alpha1.ListTopicsRequest]) (*connect.Response[v1alpha1.ListTopicsResponse], error) { + dataplaneReq := req.Msg.GetRequest() + if dataplaneReq == nil { + dataplaneReq = &v1.ListTopicsRequest{} + } + s.defaulter.applyListTopicsRequest(dataplaneReq) + + topicSummaries, err := s.consoleSvc.GetTopicsOverview(ctx) + if err != nil { + return nil, apierrors.NewConnectError( + connect.CodeInternal, + err, + apierrors.NewErrorInfo(v1.Reason_REASON_KAFKA_API_ERROR.String(), apierrors.KeyValsFromKafkaError(err)...), + ) + } + + if nameContains := dataplaneReq.GetFilter().GetNameContains(); nameContains != "" { + filteredTopics := make([]*console.TopicSummary, 0, len(topicSummaries)) + for _, topic := range topicSummaries { + if strings.Contains(topic.TopicName, nameContains) { + filteredTopics = append(filteredTopics, topic) + } + } + topicSummaries = filteredTopics + } + + topics := s.mapper.topicSummariesToProto(topicSummaries) + + var nextPageToken string + if dataplaneReq.GetPageSize() > 0 { + sort.SliceStable(topics, func(i, j int) bool { + return topics[i].GetName() < topics[j].GetName() + }) + page, token, err := pagination.SliceToPaginatedWithToken(topics, int(dataplaneReq.GetPageSize()), dataplaneReq.GetPageToken(), "name", func(x *v1alpha1.ListTopicsResponse_Topic) string { + return x.GetName() + }) + if err != nil { + return nil, apierrors.NewConnectError( + connect.CodeInternal, + fmt.Errorf("failed to apply pagination: %w", err), + apierrors.NewErrorInfo(v1.Reason_REASON_CONSOLE_ERROR.String()), + ) + } + topics = page + nextPageToken = token + } + + return connect.NewResponse(&v1alpha1.ListTopicsResponse{Topics: topics, NextPageToken: nextPageToken}), nil +} diff --git a/backend/pkg/api/connect/service/topic/v1/console_mapper.go b/backend/pkg/api/connect/service/topic/v1/console_mapper.go new file mode 100644 index 0000000000..403ba0447e --- /dev/null +++ b/backend/pkg/api/connect/service/topic/v1/console_mapper.go @@ -0,0 +1,54 @@ +// Copyright 2023 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package topic + +import ( + "github.com/redpanda-data/console/backend/pkg/console" + v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/console/v1alpha1" +) + +// consoleMapper maps console topic summaries to the Console v1alpha1 proto types. +type consoleMapper struct{} + +func (k *consoleMapper) topicSummariesToProto(summaries []*console.TopicSummary) []*v1alpha1.ListTopicsResponse_Topic { + topics := make([]*v1alpha1.ListTopicsResponse_Topic, len(summaries)) + for i, summary := range summaries { + topics[i] = k.topicSummaryToProto(summary) + } + + return topics +} + +func (k *consoleMapper) topicSummaryToProto(summary *console.TopicSummary) *v1alpha1.ListTopicsResponse_Topic { + return &v1alpha1.ListTopicsResponse_Topic{ + Name: summary.TopicName, + Internal: summary.IsInternal, + PartitionCount: int32(summary.PartitionCount), + ReplicationFactor: int32(summary.ReplicationFactor), + CleanupPolicy: summary.CleanupPolicy, + LogDirSummary: k.topicLogDirSummaryToProto(summary.LogDirSummary), + } +} + +func (*consoleMapper) topicLogDirSummaryToProto(summary console.TopicLogDirSummary) *v1alpha1.ListTopicsResponse_LogDirSummary { + replicaErrors := make([]*v1alpha1.ListTopicsResponse_ReplicaError, len(summary.ReplicaErrors)) + for i, replicaError := range summary.ReplicaErrors { + replicaErrors[i] = &v1alpha1.ListTopicsResponse_ReplicaError{ + BrokerId: replicaError.BrokerID, + Error: replicaError.Error, + } + } + + return &v1alpha1.ListTopicsResponse_LogDirSummary{ + TotalSizeBytes: summary.TotalSizeBytes, + ReplicaErrors: replicaErrors, + Hint: summary.Hint, + } +} diff --git a/backend/pkg/api/connect/service/topic/v1/console_mapper_test.go b/backend/pkg/api/connect/service/topic/v1/console_mapper_test.go new file mode 100644 index 0000000000..3a51e2fa1c --- /dev/null +++ b/backend/pkg/api/connect/service/topic/v1/console_mapper_test.go @@ -0,0 +1,101 @@ +// Copyright 2024 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package topic + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/redpanda-data/console/backend/pkg/console" + v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/console/v1alpha1" +) + +// TestConsoleTopicSummariesToProto guards the Console ListTopics response shape: the cleanup policy +// and the aggregated log dir summary (size, hint, per-broker replica errors) must survive the +// mapping from console.TopicSummary. A regression here surfaces as empty cleanup-policy / size +// columns in the UI. These fields live on the Console API rather than the dataplane one, which +// deliberately exposes only cheap metadata. +func TestConsoleTopicSummariesToProto(t *testing.T) { + testCases := []struct { + name string + input []*console.TopicSummary + validateFn func(t *testing.T, result []*v1alpha1.ListTopicsResponse_Topic) + }{ + { + name: "maps cleanup policy and log dir summary", + input: []*console.TopicSummary{ + { + TopicName: "orders", + IsInternal: false, + PartitionCount: 3, + ReplicationFactor: 2, + CleanupPolicy: "compact", + LogDirSummary: console.TopicLogDirSummary{ + TotalSizeBytes: 2048, + Hint: "partial result", + ReplicaErrors: []console.TopicLogDirSummaryReplicaError{ + {BrokerID: 1, Error: "broker down"}, + }, + }, + }, + }, + validateFn: func(t *testing.T, result []*v1alpha1.ListTopicsResponse_Topic) { + require.Len(t, result, 1) + topic := result[0] + assert.Equal(t, "orders", topic.GetName()) + assert.False(t, topic.GetInternal()) + assert.Equal(t, int32(3), topic.GetPartitionCount()) + assert.Equal(t, int32(2), topic.GetReplicationFactor()) + assert.Equal(t, "compact", topic.GetCleanupPolicy()) + + summary := topic.GetLogDirSummary() + require.NotNil(t, summary) + assert.Equal(t, int64(2048), summary.GetTotalSizeBytes()) + assert.Equal(t, "partial result", summary.GetHint()) + require.Len(t, summary.GetReplicaErrors(), 1) + assert.Equal(t, int32(1), summary.GetReplicaErrors()[0].GetBrokerId()) + assert.Equal(t, "broker down", summary.GetReplicaErrors()[0].GetError()) + }, + }, + { + name: "preserves order and the N/A cleanup policy fallback", + input: []*console.TopicSummary{ + {TopicName: "a", CleanupPolicy: "delete", LogDirSummary: console.TopicLogDirSummary{TotalSizeBytes: 1}}, + {TopicName: "b", CleanupPolicy: "N/A", LogDirSummary: console.TopicLogDirSummary{TotalSizeBytes: 2}}, + }, + validateFn: func(t *testing.T, result []*v1alpha1.ListTopicsResponse_Topic) { + require.Len(t, result, 2) + assert.Equal(t, "a", result[0].GetName()) + assert.Equal(t, "delete", result[0].GetCleanupPolicy()) + assert.Equal(t, "b", result[1].GetName()) + assert.Equal(t, "N/A", result[1].GetCleanupPolicy()) + assert.Empty(t, result[1].GetLogDirSummary().GetReplicaErrors()) + }, + }, + { + name: "empty input yields empty slice", + input: []*console.TopicSummary{}, + validateFn: func(t *testing.T, result []*v1alpha1.ListTopicsResponse_Topic) { + assert.Empty(t, result) + }, + }, + } + + consoleMapper := consoleMapper{} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + out := consoleMapper.topicSummariesToProto(tc.input) + tc.validateFn(t, out) + }) + } +} diff --git a/backend/pkg/api/routes.go b/backend/pkg/api/routes.go index a16f6f650d..4c06538925 100644 --- a/backend/pkg/api/routes.go +++ b/backend/pkg/api/routes.go @@ -124,6 +124,7 @@ func (api *API) setupConnectWithGRPCGateway(r chi.Router) { transformSvcV1 := transformsvcv1.NewService(api.Cfg, loggerpkg.Named(api.Logger, "transform_service"), v, api.RedpandaClientProvider) kafkaConnectSvcV1 := apikafkaconnectsvcv1.NewService(api.Cfg, loggerpkg.Named(api.Logger, "kafka_connect_service"), api.ConnectSvc) consoleTransformSvcV1 := &transformsvcv1.ConsoleService{Impl: transformSvcV1} + consoleTopicSvcV1 := topicsvcv1.NewConsoleService(loggerpkg.Named(api.Logger, "topic_service"), api.ConsoleSvc) monitoringSvcV1 := monitoringsvcv1.NewService(api.Cfg, loggerpkg.Named(api.Logger, "monitoring_service"), api.RedpandaClientProvider) // v1alpha2 @@ -170,6 +171,7 @@ func (api *API) setupConnectWithGRPCGateway(r chi.Router) { consolev1alpha1connect.SecurityServiceName: consolev1alpha1connect.UnimplementedSecurityServiceHandler{}, consolev1alpha1connect.LicenseServiceName: licenseSvc, consolev1alpha1connect.TransformServiceName: consoleTransformSvcV1, + consolev1alpha1connect.TopicServiceName: consoleTopicSvcV1, consolev1alpha1connect.AuthenticationServiceName: &AuthenticationDefaultHandler{}, consolev1alpha1connect.ClusterStatusServiceName: clusterStatusSvc, consolev1alpha1connect.SecretServiceName: consolev1alpha1connect.UnimplementedSecretServiceHandler{}, @@ -247,6 +249,9 @@ func (api *API) setupConnectWithGRPCGateway(r chi.Router) { consoleTransformSvcPath, consoleTransformSvcHandler := consolev1alpha1connect.NewTransformServiceHandler( hookOutput.Services[consolev1alpha1connect.TransformServiceName].(consolev1alpha1connect.TransformServiceHandler), connect.WithInterceptors(append(hookOutput.Interceptors, sunsetInterceptor)...)) + consoleTopicSvcPath, consoleTopicSvcHandler := consolev1alpha1connect.NewTopicServiceHandler( + hookOutput.Services[consolev1alpha1connect.TopicServiceName].(consolev1alpha1connect.TopicServiceHandler), + connect.WithInterceptors(append(hookOutput.Interceptors, sunsetInterceptor)...)) licenseSvcPath, licenseSvcHandler := consolev1alpha1connect.NewLicenseServiceHandler(hookOutput.Services[consolev1alpha1connect.LicenseServiceName].(consolev1alpha1connect.LicenseServiceHandler), connect.WithInterceptors(append(hookOutput.Interceptors, sunsetInterceptor)...)) authenticationSvcPath, authenticationSvcHandler := consolev1alpha1connect.NewAuthenticationServiceHandler(hookOutput.Services[consolev1alpha1connect.AuthenticationServiceName].(consolev1alpha1connect.AuthenticationServiceHandler), @@ -355,6 +360,11 @@ func (api *API) setupConnectWithGRPCGateway(r chi.Router) { MountPath: consoleTransformSvcPath, Handler: consoleTransformSvcHandler, }, + { + ServiceName: consolev1alpha1connect.TopicServiceName, + MountPath: consoleTopicSvcPath, + Handler: consoleTopicSvcHandler, + }, { ServiceName: consolev1alpha1connect.LicenseServiceName, MountPath: licenseSvcPath, diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/topic.connect.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/topic.connect.go new file mode 100644 index 0000000000..878d32abd7 --- /dev/null +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/topic.connect.go @@ -0,0 +1,117 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: redpanda/api/console/v1alpha1/topic.proto + +package consolev1alpha1connect + +import ( + context "context" + errors "errors" + http "net/http" + strings "strings" + + connect "connectrpc.com/connect" + + v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/console/v1alpha1" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // TopicServiceName is the fully-qualified name of the TopicService service. + TopicServiceName = "redpanda.api.console.v1alpha1.TopicService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // TopicServiceListTopicsProcedure is the fully-qualified name of the TopicService's ListTopics RPC. + TopicServiceListTopicsProcedure = "/redpanda.api.console.v1alpha1.TopicService/ListTopics" +) + +// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. +var ( + topicServiceServiceDescriptor = v1alpha1.File_redpanda_api_console_v1alpha1_topic_proto.Services().ByName("TopicService") + topicServiceListTopicsMethodDescriptor = topicServiceServiceDescriptor.Methods().ByName("ListTopics") +) + +// TopicServiceClient is a client for the redpanda.api.console.v1alpha1.TopicService service. +type TopicServiceClient interface { + // ListTopics lists topics with the metadata the Console topic list renders. + ListTopics(context.Context, *connect.Request[v1alpha1.ListTopicsRequest]) (*connect.Response[v1alpha1.ListTopicsResponse], error) +} + +// NewTopicServiceClient constructs a client for the redpanda.api.console.v1alpha1.TopicService +// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for +// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply +// the connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewTopicServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) TopicServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &topicServiceClient{ + listTopics: connect.NewClient[v1alpha1.ListTopicsRequest, v1alpha1.ListTopicsResponse]( + httpClient, + baseURL+TopicServiceListTopicsProcedure, + connect.WithSchema(topicServiceListTopicsMethodDescriptor), + connect.WithClientOptions(opts...), + ), + } +} + +// topicServiceClient implements TopicServiceClient. +type topicServiceClient struct { + listTopics *connect.Client[v1alpha1.ListTopicsRequest, v1alpha1.ListTopicsResponse] +} + +// ListTopics calls redpanda.api.console.v1alpha1.TopicService.ListTopics. +func (c *topicServiceClient) ListTopics(ctx context.Context, req *connect.Request[v1alpha1.ListTopicsRequest]) (*connect.Response[v1alpha1.ListTopicsResponse], error) { + return c.listTopics.CallUnary(ctx, req) +} + +// TopicServiceHandler is an implementation of the redpanda.api.console.v1alpha1.TopicService +// service. +type TopicServiceHandler interface { + // ListTopics lists topics with the metadata the Console topic list renders. + ListTopics(context.Context, *connect.Request[v1alpha1.ListTopicsRequest]) (*connect.Response[v1alpha1.ListTopicsResponse], error) +} + +// NewTopicServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewTopicServiceHandler(svc TopicServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + topicServiceListTopicsHandler := connect.NewUnaryHandler( + TopicServiceListTopicsProcedure, + svc.ListTopics, + connect.WithSchema(topicServiceListTopicsMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) + return "/redpanda.api.console.v1alpha1.TopicService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case TopicServiceListTopicsProcedure: + topicServiceListTopicsHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedTopicServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedTopicServiceHandler struct{} + +func (UnimplementedTopicServiceHandler) ListTopics(context.Context, *connect.Request[v1alpha1.ListTopicsRequest]) (*connect.Response[v1alpha1.ListTopicsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("redpanda.api.console.v1alpha1.TopicService.ListTopics is not implemented")) +} diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/topic.connect.gw.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/topic.connect.gw.go new file mode 100644 index 0000000000..790ba918aa --- /dev/null +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/consolev1alpha1connect/topic.connect.gw.go @@ -0,0 +1,41 @@ +// Code generated by protoc-gen-connect-gateway. DO NOT EDIT. +// +// Source: redpanda/api/console/v1alpha1/topic.proto + +package consolev1alpha1connect + +import ( + context "context" + fmt "fmt" + + runtime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + connect_gateway "go.vallahaye.net/connect-gateway" + + v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/console/v1alpha1" +) + +// TopicServiceGatewayServer implements the gRPC server API for the TopicService service. +type TopicServiceGatewayServer struct { + v1alpha1.UnimplementedTopicServiceServer + listTopics connect_gateway.UnaryHandler[v1alpha1.ListTopicsRequest, v1alpha1.ListTopicsResponse] +} + +// NewTopicServiceGatewayServer constructs a Connect-Gateway gRPC server for the TopicService +// service. +func NewTopicServiceGatewayServer(svc TopicServiceHandler, opts ...connect_gateway.HandlerOption) *TopicServiceGatewayServer { + return &TopicServiceGatewayServer{ + listTopics: connect_gateway.NewUnaryHandler(TopicServiceListTopicsProcedure, svc.ListTopics, opts...), + } +} + +func (s *TopicServiceGatewayServer) ListTopics(ctx context.Context, req *v1alpha1.ListTopicsRequest) (*v1alpha1.ListTopicsResponse, error) { + return s.listTopics(ctx, req) +} + +// RegisterTopicServiceHandlerGatewayServer registers the Connect handlers for the TopicService +// "svc" to "mux". +func RegisterTopicServiceHandlerGatewayServer(mux *runtime.ServeMux, svc TopicServiceHandler, opts ...connect_gateway.HandlerOption) { + if err := v1alpha1.RegisterTopicServiceHandlerServer(context.TODO(), mux, NewTopicServiceGatewayServer(svc, opts...)); err != nil { + panic(fmt.Errorf("connect-gateway: %w", err)) + } +} diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic.pb.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic.pb.go new file mode 100644 index 0000000000..e2e0cbc288 --- /dev/null +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic.pb.go @@ -0,0 +1,489 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.0 +// protoc (unknown) +// source: redpanda/api/console/v1alpha1/topic.proto + +package consolev1alpha1 + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + + _ "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/auth/v1" + v1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ListTopicsRequest wraps the dataplane request so paging and filtering stay in sync with the +// public API. +type ListTopicsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Request *v1.ListTopicsRequest `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTopicsRequest) Reset() { + *x = ListTopicsRequest{} + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTopicsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTopicsRequest) ProtoMessage() {} + +func (x *ListTopicsRequest) ProtoReflect() protoreflect.Message { + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTopicsRequest.ProtoReflect.Descriptor instead. +func (*ListTopicsRequest) Descriptor() ([]byte, []int) { + return file_redpanda_api_console_v1alpha1_topic_proto_rawDescGZIP(), []int{0} +} + +func (x *ListTopicsRequest) GetRequest() *v1.ListTopicsRequest { + if x != nil { + return x.Request + } + return nil +} + +// ListTopicsResponse is Console-specific rather than a wrapper of the dataplane response: it +// enriches each topic with data the Console topic list renders but the public dataplane API +// deliberately does not expose (cleanup policy and on-disk size). +type ListTopicsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Topics []*ListTopicsResponse_Topic `protobuf:"bytes,1,rep,name=topics,proto3" json:"topics,omitempty"` + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTopicsResponse) Reset() { + *x = ListTopicsResponse{} + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTopicsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTopicsResponse) ProtoMessage() {} + +func (x *ListTopicsResponse) ProtoReflect() protoreflect.Message { + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTopicsResponse.ProtoReflect.Descriptor instead. +func (*ListTopicsResponse) Descriptor() ([]byte, []int) { + return file_redpanda_api_console_v1alpha1_topic_proto_rawDescGZIP(), []int{1} +} + +func (x *ListTopicsResponse) GetTopics() []*ListTopicsResponse_Topic { + if x != nil { + return x.Topics + } + return nil +} + +func (x *ListTopicsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// ReplicaError describes a log dir query error for a single broker. +type ListTopicsResponse_ReplicaError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Broker that returned the error. + BrokerId int32 `protobuf:"varint,1,opt,name=broker_id,json=brokerId,proto3" json:"broker_id,omitempty"` + // Error message returned by the broker. + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTopicsResponse_ReplicaError) Reset() { + *x = ListTopicsResponse_ReplicaError{} + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTopicsResponse_ReplicaError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTopicsResponse_ReplicaError) ProtoMessage() {} + +func (x *ListTopicsResponse_ReplicaError) ProtoReflect() protoreflect.Message { + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTopicsResponse_ReplicaError.ProtoReflect.Descriptor instead. +func (*ListTopicsResponse_ReplicaError) Descriptor() ([]byte, []int) { + return file_redpanda_api_console_v1alpha1_topic_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *ListTopicsResponse_ReplicaError) GetBrokerId() int32 { + if x != nil { + return x.BrokerId + } + return 0 +} + +func (x *ListTopicsResponse_ReplicaError) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// LogDirSummary describes the aggregated on-disk size of a topic. Gathering it fans out a +// DescribeLogDirs request to every broker, so it may be partial: brokers that failed are +// reported in replica_errors and total_size_bytes is then an estimate. +type ListTopicsResponse_LogDirSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Total size of the topic in bytes across all replicas. + TotalSizeBytes int64 `protobuf:"varint,1,opt,name=total_size_bytes,json=totalSizeBytes,proto3" json:"total_size_bytes,omitempty"` + // Errors encountered while querying log dirs from individual brokers. + ReplicaErrors []*ListTopicsResponse_ReplicaError `protobuf:"bytes,2,rep,name=replica_errors,json=replicaErrors,proto3" json:"replica_errors,omitempty"` + // Optional human-readable hint explaining a partial or missing result. + Hint string `protobuf:"bytes,3,opt,name=hint,proto3" json:"hint,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTopicsResponse_LogDirSummary) Reset() { + *x = ListTopicsResponse_LogDirSummary{} + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTopicsResponse_LogDirSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTopicsResponse_LogDirSummary) ProtoMessage() {} + +func (x *ListTopicsResponse_LogDirSummary) ProtoReflect() protoreflect.Message { + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTopicsResponse_LogDirSummary.ProtoReflect.Descriptor instead. +func (*ListTopicsResponse_LogDirSummary) Descriptor() ([]byte, []int) { + return file_redpanda_api_console_v1alpha1_topic_proto_rawDescGZIP(), []int{1, 1} +} + +func (x *ListTopicsResponse_LogDirSummary) GetTotalSizeBytes() int64 { + if x != nil { + return x.TotalSizeBytes + } + return 0 +} + +func (x *ListTopicsResponse_LogDirSummary) GetReplicaErrors() []*ListTopicsResponse_ReplicaError { + if x != nil { + return x.ReplicaErrors + } + return nil +} + +func (x *ListTopicsResponse_LogDirSummary) GetHint() string { + if x != nil { + return x.Hint + } + return "" +} + +type ListTopicsResponse_Topic struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Topic name. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Whether the topic is internal. + Internal bool `protobuf:"varint,2,opt,name=internal,proto3" json:"internal,omitempty"` + // Topic partition count. + PartitionCount int32 `protobuf:"varint,3,opt,name=partition_count,json=partitionCount,proto3" json:"partition_count,omitempty"` + // Topic replication factor. + ReplicationFactor int32 `protobuf:"varint,4,opt,name=replication_factor,json=replicationFactor,proto3" json:"replication_factor,omitempty"` + // Topic cleanup policy (e.g. `delete`, `compact`). + CleanupPolicy string `protobuf:"bytes,5,opt,name=cleanup_policy,json=cleanupPolicy,proto3" json:"cleanup_policy,omitempty"` + // Aggregated log directory size for the topic across all replicas. + LogDirSummary *ListTopicsResponse_LogDirSummary `protobuf:"bytes,6,opt,name=log_dir_summary,json=logDirSummary,proto3" json:"log_dir_summary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTopicsResponse_Topic) Reset() { + *x = ListTopicsResponse_Topic{} + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTopicsResponse_Topic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTopicsResponse_Topic) ProtoMessage() {} + +func (x *ListTopicsResponse_Topic) ProtoReflect() protoreflect.Message { + mi := &file_redpanda_api_console_v1alpha1_topic_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTopicsResponse_Topic.ProtoReflect.Descriptor instead. +func (*ListTopicsResponse_Topic) Descriptor() ([]byte, []int) { + return file_redpanda_api_console_v1alpha1_topic_proto_rawDescGZIP(), []int{1, 2} +} + +func (x *ListTopicsResponse_Topic) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListTopicsResponse_Topic) GetInternal() bool { + if x != nil { + return x.Internal + } + return false +} + +func (x *ListTopicsResponse_Topic) GetPartitionCount() int32 { + if x != nil { + return x.PartitionCount + } + return 0 +} + +func (x *ListTopicsResponse_Topic) GetReplicationFactor() int32 { + if x != nil { + return x.ReplicationFactor + } + return 0 +} + +func (x *ListTopicsResponse_Topic) GetCleanupPolicy() string { + if x != nil { + return x.CleanupPolicy + } + return "" +} + +func (x *ListTopicsResponse_Topic) GetLogDirSummary() *ListTopicsResponse_LogDirSummary { + if x != nil { + return x.LogDirSummary + } + return nil +} + +var File_redpanda_api_console_v1alpha1_topic_proto protoreflect.FileDescriptor + +var file_redpanda_api_console_v1alpha1_topic_proto_rawDesc = []byte{ + 0x0a, 0x29, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, + 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, + 0x74, 0x6f, 0x70, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1d, 0x72, 0x65, 0x64, + 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x28, 0x72, 0x65, 0x64, 0x70, + 0x61, 0x6e, 0x64, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x25, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x2f, + 0x74, 0x6f, 0x70, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5b, 0x0a, 0x11, 0x4c, + 0x69, 0x73, 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x46, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, + 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa9, 0x05, 0x0a, 0x12, 0x4c, 0x69, 0x73, + 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4f, 0x0a, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x37, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x52, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x1a, 0x41, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x72, 0x6f, 0x6b, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x62, 0x72, 0x6f, + 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0xb4, 0x01, 0x0a, 0x0d, + 0x4c, 0x6f, 0x67, 0x44, 0x69, 0x72, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x28, 0x0a, + 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, + 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x65, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x3e, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x0d, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x69, + 0x6e, 0x74, 0x1a, 0x9f, 0x02, 0x0a, 0x05, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x27, 0x0a, 0x0f, + 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x11, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, + 0x63, 0x74, 0x6f, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x5f, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, + 0x65, 0x61, 0x6e, 0x75, 0x70, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x67, 0x0a, 0x0f, 0x6c, + 0x6f, 0x67, 0x5f, 0x64, 0x69, 0x72, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, + 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x44, 0x69, 0x72, 0x53, 0x75, + 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x0d, 0x6c, 0x6f, 0x67, 0x44, 0x69, 0x72, 0x53, 0x75, 0x6d, + 0x6d, 0x61, 0x72, 0x79, 0x32, 0x8b, 0x01, 0x0a, 0x0c, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x70, + 0x69, 0x63, 0x73, 0x12, 0x30, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, + 0x68, 0x61, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x61, + 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, 0x8a, 0xa6, 0x1d, 0x04, 0x08, 0x01, + 0x10, 0x01, 0x42, 0xab, 0x02, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x72, 0x65, 0x64, 0x70, 0x61, + 0x6e, 0x64, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x42, 0x0a, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x63, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x72, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2d, 0x64, 0x61, 0x74, 0x61, + 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, + 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x72, 0x65, + 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x6f, + 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x73, + 0x6f, 0x6c, 0x65, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x52, 0x41, + 0x43, 0xaa, 0x02, 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x2e, 0x41, 0x70, 0x69, + 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x31, 0xca, 0x02, 0x1d, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, + 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x31, 0xe2, 0x02, 0x29, 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x5c, 0x41, 0x70, 0x69, + 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, + 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x20, + 0x52, 0x65, 0x64, 0x70, 0x61, 0x6e, 0x64, 0x61, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x43, + 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_redpanda_api_console_v1alpha1_topic_proto_rawDescOnce sync.Once + file_redpanda_api_console_v1alpha1_topic_proto_rawDescData = file_redpanda_api_console_v1alpha1_topic_proto_rawDesc +) + +func file_redpanda_api_console_v1alpha1_topic_proto_rawDescGZIP() []byte { + file_redpanda_api_console_v1alpha1_topic_proto_rawDescOnce.Do(func() { + file_redpanda_api_console_v1alpha1_topic_proto_rawDescData = protoimpl.X.CompressGZIP(file_redpanda_api_console_v1alpha1_topic_proto_rawDescData) + }) + return file_redpanda_api_console_v1alpha1_topic_proto_rawDescData +} + +var file_redpanda_api_console_v1alpha1_topic_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_redpanda_api_console_v1alpha1_topic_proto_goTypes = []any{ + (*ListTopicsRequest)(nil), // 0: redpanda.api.console.v1alpha1.ListTopicsRequest + (*ListTopicsResponse)(nil), // 1: redpanda.api.console.v1alpha1.ListTopicsResponse + (*ListTopicsResponse_ReplicaError)(nil), // 2: redpanda.api.console.v1alpha1.ListTopicsResponse.ReplicaError + (*ListTopicsResponse_LogDirSummary)(nil), // 3: redpanda.api.console.v1alpha1.ListTopicsResponse.LogDirSummary + (*ListTopicsResponse_Topic)(nil), // 4: redpanda.api.console.v1alpha1.ListTopicsResponse.Topic + (*v1.ListTopicsRequest)(nil), // 5: redpanda.api.dataplane.v1.ListTopicsRequest +} +var file_redpanda_api_console_v1alpha1_topic_proto_depIdxs = []int32{ + 5, // 0: redpanda.api.console.v1alpha1.ListTopicsRequest.request:type_name -> redpanda.api.dataplane.v1.ListTopicsRequest + 4, // 1: redpanda.api.console.v1alpha1.ListTopicsResponse.topics:type_name -> redpanda.api.console.v1alpha1.ListTopicsResponse.Topic + 2, // 2: redpanda.api.console.v1alpha1.ListTopicsResponse.LogDirSummary.replica_errors:type_name -> redpanda.api.console.v1alpha1.ListTopicsResponse.ReplicaError + 3, // 3: redpanda.api.console.v1alpha1.ListTopicsResponse.Topic.log_dir_summary:type_name -> redpanda.api.console.v1alpha1.ListTopicsResponse.LogDirSummary + 0, // 4: redpanda.api.console.v1alpha1.TopicService.ListTopics:input_type -> redpanda.api.console.v1alpha1.ListTopicsRequest + 1, // 5: redpanda.api.console.v1alpha1.TopicService.ListTopics:output_type -> redpanda.api.console.v1alpha1.ListTopicsResponse + 5, // [5:6] is the sub-list for method output_type + 4, // [4:5] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_redpanda_api_console_v1alpha1_topic_proto_init() } +func file_redpanda_api_console_v1alpha1_topic_proto_init() { + if File_redpanda_api_console_v1alpha1_topic_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_redpanda_api_console_v1alpha1_topic_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_redpanda_api_console_v1alpha1_topic_proto_goTypes, + DependencyIndexes: file_redpanda_api_console_v1alpha1_topic_proto_depIdxs, + MessageInfos: file_redpanda_api_console_v1alpha1_topic_proto_msgTypes, + }.Build() + File_redpanda_api_console_v1alpha1_topic_proto = out.File + file_redpanda_api_console_v1alpha1_topic_proto_rawDesc = nil + file_redpanda_api_console_v1alpha1_topic_proto_goTypes = nil + file_redpanda_api_console_v1alpha1_topic_proto_depIdxs = nil +} diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic.pb.gw.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic.pb.gw.go new file mode 100644 index 0000000000..79e89e5ca4 --- /dev/null +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic.pb.gw.go @@ -0,0 +1,157 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: redpanda/api/console/v1alpha1/topic.proto + +/* +Package consolev1alpha1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package consolev1alpha1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_TopicService_ListTopics_0(ctx context.Context, marshaler runtime.Marshaler, client TopicServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListTopicsRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ListTopics(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_TopicService_ListTopics_0(ctx context.Context, marshaler runtime.Marshaler, server TopicServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListTopicsRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListTopics(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterTopicServiceHandlerServer registers the http handlers for service TopicService to "mux". +// UnaryRPC :call TopicServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterTopicServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterTopicServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server TopicServiceServer) error { + mux.Handle(http.MethodPost, pattern_TopicService_ListTopics_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/redpanda.api.console.v1alpha1.TopicService/ListTopics", runtime.WithHTTPPathPattern("/redpanda.api.console.v1alpha1.TopicService/ListTopics")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_TopicService_ListTopics_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_TopicService_ListTopics_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterTopicServiceHandlerFromEndpoint is same as RegisterTopicServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterTopicServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterTopicServiceHandler(ctx, mux, conn) +} + +// RegisterTopicServiceHandler registers the http handlers for service TopicService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterTopicServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterTopicServiceHandlerClient(ctx, mux, NewTopicServiceClient(conn)) +} + +// RegisterTopicServiceHandlerClient registers the http handlers for service TopicService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "TopicServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "TopicServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "TopicServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterTopicServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client TopicServiceClient) error { + mux.Handle(http.MethodPost, pattern_TopicService_ListTopics_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/redpanda.api.console.v1alpha1.TopicService/ListTopics", runtime.WithHTTPPathPattern("/redpanda.api.console.v1alpha1.TopicService/ListTopics")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_TopicService_ListTopics_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_TopicService_ListTopics_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_TopicService_ListTopics_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"redpanda.api.console.v1alpha1.TopicService", "ListTopics"}, "")) +) + +var ( + forward_TopicService_ListTopics_0 = runtime.ForwardResponseMessage +) diff --git a/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic_grpc.pb.go b/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic_grpc.pb.go new file mode 100644 index 0000000000..bc26177cc2 --- /dev/null +++ b/backend/pkg/protogen/redpanda/api/console/v1alpha1/topic_grpc.pb.go @@ -0,0 +1,130 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: redpanda/api/console/v1alpha1/topic.proto + +package consolev1alpha1 + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + TopicService_ListTopics_FullMethodName = "/redpanda.api.console.v1alpha1.TopicService/ListTopics" +) + +// TopicServiceClient is the client API for TopicService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// TopicService is the Console-facing topic API. It exists alongside the dataplane TopicService so +// that Console-only enrichment does not leak into the public dataplane contract. +type TopicServiceClient interface { + // ListTopics lists topics with the metadata the Console topic list renders. + ListTopics(ctx context.Context, in *ListTopicsRequest, opts ...grpc.CallOption) (*ListTopicsResponse, error) +} + +type topicServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewTopicServiceClient(cc grpc.ClientConnInterface) TopicServiceClient { + return &topicServiceClient{cc} +} + +func (c *topicServiceClient) ListTopics(ctx context.Context, in *ListTopicsRequest, opts ...grpc.CallOption) (*ListTopicsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTopicsResponse) + err := c.cc.Invoke(ctx, TopicService_ListTopics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TopicServiceServer is the server API for TopicService service. +// All implementations must embed UnimplementedTopicServiceServer +// for forward compatibility. +// +// TopicService is the Console-facing topic API. It exists alongside the dataplane TopicService so +// that Console-only enrichment does not leak into the public dataplane contract. +type TopicServiceServer interface { + // ListTopics lists topics with the metadata the Console topic list renders. + ListTopics(context.Context, *ListTopicsRequest) (*ListTopicsResponse, error) + mustEmbedUnimplementedTopicServiceServer() +} + +// UnimplementedTopicServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTopicServiceServer struct{} + +func (UnimplementedTopicServiceServer) ListTopics(context.Context, *ListTopicsRequest) (*ListTopicsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListTopics not implemented") +} +func (UnimplementedTopicServiceServer) mustEmbedUnimplementedTopicServiceServer() {} +func (UnimplementedTopicServiceServer) testEmbeddedByValue() {} + +// UnsafeTopicServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TopicServiceServer will +// result in compilation errors. +type UnsafeTopicServiceServer interface { + mustEmbedUnimplementedTopicServiceServer() +} + +func RegisterTopicServiceServer(s grpc.ServiceRegistrar, srv TopicServiceServer) { + // If the following call pancis, it indicates UnimplementedTopicServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&TopicService_ServiceDesc, srv) +} + +func _TopicService_ListTopics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTopicsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TopicServiceServer).ListTopics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TopicService_ListTopics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TopicServiceServer).ListTopics(ctx, req.(*ListTopicsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// TopicService_ServiceDesc is the grpc.ServiceDesc for TopicService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TopicService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "redpanda.api.console.v1alpha1.TopicService", + HandlerType: (*TopicServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListTopics", + Handler: _TopicService_ListTopics_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "redpanda/api/console/v1alpha1/topic.proto", +} diff --git a/frontend/src/components/misc/common.tsx b/frontend/src/components/misc/common.tsx index 854f2c298d..e53ea34999 100644 --- a/frontend/src/components/misc/common.tsx +++ b/frontend/src/components/misc/common.tsx @@ -13,7 +13,6 @@ import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Modal import { AlertIcon } from 'components/icons'; import React, { type JSX, type PropsWithChildren, useState } from 'react'; -import type { TopicLogDirSummary } from '../../state/rest-interfaces'; import { uiState } from '../../state/ui-state'; import env, { IsDev } from '../../utils/env'; import { ZeroSizeWrapper } from '../../utils/tsx-utils'; @@ -122,14 +121,17 @@ export const UpdatePopup = () => { ); }; -export function renderLogDirSummary(summary: TopicLogDirSummary): JSX.Element { - if (!summary.hint) { - return <>{prettyBytesOrNA(summary.totalSizeBytes)}; +// Accepts both the REST `TopicLogDirSummary` (number) and the gRPC `ListTopicsResponse_LogDirSummary` +// (bigint, optional) shapes — only `totalSizeBytes` and `hint` are rendered. +export function renderLogDirSummary(summary?: { totalSizeBytes: number | bigint; hint?: string | null }): JSX.Element { + const totalSizeBytes = Number(summary?.totalSizeBytes ?? 0); + if (!summary?.hint) { + return <>{prettyBytesOrNA(totalSizeBytes)}; } return ( <> - {prettyBytesOrNA(summary.totalSizeBytes)} + {prettyBytesOrNA(totalSizeBytes)} ); } diff --git a/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.browser.test.tsx b/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.browser.test.tsx index 68ca7b2c7a..b1f41692e5 100644 --- a/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.browser.test.tsx +++ b/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.browser.test.tsx @@ -101,7 +101,7 @@ vi.mock('react-query/api/remote-mcp', () => ({ })); vi.mock('react-query/api/topic', () => ({ - useLegacyListTopicsQuery: () => mocks.listTopics(), + useListTopicsQuery: () => mocks.listTopics(), useCreateTopicMutation: () => ({ mutateAsync: mocks.createTopic }), })); diff --git a/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.tsx b/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.tsx index 5972307b0a..48f5fad996 100644 --- a/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.tsx +++ b/frontend/src/components/pages/mcp-servers/details/remote-mcp-inspector-tab.tsx @@ -42,7 +42,7 @@ import { useListMCPServerTools, useStreamMCPServerToolMutation, } from 'react-query/api/remote-mcp'; -import { useCreateTopicMutation, useLegacyListTopicsQuery } from 'react-query/api/topic'; +import { useCreateTopicMutation, useListTopicsQuery } from 'react-query/api/topic'; import { toast } from 'sonner'; import { RemoteMCPToolButton } from './remote-mcp-tool-button'; @@ -197,7 +197,7 @@ export const RemoteMCPInspectorTab = () => { mcpServer: mcpServerData?.mcpServer, }); - const { data: topicsData, refetch: refetchTopics } = useLegacyListTopicsQuery(undefined, { + const { data: topicsData, refetch: refetchTopics } = useListTopicsQuery({ pageSize: -1 }, undefined, { hideInternalTopics: true, }); const { mutateAsync: createTopic } = useCreateTopicMutation(); @@ -262,7 +262,7 @@ export const RemoteMCPInspectorTab = () => { Array.isArray(topicsData.topics) ) { // Validate that the topic_name exists in the available topics - const topicExists = topicsData.topics.some((topic: { topicName: string }) => topic.topicName === value); + const topicExists = topicsData.topics.some((topic: { name: string }) => topic.name === value); if (!topicExists) { errors[requiredField] = `Topic '${value}' does not exist. Select a valid topic name or create a new one.`; continue; @@ -293,7 +293,7 @@ export const RemoteMCPInspectorTab = () => { (mcpServerData.mcpServer.tools[selectedTool].componentType || getComponentTypeFromToolName(selectedTool)) === MCPServer_Tool_ComponentType.OUTPUT ) { - const availableTopic = topicsData.topics[0].topicName; + const availableTopic = topicsData.topics[0].name; const params = toolParameters as { topic_name?: string; messages?: Array<{ topic_name?: string }> }; // Check if topic_name needs to be set - auto-select for messages without topic_name @@ -549,8 +549,8 @@ export const RemoteMCPInspectorTab = () => { { fieldName: 'topic_name', options: topicsData.topics.map((topic) => ({ - value: topic.topicName, - label: topic.topicName, + value: topic.name, + label: topic.name, })), placeholder: 'Select a topic or create a new one...', onCreateOption: async ( diff --git a/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.test.tsx b/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.test.tsx index 630400d565..35673999d2 100644 --- a/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.test.tsx +++ b/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.test.tsx @@ -12,6 +12,8 @@ import { create } from '@bufbuild/protobuf'; import { ConnectError, createRouterTransport } from '@connectrpc/connect'; import userEvent from '@testing-library/user-event'; +import { ListTopicsResponseSchema } from 'protogen/redpanda/api/console/v1alpha1/topic_pb'; +import { listTopics } from 'protogen/redpanda/api/console/v1alpha1/topic-TopicService_connectquery'; import { CreateTopicResponseSchema } from 'protogen/redpanda/api/dataplane/v1/topic_pb'; import { createTopic } from 'protogen/redpanda/api/dataplane/v1/topic-TopicService_connectquery'; import type { ComponentProps } from 'react'; @@ -60,23 +62,7 @@ import { AddTopicStep } from './add-topic-step'; // ── Helpers ──────────────────────────────────────────────────────────── -/** REST response for useLegacyListTopicsQuery */ -function createTopicsResponse(topicNames: string[]) { - return { - topics: topicNames.map((name) => ({ - topicName: name, - isInternal: false, - partitionCount: 1, - replicationFactor: 3, - cleanupPolicy: 'delete', - documentation: 'UNKNOWN' as const, - logDirSummary: { totalSizeBytes: 0, partitionCount: 0, replicaErrors: [] }, - allowedActions: undefined, - })), - }; -} - -function createTransport(overrides?: { createTopicMock?: ReturnType }) { +function createTransport(overrides?: { createTopicMock?: ReturnType; topicNames?: string[] }) { return createRouterTransport(({ rpc }) => { rpc( createTopic, @@ -89,6 +75,20 @@ function createTransport(overrides?: { createTopicMock?: ReturnType + create(ListTopicsResponseSchema, { + topics: (overrides?.topicNames ?? []).map((name) => ({ + name, + internal: false, + partitionCount: 1, + replicationFactor: 3, + cleanupPolicy: 'delete', + logDirSummary: { totalSizeBytes: 0n, replicaErrors: [], hint: '' }, + })), + nextPageToken: '', + }) + ); }); } @@ -113,22 +113,13 @@ function TestHarness({ onResult, ...props }: HarnessProps) { describe('AddTopicStep', () => { beforeEach(() => { mockFetch.mockReset(); - // Default: return empty topic list - mockFetch.mockResolvedValue({ - ok: true, - json: () => Promise.resolve(createTopicsResponse([])), - }); }); it('existing topic returns name via triggerSubmit', async () => { const user = userEvent.setup(); - mockFetch.mockResolvedValue({ - ok: true, - json: () => Promise.resolve(createTopicsResponse(['my-topic', 'other-topic'])), - }); let result: unknown; - const transport = createTransport(); + const transport = createTransport({ topicNames: ['my-topic', 'other-topic'] }); render( { { transport } ); - // Wait for topics to load so the combobox has options - await waitFor(() => { - expect(mockFetch).toHaveBeenCalled(); - }); - // The component starts in "Existing" mode when topics exist. // Open the combobox, type to filter, then click the matching option. // (Pressing Enter races cmdk's highlighted-value bookkeeping in CI under @@ -285,11 +271,7 @@ describe('AddTopicStep', () => { }); it('selectionMode=existing shows combobox', async () => { - mockFetch.mockResolvedValue({ - ok: true, - json: () => Promise.resolve(createTopicsResponse(['topic-a'])), - }); - const transport = createTransport(); + const transport = createTransport({ topicNames: ['topic-a'] }); render( {}} selectionMode="existing" />, { transport }); @@ -299,19 +281,10 @@ describe('AddTopicStep', () => { it('existing topic alert shown in create mode when name matches', async () => { const user = userEvent.setup(); - mockFetch.mockResolvedValue({ - ok: true, - json: () => Promise.resolve(createTopicsResponse(['existing-topic'])), - }); - const transport = createTransport(); + const transport = createTransport({ topicNames: ['existing-topic'] }); render( {}} selectionMode="both" />, { transport }); - // Wait for topics to load - await waitFor(() => { - expect(mockFetch).toHaveBeenCalled(); - }); - // Switch to "New" tab (single-select ToggleGroupItem renders as a Base UI toggle button (aria-pressed)) const newButton = await screen.findByRole('button', { name: 'New' }); await user.click(newButton); @@ -359,11 +332,7 @@ describe('AddTopicStep', () => { }); it('selectionMode=both renders toggle group', async () => { - mockFetch.mockResolvedValue({ - ok: true, - json: () => Promise.resolve(createTopicsResponse(['t1'])), - }); - const transport = createTransport(); + const transport = createTransport({ topicNames: ['t1'] }); render( {}} selectionMode="both" />, { transport }); diff --git a/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.tsx b/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.tsx index a8c001cc5d..fb0c4499ad 100644 --- a/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.tsx +++ b/frontend/src/components/pages/rp-connect/onboarding/add-topic-step.tsx @@ -26,7 +26,6 @@ import { listTopics } from 'protogen/redpanda/api/dataplane/v1/topic-TopicServic import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { useGetKafkaInfoQuery } from 'react-query/api/cluster-status'; -import { useLegacyListTopicsQuery } from 'react-query/api/topic'; import { LONG_LIVED_CACHE_STALE_TIME } from 'react-query/react-query.utils'; import { isFalsy } from 'utils/falsy'; @@ -36,7 +35,7 @@ import { CreateTopicRequest_TopicSchema, CreateTopicRequestSchema, } from '../../../../protogen/redpanda/api/dataplane/v1/topic_pb'; -import { useCreateTopicMutation, useTopicConfigQuery } from '../../../../react-query/api/topic'; +import { useCreateTopicMutation, useListTopicsQuery, useTopicConfigQuery } from '../../../../react-query/api/topic'; import { convertRetentionSizeToBytes, convertRetentionTimeToMs } from '../../../../utils/topic-utils'; import { type AddTopicFormData, @@ -76,21 +75,21 @@ export const AddTopicStep = forwardRef, AddTopicSt ) => { const queryClient = useQueryClient(); - const { data: topicList } = useLegacyListTopicsQuery(create(ListTopicsRequestSchema, {}), { - hideInternalTopics: true, - staleTime: LONG_LIVED_CACHE_STALE_TIME, - refetchOnWindowFocus: false, - }); + const { data: topicList } = useListTopicsQuery( + create(ListTopicsRequestSchema, { pageSize: -1 }), + { staleTime: LONG_LIVED_CACHE_STALE_TIME, refetchOnWindowFocus: false }, + { hideInternalTopics: true } + ); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const topicOptions = useMemo( () => topicList?.topics - ?.filter((topic) => !(hideInternal && topic.topicName.startsWith('__'))) + ?.filter((topic) => !(hideInternal && topic.name.startsWith('__'))) .map((topic) => ({ - value: topic.topicName, - label: topic.topicName, + value: topic.name, + label: topic.name, })) ?? [], [topicList, hideInternal] ); @@ -155,12 +154,12 @@ export const AddTopicStep = forwardRef, AddTopicSt if (!watchedTopicName) { return; } - return topicList?.topics?.find((topic) => topic.topicName === watchedTopicName); + return topicList?.topics?.find((topic) => topic.name === watchedTopicName); }, [watchedTopicName, topicList]); const { data: topicConfig } = useTopicConfigQuery( - existingTopicSelected?.topicName || '', - !isFalsy(existingTopicSelected?.topicName) + existingTopicSelected?.name || '', + !isFalsy(existingTopicSelected?.name) ); useEffect(() => { @@ -168,13 +167,20 @@ export const AddTopicStep = forwardRef, AddTopicSt return; } if (topicConfig && !topicConfig.error) { - const allTopicValues = parseTopicConfigFromExisting(existingTopicSelected, topicConfig); + const allTopicValues = parseTopicConfigFromExisting( + { + topicName: existingTopicSelected.name, + partitionCount: existingTopicSelected.partitionCount, + replicationFactor: existingTopicSelected.replicationFactor, + }, + topicConfig + ); // Override the form-level `keepDirtyValues: true` default — when a user // selects an existing topic, its config must fully replace any partial // input they've made. form.reset(allTopicValues, { keepDefaultValues: false, keepDirtyValues: false }); } else { - form.setValue('topicName', existingTopicSelected.topicName, { + form.setValue('topicName', existingTopicSelected.name, { shouldDirty: false, }); } diff --git a/frontend/src/components/pages/rp-connect/pipeline/pipeline-command-menu.test.tsx b/frontend/src/components/pages/rp-connect/pipeline/pipeline-command-menu.test.tsx index 81addbccb9..08faf2fe16 100644 --- a/frontend/src/components/pages/rp-connect/pipeline/pipeline-command-menu.test.tsx +++ b/frontend/src/components/pages/rp-connect/pipeline/pipeline-command-menu.test.tsx @@ -15,9 +15,12 @@ import userEvent from '@testing-library/user-event'; import type { editor } from 'monaco-editor'; import { ListSecretsResponseSchema } from 'protogen/redpanda/api/console/v1alpha1/secret_pb'; import { listSecrets } from 'protogen/redpanda/api/console/v1alpha1/secret-SecretService_connectquery'; +import { + ListTopicsResponse_TopicSchema, + ListTopicsResponseSchema, +} from 'protogen/redpanda/api/console/v1alpha1/topic_pb'; +import { listTopics } from 'protogen/redpanda/api/console/v1alpha1/topic-TopicService_connectquery'; import { SecretSchema } from 'protogen/redpanda/api/dataplane/v1/secret_pb'; -import { ListTopicsResponse_TopicSchema, ListTopicsResponseSchema } from 'protogen/redpanda/api/dataplane/v1/topic_pb'; -import { listTopics } from 'protogen/redpanda/api/dataplane/v1/topic-TopicService_connectquery'; import { ListUsersResponse_UserSchema, ListUsersResponseSchema } from 'protogen/redpanda/api/dataplane/v1/user_pb'; import { listUsers } from 'protogen/redpanda/api/dataplane/v1/user-UserService_connectquery'; import { render, screen, waitFor } from 'test-utils'; diff --git a/frontend/src/components/pages/sql/sql-workspace.tsx b/frontend/src/components/pages/sql/sql-workspace.tsx index b3e9c42327..f9b11d8839 100644 --- a/frontend/src/components/pages/sql/sql-workspace.tsx +++ b/frontend/src/components/pages/sql/sql-workspace.tsx @@ -35,7 +35,7 @@ import { useListTablesQuery, useTopicIcebergQuery, } from 'react-query/api/sql'; -import { useLegacyListTopicsQuery } from 'react-query/api/topic'; +import { useListTopicsQuery } from 'react-query/api/topic'; import { toast } from 'sonner'; import { Feature, isSupported, useSupportedFeaturesStore } from 'state/supported-features'; import { uiState } from 'state/ui-state'; @@ -565,7 +565,7 @@ export function SqlWorkspace({ sqlRole: sqlRoleProp }: SqlWorkspaceProps) { // ---- Add-topic wizard ---- const [wizardOpen, setWizardOpen] = useState(false); const [wizardError, setWizardError] = useState(undefined); - const { data: topicsData } = useLegacyListTopicsQuery(undefined, { hideInternalTopics: true }); + const { data: topicsData } = useListTopicsQuery(undefined, undefined, { hideInternalTopics: true }); const invalidateSqlCatalog = useInvalidateSqlCatalog(); // Topics already exposed as tables in the Redpanda catalog — excluded from @@ -584,8 +584,8 @@ export function SqlWorkspace({ sqlRole: sqlRoleProp }: SqlWorkspaceProps) { const wizardTopics = useMemo( () => (topicsData?.topics ?? []) - .filter((t) => !takenTopics.has(t.topicName)) - .map((t) => ({ name: t.topicName, partitions: t.partitionCount })), + .filter((t) => !takenTopics.has(t.name)) + .map((t) => ({ name: t.name, partitions: t.partitionCount })), [topicsData, takenTopics] ); diff --git a/frontend/src/components/pages/topics/create-topic-modal.tsx b/frontend/src/components/pages/topics/create-topic-modal.tsx index 0b937ca499..af283a762e 100644 --- a/frontend/src/components/pages/topics/create-topic-modal.tsx +++ b/frontend/src/components/pages/topics/create-topic-modal.tsx @@ -21,7 +21,7 @@ import { ListTopicsRequestSchema, } from 'protogen/redpanda/api/dataplane/v1/topic_pb'; import { useGetKafkaInfoQuery } from 'react-query/api/cluster-status'; -import { useCreateTopicMutation, useLegacyListTopicsQuery } from 'react-query/api/topic'; +import { useCreateTopicMutation, useListTopicsQuery } from 'react-query/api/topic'; import { z } from 'zod'; export const topicSchema = z.object({ @@ -41,7 +41,7 @@ export const DEFAULT_TOPIC_PARTITION_COUNT = 1; export const DEFAULT_TOPIC_REPLICATION_FACTOR = 3; export const CreateTopicModal = ({ isOpen, onClose }: CreateTopicModalProps) => { - const { data: topicList } = useLegacyListTopicsQuery(create(ListTopicsRequestSchema, {}), { + const { data: topicList } = useListTopicsQuery(create(ListTopicsRequestSchema, { pageSize: -1 }), undefined, { hideInternalTopics: true, }); const { mutateAsync: createTopic, isPending: isCreateTopicPending } = useCreateTopicMutation(); @@ -104,7 +104,7 @@ export const CreateTopicModal = ({ isOpen, onClose }: CreateTopicModalProps) => name="name" validators={{ onChange: ({ value }) => - topicList?.topics?.some((topic) => topic?.topicName === value) + topicList?.topics?.some((topic) => topic?.name === value) ? { message: 'Name is already in use', path: 'name' } : undefined, }} diff --git a/frontend/src/components/pages/topics/topic-list-new.tsx b/frontend/src/components/pages/topics/topic-list-new.tsx index 65b09de56e..3c9033bb3b 100644 --- a/frontend/src/components/pages/topics/topic-list-new.tsx +++ b/frontend/src/components/pages/topics/topic-list-new.tsx @@ -39,18 +39,18 @@ import { ListLayoutPagination, ListLayoutSearchInput, } from 'components/redpanda-ui/components/list-layout'; -import { AlertCircle, AlertTriangle, DatabaseIcon, EyeOff, Search, X } from 'lucide-react'; +import { AlertCircle, AlertTriangle, DatabaseIcon, Search, X } from 'lucide-react'; import { parseAsBoolean, parseAsInteger, parseAsString, useQueryState } from 'nuqs'; +import type { ListTopicsResponse_Topic } from 'protogen/redpanda/api/console/v1alpha1/topic_pb'; import type { FC } from 'react'; import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; -import { useLegacyListTopicsQuery } from 'react-query/api/topic'; +import { useListTopicsQuery } from 'react-query/api/topic'; import { toast } from 'sonner'; import { CreateTopicDialog } from './create-topic-dialog'; import { useQueryStateWithCallback } from '../../../hooks/use-query-state-with-callback'; import { appGlobal } from '../../../state/app-global'; import { api } from '../../../state/backend-api'; -import { type Topic, TopicActions } from '../../../state/rest-interfaces'; import { uiSettings } from '../../../state/ui'; import { setPageHeader } from '../../../state/ui-state'; import { DEFAULT_TABLE_PAGE_SIZE } from '../../constants'; @@ -71,7 +71,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ import { Text } from '../../redpanda-ui/components/typography'; import { DeleteResourceAlertDialog } from '../../ui/delete-resource-alert-dialog'; -const nameFilterFn = (row: Row, columnId: string, filterValue: string) => { +const nameFilterFn = (row: Row, columnId: string, filterValue: string) => { if (!filterValue) { return true; } @@ -99,8 +99,9 @@ const TopicList: FC = () => { parseAsBoolean ); - const { data, isLoading, isError, error, refetch: refetchTopics } = useLegacyListTopicsQuery(); - const [topicToDelete, setTopicToDelete] = useState(null); + // pageSize -1 disables server-side pagination so the full topic list is returned for client-side filtering. + const { data, isLoading, isError, error, refetch: refetchTopics } = useListTopicsQuery({ pageSize: -1 }); + const [topicToDelete, setTopicToDelete] = useState(null); const [deletionPending, setDeletionPending] = useState(false); const [isCreateTopicModalOpen, setIsCreateTopicModalOpen] = useState(false); @@ -134,7 +135,7 @@ const TopicList: FC = () => { const allTopics = useMemo(() => { let filtered = data.topics ?? []; if (!showInternalTopics) { - filtered = filtered.filter((x) => !(x.isInternal || x.topicName.startsWith('_'))); + filtered = filtered.filter((x) => !(x.internal || x.name.startsWith('_'))); } return filtered; }, [data.topics, showInternalTopics]); @@ -197,7 +198,7 @@ const TopicList: FC = () => { void setPageIndex(0); }; const [columnFilters, setColumnFilters] = useState( - searchValue ? [{ id: 'topicName', value: searchValue }] : [] + searchValue ? [{ id: 'name', value: searchValue }] : [] ); const pagination: PaginationState = { pageIndex, pageSize }; @@ -212,13 +213,13 @@ const TopicList: FC = () => { const next = typeof updater === 'function' ? updater(columnFilters) : updater; setColumnFilters(next); void setPageIndex(0); - const nameFilter = next.find((f) => f.id === 'topicName'); + const nameFilter = next.find((f) => f.id === 'name'); setSearchValue((nameFilter?.value as string) || null); }; - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { - accessorKey: 'topicName', + accessorKey: 'name', header: ({ column }) => , filterFn: nameFilterFn, meta: { headWidth: 'full' as const }, @@ -226,8 +227,8 @@ const TopicList: FC = () => {
@@ -256,7 +257,7 @@ const TopicList: FC = () => { }, { id: 'size', - accessorFn: (topic) => topic.logDirSummary?.totalSizeBytes ?? 0, + accessorFn: (topic) => Number(topic.logDirSummary?.totalSizeBytes ?? 0), header: ({ column }) => , meta: { headWidth: 'sm' as const }, cell: ({ row: { original: topic } }) => renderLogDirSummary(topic.logDirSummary), @@ -272,7 +273,7 @@ const TopicList: FC = () => { render={