Compare commits

...
Sign in to create a new pull request.

12 commits

Author SHA1 Message Date
Roman Zabaluev
56fa824510 BE: RBAC: Implement an authorities extractor to support subject-level role matching (#3979)
Co-authored-by: Ilya Kuramshin <iliax@proton.me>
(cherry picked from commit b700ac3991)
2023-06-27 20:42:41 +08:00
Roman Zabaluev
b0c0e06f19 BE: RBAC: Fix viewable topics filter (#3946)
(cherry picked from commit 6fe6165427)
2023-06-27 20:42:34 +08:00
Roman Zabaluev
556ec290eb BE: Make webclients use system proxy (#3881)
Co-authored-by: Ilya Kuramshin <iliax@proton.me>
(cherry picked from commit f19abb2036)
2023-06-27 20:42:12 +08:00
Roman Zabaluev
2b334d5209 FE: Fix react query not initiating requests for local host (#3915)
(cherry picked from commit 2ac8646769)
2023-06-27 20:41:48 +08:00
Roman Zabaluev
59f03200f4 FE: Fix permissions check for cluster edit (#3862)
(cherry picked from commit 71a7a1ec84)
2023-06-27 20:41:31 +08:00
David Bejanyan
aa633f424c FE: Fix latest version is null (#3833)
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
(cherry picked from commit f22c910f5c)
2023-06-27 20:41:20 +08:00
Ilya Kuramshin
f7aaea85f4 BE: Chore: CVEs fixes, May 2023 (#3840)
Co-authored-by: iliax <ikuramshin@provectus.com>
(cherry picked from commit f7d85d86e6)
2023-06-27 20:41:06 +08:00
Roman Zabaluev
4ae1da6ebf RBAC: Fix KC restart permissions (#3891)
Co-authored-by: David Bejanyan <58771979+David-DB88@users.noreply.github.com>
(cherry picked from commit 8a68ba0778)
2023-06-27 20:39:50 +08:00
Roman Zabaluev
3377289517 FE: Ignore expected 404 on topic statistics page (#3964)
(cherry picked from commit b9bbb1a823)
2023-06-27 20:38:23 +08:00
Roman Zabaluev
8727393501 BE: Fix CORS once again (#3957)
(cherry picked from commit 9549f68d7e)
2023-06-27 20:38:03 +08:00
Vikas Rajput
26464ba37d FE: Messages: Reset timestamp value w/ Clear all filters (#3923)
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
(cherry picked from commit d0088490a4)
2023-06-27 20:37:39 +08:00
David Bejanyan
023e8e3b3c FE: Fix version display with a tag (#3827)
(cherry picked from commit ab9d0e2b3f)
2023-06-27 20:37:07 +08:00
22 changed files with 230 additions and 85 deletions

View file

@ -21,12 +21,6 @@
</properties>
<dependencies>
<dependency>
<!--TODO: remove, when spring-boot fixed dependency to 6.0.8+ (6.0.7 has CVE) -->
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.0.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
@ -61,7 +55,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@ -97,7 +91,7 @@
<dependency>
<groupId>software.amazon.msk</groupId>
<artifactId>aws-msk-iam-auth</artifactId>
<version>1.1.5</version>
<version>1.1.6</version>
</dependency>
<dependency>
@ -115,7 +109,6 @@
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-extra</artifactId>
</dependency>
<!-- https://github.com/provectus/kafka-ui/pull/3693 -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>

View file

@ -1,18 +1,41 @@
package com.provectus.kafka.ui.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Configuration
public class CorsGlobalConfiguration implements WebFluxConfigurer {
public class CorsGlobalConfiguration {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(false);
@Bean
public WebFilter corsFilter() {
return (final ServerWebExchange ctx, final WebFilterChain chain) -> {
final ServerHttpRequest request = ctx.getRequest();
final ServerHttpResponse response = ctx.getResponse();
final HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", "*");
headers.add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
headers.add("Access-Control-Max-Age", "3600");
headers.add("Access-Control-Allow-Headers", "Content-Type");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
};
}
}

View file

@ -15,6 +15,8 @@ public class LdapProperties {
private String userFilterSearchBase;
private String userFilterSearchFilter;
private String groupFilterSearchBase;
private String groupFilterSearchFilter;
private String groupRoleAttribute;
@Value("${oauth2.ldap.activeDirectory:false}")
private boolean isActiveDirectory;

View file

@ -3,14 +3,16 @@ package com.provectus.kafka.ui.config.auth;
import static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;
import com.provectus.kafka.ui.service.rbac.AccessControlService;
import com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -50,9 +52,9 @@ public class LdapSecurityConfig {
@Bean
public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
LdapAuthoritiesPopulator ldapAuthoritiesPopulator,
@Nullable AccessControlService acs) {
var rbacEnabled = acs != null && acs.isRbacEnabled();
LdapAuthoritiesPopulator authoritiesExtractor,
AccessControlService acs) {
var rbacEnabled = acs.isRbacEnabled();
BindAuthenticator ba = new BindAuthenticator(contextSource);
if (props.getBase() != null) {
ba.setUserDnPatterns(new String[] {props.getBase()});
@ -67,7 +69,7 @@ public class LdapSecurityConfig {
AbstractLdapAuthenticationProvider authenticationProvider;
if (!props.isActiveDirectory()) {
authenticationProvider = rbacEnabled
? new LdapAuthenticationProvider(ba, ldapAuthoritiesPopulator)
? new LdapAuthenticationProvider(ba, authoritiesExtractor)
: new LdapAuthenticationProvider(ba);
} else {
authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
@ -97,11 +99,24 @@ public class LdapSecurityConfig {
@Bean
@Primary
public LdapAuthoritiesPopulator ldapAuthoritiesPopulator(BaseLdapPathContextSource contextSource) {
var authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
authoritiesPopulator.setRolePrefix("");
authoritiesPopulator.setConvertToUpperCase(false);
return authoritiesPopulator;
public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context,
BaseLdapPathContextSource contextSource,
AccessControlService acs) {
var rbacEnabled = acs != null && acs.isRbacEnabled();
DefaultLdapAuthoritiesPopulator extractor;
if (rbacEnabled) {
extractor = new RbacLdapAuthoritiesExtractor(context, contextSource, props.getGroupFilterSearchBase());
} else {
extractor = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
}
Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter);
extractor.setRolePrefix("");
extractor.setConvertToUpperCase(false);
extractor.setSearchSubtree(true);
return extractor;
}
@Bean

View file

@ -1,5 +1,9 @@
package com.provectus.kafka.ui.controller;
import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART;
import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_ALL_TASKS;
import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_FAILED_TASKS;
import com.provectus.kafka.ui.api.KafkaConnectApi;
import com.provectus.kafka.ui.model.ConnectDTO;
import com.provectus.kafka.ui.model.ConnectorActionDTO;
@ -17,6 +21,7 @@ import com.provectus.kafka.ui.service.KafkaConnectService;
import com.provectus.kafka.ui.service.rbac.AccessControlService;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -30,6 +35,8 @@ import reactor.core.publisher.Mono;
@RequiredArgsConstructor
@Slf4j
public class KafkaConnectController extends AbstractController implements KafkaConnectApi {
private static final Set<ConnectorActionDTO> RESTART_ACTIONS
= Set.of(RESTART, RESTART_FAILED_TASKS, RESTART_ALL_TASKS);
private final KafkaConnectService kafkaConnectService;
private final AccessControlService accessControlService;
@ -172,10 +179,17 @@ public class KafkaConnectController extends AbstractController implements KafkaC
ConnectorActionDTO action,
ServerWebExchange exchange) {
ConnectAction[] connectActions;
if (RESTART_ACTIONS.contains(action)) {
connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.RESTART};
} else {
connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.EDIT};
}
Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
.cluster(clusterName)
.connect(connectName)
.connectActions(ConnectAction.VIEW, ConnectAction.EDIT)
.connectActions(connectActions)
.build());
return validateAccess.then(
@ -253,16 +267,11 @@ public class KafkaConnectController extends AbstractController implements KafkaC
if (orderBy == null) {
return defaultComparator;
}
switch (orderBy) {
case CONNECT:
return Comparator.comparing(FullConnectorInfoDTO::getConnect);
case TYPE:
return Comparator.comparing(FullConnectorInfoDTO::getType);
case STATUS:
return Comparator.comparing(fullConnectorInfoDTO -> fullConnectorInfoDTO.getStatus().getState());
case NAME:
default:
return defaultComparator;
}
return switch (orderBy) {
case CONNECT -> Comparator.comparing(FullConnectorInfoDTO::getConnect);
case TYPE -> Comparator.comparing(FullConnectorInfoDTO::getType);
case STATUS -> Comparator.comparing(fullConnectorInfoDTO -> fullConnectorInfoDTO.getStatus().getState());
default -> defaultComparator;
};
}
}

View file

@ -167,12 +167,13 @@ public class TopicsController extends AbstractController implements TopicsApi {
ServerWebExchange exchange) {
return topicsService.getTopicsForPagination(getCluster(clusterName))
.flatMap(existingTopics -> {
.flatMap(topics -> accessControlService.filterViewableTopics(topics, clusterName))
.flatMap(topics -> {
int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
var topicsToSkip = ((page != null && page > 0 ? page : 1) - 1) * pageSize;
var comparator = sortOrder == null || !sortOrder.equals(SortOrderDTO.DESC)
? getComparatorForTopic(orderBy) : getComparatorForTopic(orderBy).reversed();
List<InternalTopic> filtered = existingTopics.stream()
List<InternalTopic> filtered = topics.stream()
.filter(topic -> !topic.isInternal()
|| showInternal != null && showInternal)
.filter(topic -> search == null || StringUtils.containsIgnoreCase(topic.getName(), search))
@ -189,7 +190,6 @@ public class TopicsController extends AbstractController implements TopicsApi {
return topicsService.loadTopics(getCluster(clusterName), topicsPage)
.flatMapMany(Flux::fromIterable)
.filterWhen(dto -> accessControlService.isTopicAccessible(dto, clusterName))
.collectList()
.map(topicsToRender ->
new TopicsResponseDTO()

View file

@ -202,19 +202,23 @@ public class AccessControlService {
return isAccessible(Resource.TOPIC, context.getTopic(), user, context, requiredActions);
}
public Mono<Boolean> isTopicAccessible(InternalTopic dto, String clusterName) {
public Mono<List<InternalTopic>> filterViewableTopics(List<InternalTopic> topics, String clusterName) {
if (!rbacEnabled) {
return Mono.just(true);
return Mono.just(topics);
}
AccessContext accessContext = AccessContext
.builder()
.cluster(clusterName)
.topic(dto.getName())
.topicActions(TopicAction.VIEW)
.build();
return getUser().map(u -> isTopicAccessible(accessContext, u));
return getUser()
.map(user -> topics.stream()
.filter(topic -> {
var accessContext = AccessContext
.builder()
.cluster(clusterName)
.topic(topic.getName())
.topicActions(TopicAction.VIEW)
.build();
return isTopicAccessible(accessContext, user);
}
).toList());
}
private boolean isConsumerGroupAccessible(AccessContext context, AuthenticatedUser user) {

View file

@ -0,0 +1,78 @@
package com.provectus.kafka.ui.service.rbac.extractor;
import com.provectus.kafka.ui.config.auth.LdapProperties;
import com.provectus.kafka.ui.model.rbac.Role;
import com.provectus.kafka.ui.model.rbac.provider.Provider;
import com.provectus.kafka.ui.service.rbac.AccessControlService;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.util.Assert;
@Slf4j
public class RbacLdapAuthoritiesExtractor extends DefaultLdapAuthoritiesPopulator {
private final AccessControlService acs;
private final LdapProperties props;
public RbacLdapAuthoritiesExtractor(ApplicationContext context,
BaseLdapPathContextSource contextSource, String groupFilterSearchBase) {
super(contextSource, groupFilterSearchBase);
this.acs = context.getBean(AccessControlService.class);
this.props = context.getBean(LdapProperties.class);
}
@Override
protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {
var ldapGroups = getRoles(user.getNameInNamespace(), username);
return acs.getRoles()
.stream()
.filter(r -> r.getSubjects()
.stream()
.filter(subject -> subject.getProvider().equals(Provider.LDAP))
.filter(subject -> subject.getType().equals("group"))
.anyMatch(subject -> ldapGroups.contains(subject.getValue()))
)
.map(Role::getName)
.peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}
private Set<String> getRoles(String userDn, String username) {
var groupSearchBase = props.getGroupFilterSearchBase();
Assert.notNull(groupSearchBase, "groupSearchBase is empty");
var groupRoleAttribute = props.getGroupRoleAttribute();
if (groupRoleAttribute == null) {
groupRoleAttribute = "cn";
}
log.trace(
"Searching for roles for user [{}] with DN [{}], groupRoleAttribute [{}] and filter [{}] in search base [{}]",
username, userDn, groupRoleAttribute, getGroupSearchFilter(), groupSearchBase);
var ldapTemplate = getLdapTemplate();
ldapTemplate.setIgnoreNameNotFoundException(true);
Set<Map<String, List<String>>> userRoles = ldapTemplate.searchForMultipleAttributeValues(
groupSearchBase, getGroupSearchFilter(), new String[] {userDn, username},
new String[] {groupRoleAttribute});
return userRoles.stream()
.map(record -> record.get(getGroupRoleAttribute()).get(0))
.peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username))
.collect(Collectors.toSet());
}
}

View file

@ -3,7 +3,6 @@ package com.provectus.kafka.ui.util;
import com.google.common.annotations.VisibleForTesting;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@Slf4j
@ -31,7 +30,7 @@ public class GithubReleaseInfo {
@VisibleForTesting
GithubReleaseInfo(String url) {
this.refreshMono = WebClient.create()
this.refreshMono = new WebClientConfigurator().build()
.get()
.uri(url)
.exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class))

View file

@ -5,11 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.provectus.kafka.ui.config.ClustersProperties;
import com.provectus.kafka.ui.exception.ValidationException;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.util.function.Consumer;
@ -93,7 +90,12 @@ public class WebClientConfigurator {
// Create webclient
SslContext context = contextBuilder.build();
builder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().secure(t -> t.sslContext(context))));
var httpClient = HttpClient
.create()
.secure(t -> t.sslContext(context))
.proxyWithSystemProperties();
builder.clientConnector(new ReactorClientHttpConnector(httpClient));
return this;
}

View file

@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import com.provectus.kafka.ui.service.rbac.AccessControlService;
import java.util.Collections;
import org.mockito.Mockito;
import reactor.core.publisher.Mono;
@ -16,7 +17,7 @@ public class AccessControlServiceMock {
when(mock.validateAccess(any())).thenReturn(Mono.empty());
when(mock.isSchemaAccessible(anyString(), anyString())).thenReturn(Mono.just(true));
when(mock.isTopicAccessible(any(), anyString())).thenReturn(Mono.just(true));
when(mock.filterViewableTopics(any(), any())).then(invocation -> Mono.just(invocation.getArgument(0)));
return mock;
}

View file

@ -30,6 +30,7 @@ const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
networkMode: 'offlineFirst',
onError(error) {
showServerError(error as Response);
},

View file

@ -102,7 +102,7 @@ const Actions: React.FC = () => {
disabled={isMutating}
permission={{
resource: ResourceType.CONNECT,
action: Action.EDIT,
action: Action.RESTART,
value: routerProps.connectorName,
}}
>
@ -113,7 +113,7 @@ const Actions: React.FC = () => {
disabled={isMutating}
permission={{
resource: ResourceType.CONNECT,
action: Action.EDIT,
action: Action.RESTART,
value: routerProps.connectorName,
}}
>
@ -124,7 +124,7 @@ const Actions: React.FC = () => {
disabled={isMutating}
permission={{
resource: ResourceType.CONNECT,
action: Action.EDIT,
action: Action.RESTART,
value: routerProps.connectorName,
}}
>

View file

@ -1,9 +1,10 @@
import React from 'react';
import { Task } from 'generated-sources';
import { Action, ResourceType, Task } from 'generated-sources';
import { CellContext } from '@tanstack/react-table';
import useAppParams from 'lib/hooks/useAppParams';
import { useRestartConnectorTask } from 'lib/hooks/api/kafkaConnect';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
import { Dropdown } from 'components/common/Dropdown';
import { ActionDropdownItem } from 'components/common/ActionComponent';
import { RouterParamsClusterConnectConnector } from 'lib/paths';
const ActionsCellTasks: React.FC<CellContext<Task, unknown>> = ({ row }) => {
@ -18,13 +19,18 @@ const ActionsCellTasks: React.FC<CellContext<Task, unknown>> = ({ row }) => {
return (
<Dropdown>
<DropdownItem
<ActionDropdownItem
onClick={() => restartTaskHandler(id?.task)}
danger
confirm="Are you sure you want to restart the task?"
permission={{
resource: ResourceType.CONNECT,
action: Action.RESTART,
value: routerProps.connectorName,
}}
>
<span>Restart task</span>
</DropdownItem>
</ActionDropdownItem>
</Dropdown>
);
};

View file

@ -78,7 +78,7 @@ const ActionsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
disabled={isMutating}
permission={{
resource: ResourceType.CONNECT,
action: Action.EDIT,
action: Action.RESTART,
value: name,
}}
>
@ -89,7 +89,7 @@ const ActionsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
disabled={isMutating}
permission={{
resource: ResourceType.CONNECT,
action: Action.EDIT,
action: Action.RESTART,
value: name,
}}
>
@ -100,7 +100,7 @@ const ActionsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
disabled={isMutating}
permission={{
resource: ResourceType.CONNECT,
action: Action.EDIT,
action: Action.RESTART,
value: name,
}}
>

View file

@ -38,7 +38,7 @@ const New: React.FC = () => {
const { clusterName } = useAppParams<ClusterNameRoute>();
const navigate = useNavigate();
const { data: connects } = useConnects(clusterName);
const { data: connects = [] } = useConnects(clusterName);
const mutation = useCreateConnector(clusterName);
const methods = useForm<FormValues>({
@ -88,10 +88,6 @@ const New: React.FC = () => {
}
};
if (!connects || connects.length === 0) {
return null;
}
const connectOptions = connects.map(({ name: connectName }) => ({
value: connectName,
label: connectName,
@ -108,10 +104,10 @@ const New: React.FC = () => {
onSubmit={handleSubmit(onSubmit)}
aria-label="Create connect form"
>
<S.Filed $hidden={connects.length <= 1}>
<S.Filed $hidden={connects?.length <= 1}>
<Heading level={3}>Connect *</Heading>
<Controller
defaultValue={connectOptions[0].value}
defaultValue={connectOptions[0]?.value}
control={control}
name="connectName"
render={({ field: { name, onChange } }) => (
@ -120,7 +116,7 @@ const New: React.FC = () => {
name={name}
disabled={isSubmitting}
onChange={onChange}
value={connectOptions[0].value}
value={connectOptions[0]?.value}
minWidth="100%"
options={connectOptions}
/>

View file

@ -11,7 +11,8 @@ const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
const { name } = row.original;
const { data } = useGetUserInfo();
const isApplicationConfig = useMemo(() => {
const hasPermissions = useMemo(() => {
if (!data?.rbacEnabled) return true;
return !!data?.userInfo?.permissions.some(
(permission) => permission.resource === ResourceType.APPLICATIONCONFIG
);
@ -22,7 +23,7 @@ const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
buttonType="secondary"
buttonSize="S"
to={clusterConfigPath(name)}
canDoAction={isApplicationConfig}
canDoAction={hasPermissions}
>
Configure
</ActionCanButton>

View file

@ -57,7 +57,8 @@ const Dashboard: React.FC = () => {
return initialColumns;
}, []);
const isApplicationConfig = useMemo(() => {
const hasPermissions = useMemo(() => {
if (!data?.rbacEnabled) return true;
return !!data?.userInfo?.permissions.some(
(permission) => permission.resource === ResourceType.APPLICATIONCONFIG
);
@ -91,7 +92,7 @@ const Dashboard: React.FC = () => {
buttonType="primary"
buttonSize="M"
to={clusterNewConfigPath}
canDoAction={isApplicationConfig}
canDoAction={hasPermissions}
>
Configure new cluster
</ActionCanButton>

View file

@ -185,6 +185,7 @@ const Filters: React.FC<FiltersProps> = ({
const handleClearAllFilters = () => {
setCurrentSeekType(SeekType.OFFSET);
setOffset('');
setTimestamp(null);
setQuery('');
changeSeekDirection(SeekDirection.FORWARD);
getSelectedPartitionsFromSeekToParam(searchParams, partitions);

View file

@ -8,14 +8,22 @@ import * as S from './Version.styled';
const Version: React.FC = () => {
const { data: latestVersionInfo = {} } = useLatestVersion();
const { buildTime, commitId, isLatestRelease } = latestVersionInfo.build;
const { buildTime, commitId, isLatestRelease, version } =
latestVersionInfo.build;
const { versionTag } = latestVersionInfo?.latestRelease || '';
const currentVersion =
isLatestRelease && version?.match(versionTag)
? versionTag
: formatTimestamp(buildTime);
return (
<S.Wrapper>
{!isLatestRelease && (
<S.OutdatedWarning
title={`Your app version is outdated. Current latest version is ${versionTag}`}
title={`Your app version is outdated. Latest version is ${
versionTag || 'UNKNOWN'
}`}
>
<WarningIcon />
</S.OutdatedWarning>
@ -32,7 +40,7 @@ const Version: React.FC = () => {
</S.CurrentCommitLink>
</div>
)}
<S.CurrentVersion>{formatTimestamp(buildTime)}</S.CurrentVersion>
<S.CurrentVersion>{currentVersion}</S.CurrentVersion>
</S.Wrapper>
);
};

View file

@ -304,6 +304,11 @@ export function useTopicAnalysis(
useErrorBoundary: true,
retry: false,
suspense: false,
onError: (error: Response) => {
if (error.status !== 404) {
showServerError(error as Response);
}
},
}
);
}

View file

@ -36,7 +36,7 @@
<protobuf-java.version>3.21.9</protobuf-java.version>
<scala-lang.library.version>2.13.9</scala-lang.library.version>
<snakeyaml.version>2.0</snakeyaml.version>
<spring-boot.version>3.0.5</spring-boot.version>
<spring-boot.version>3.0.6</spring-boot.version>
<kafka-ui-serde-api.version>1.0.0</kafka-ui-serde-api.version>
<odd-oddrn-generator.version>0.1.15</odd-oddrn-generator.version>
<odd-oddrn-client.version>0.1.23</odd-oddrn-client.version>