OpenAPI integration for kafka-ui frontend (#120)

* Added react openapi gen

* Integrated Openapi to React

Co-authored-by: German Osin <german.osin@gmail.com>
Co-authored-by: Sofia Shnaidman <sshnaidman@provectus.com>
This commit is contained in:
soffest 2020-11-20 14:16:27 +03:00 committed by GitHub
parent 9fd3697062
commit 494443bb08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 4090 additions and 4416 deletions

1
.gitignore vendored
View file

@ -32,3 +32,4 @@ build/
/kafka-ui-api/app/node /kafka-ui-api/app/node
.DS_Store .DS_Store
*.code-workspace

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>kafka-ui</artifactId> <artifactId>kafka-ui</artifactId>
<groupId>com.provectus</groupId> <groupId>com.provectus</groupId>
@ -70,8 +69,57 @@
</configOptions> </configOptions>
</configuration> </configuration>
</execution> </execution>
<execution>
<id>generate-frontend-api</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/swagger/kafka-ui-api.yaml
</inputSpec>
<output>${project.build.directory}/generated-sources/frontend/</output>
<generatorName>typescript-fetch</generatorName>
<configOptions>
<modelPackage>com.provectus.kafka.ui.model</modelPackage>
<apiPackage>com.provectus.kafka.ui.api</apiPackage>
<apiPackage>com.provectus.kafka.ui.invoker</apiPackage>
<sourceFolder>kafka-ui-contract</sourceFolder>
<typescriptThreePlus>true</typescriptThreePlus>
<supportsES6>true</supportsES6>
<nullSafeAdditionalProps>true</nullSafeAdditionalProps>
<withInterfaces>true</withInterfaces>
</configOptions>
</configuration>
</execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-resource-one</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/..//kafka-ui-react-app/src/generated-sources</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}/generated-sources/frontend/</directory>
<includes>
<include>**/*.ts</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
</profile> </profile>

View file

@ -1,2 +1,2 @@
# Kafka REST API # Kafka REST API
REACT_APP_API_URL=/api REACT_APP_API_URL=

View file

@ -24,3 +24,6 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
.idea .idea
# generated sources
src/generated-sources

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,21 @@
import React from 'react'; import React from 'react';
import { ClusterName, BrokerMetrics, ZooKeeperStatus } from 'redux/interfaces'; import { ClusterName, ZooKeeperStatus } from 'redux/interfaces';
import { ClusterStats } from 'generated-sources';
import useInterval from 'lib/hooks/useInterval'; import useInterval from 'lib/hooks/useInterval';
import cx from 'classnames'; import cx from 'classnames';
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper'; import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
import Indicator from 'components/common/Dashboard/Indicator'; import Indicator from 'components/common/Dashboard/Indicator';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
interface Props extends BrokerMetrics { interface Props extends ClusterStats {
clusterName: ClusterName; clusterName: ClusterName;
isFetched: boolean; isFetched: boolean;
fetchClusterStats: (clusterName: ClusterName) => void;
fetchBrokers: (clusterName: ClusterName) => void; fetchBrokers: (clusterName: ClusterName) => void;
fetchBrokerMetrics: (clusterName: ClusterName) => void;
} }
const Topics: React.FC<Props> = ({ const Topics: React.FC<Props> = ({
clusterName, clusterName,
isFetched,
brokerCount, brokerCount,
activeControllers, activeControllers,
zooKeeperStatus, zooKeeperStatus,
@ -24,18 +24,18 @@ const Topics: React.FC<Props> = ({
inSyncReplicasCount, inSyncReplicasCount,
outOfSyncReplicasCount, outOfSyncReplicasCount,
underReplicatedPartitionCount, underReplicatedPartitionCount,
fetchClusterStats,
fetchBrokers, fetchBrokers,
fetchBrokerMetrics,
}) => { }) => {
React.useEffect( React.useEffect(
() => { () => {
fetchClusterStats(clusterName);
fetchBrokers(clusterName); fetchBrokers(clusterName);
fetchBrokerMetrics(clusterName);
}, },
[fetchBrokers, fetchBrokerMetrics, clusterName], [fetchClusterStats, fetchBrokers, clusterName],
); );
useInterval(() => { fetchBrokerMetrics(clusterName); }, 5000); useInterval(() => { fetchClusterStats(clusterName); }, 5000);
const zkOnline = zooKeeperStatus === ZooKeeperStatus.online; const zkOnline = zooKeeperStatus === ZooKeeperStatus.online;
@ -62,7 +62,7 @@ const Topics: React.FC<Props> = ({
<span className={cx({'has-text-danger': offlinePartitionCount !== 0})}> <span className={cx({'has-text-danger': offlinePartitionCount !== 0})}>
{onlinePartitionCount} {onlinePartitionCount}
</span> </span>
<span className="subtitle has-text-weight-light"> of {onlinePartitionCount + offlinePartitionCount}</span> <span className="subtitle has-text-weight-light"> of {(onlinePartitionCount || 0) + (offlinePartitionCount || 0)}</span>
</Indicator> </Indicator>
<Indicator label="URP" title="Under replicated partitions"> <Indicator label="URP" title="Under replicated partitions">
{underReplicatedPartitionCount} {underReplicatedPartitionCount}

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
fetchClusterStats,
fetchBrokers, fetchBrokers,
fetchBrokerMetrics,
} from 'redux/actions'; } from 'redux/actions';
import Brokers from './Brokers'; import Brokers from './Brokers';
import * as brokerSelectors from 'redux/reducers/brokers/selectors'; import * as brokerSelectors from 'redux/reducers/brokers/selectors';
@ -28,8 +28,8 @@ const mapStateToProps = (state: RootState, { match: { params: { clusterName } }}
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
fetchClusterStats: (clusterName: ClusterName) => fetchClusterStats(clusterName),
fetchBrokers: (clusterName: ClusterName) => fetchBrokers(clusterName), fetchBrokers: (clusterName: ClusterName) => fetchBrokers(clusterName),
fetchBrokerMetrics: (clusterName: ClusterName) => fetchBrokerMetrics(clusterName),
}; };
export default connect(mapStateToProps, mapDispatchToProps)(Brokers); export default connect(mapStateToProps, mapDispatchToProps)(Brokers);

View file

@ -2,12 +2,12 @@ import React from 'react';
import { ClusterName } from 'redux/interfaces'; import { ClusterName } from 'redux/interfaces';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { clusterConsumerGroupsPath } from 'lib/paths'; import { clusterConsumerGroupsPath } from 'lib/paths';
import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
import { import {
ConsumerGroupID,
ConsumerGroup, ConsumerGroup,
ConsumerGroupDetails, ConsumerGroupDetails,
Consumer, ConsumerTopicPartitionDetail,
} from 'redux/interfaces/consumerGroup'; } from 'generated-sources';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import ListItem from './ListItem'; import ListItem from './ListItem';
@ -15,7 +15,7 @@ import ListItem from './ListItem';
interface Props extends ConsumerGroup, ConsumerGroupDetails { interface Props extends ConsumerGroup, ConsumerGroupDetails {
clusterName: ClusterName; clusterName: ClusterName;
consumerGroupID: ConsumerGroupID; consumerGroupID: ConsumerGroupID;
consumers: Consumer[]; consumers?: ConsumerTopicPartitionDetail[];
isFetched: boolean; isFetched: boolean;
fetchConsumerGroupDetails: ( fetchConsumerGroupDetails: (
clusterName: ClusterName, clusterName: ClusterName,

View file

@ -8,7 +8,7 @@ import { fetchConsumerGroupDetails } from 'redux/actions/thunks';
interface RouteProps { interface RouteProps {
clusterName: ClusterName; clusterName: ClusterName;
consumerGroupID: string; consumerGroupID: ConsumerGroupID;
} }
interface OwnProps extends RouteComponentProps<RouteProps> { } interface OwnProps extends RouteComponentProps<RouteProps> { }

View file

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { Consumer } from 'redux/interfaces/consumerGroup'; import { ConsumerTopicPartitionDetail } from 'generated-sources';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { ClusterName } from 'redux/interfaces/cluster'; import { ClusterName } from 'redux/interfaces/cluster';
interface Props { interface Props {
clusterName: ClusterName; clusterName: ClusterName;
consumer: Consumer; consumer: ConsumerTopicPartitionDetail;
} }
const ListItem: React.FC<Props> = ({ clusterName, consumer }) => { const ListItem: React.FC<Props> = ({ clusterName, consumer }) => {

View file

@ -1,5 +1,10 @@
import React from 'react'; import React from 'react';
import { ClusterName, ConsumerGroup } from 'redux/interfaces'; import {
ClusterName
} from 'redux/interfaces';
import {
ConsumerGroup
} from 'generated-sources';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import ListItem from './ListItem'; import ListItem from './ListItem';

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { ConsumerGroup } from 'redux/interfaces'; import { ConsumerGroup } from 'generated-sources';
const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({ const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({
consumerGroup, consumerGroup,

View file

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Cluster, ClusterStatus } from 'redux/interfaces';
import formatBytes from 'lib/utils/formatBytes'; import formatBytes from 'lib/utils/formatBytes';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { clusterBrokersPath } from 'lib/paths'; import { clusterBrokersPath } from 'lib/paths';
import { Cluster, ServerStatus } from 'generated-sources';
const ClusterWidget: React.FC<Cluster> = ({ const ClusterWidget: React.FC<Cluster> = ({
name, name,
@ -20,7 +20,7 @@ const ClusterWidget: React.FC<Cluster> = ({
title={name} title={name}
> >
<div <div
className={`tag has-margin-right ${status === ClusterStatus.Online ? 'is-primary' : 'is-danger'}`} className={`tag has-margin-right ${status === ServerStatus.Online ? 'is-primary' : 'is-danger'}`}
> >
{status} {status}
</div> </div>
@ -43,11 +43,11 @@ const ClusterWidget: React.FC<Cluster> = ({
</tr> </tr>
<tr> <tr>
<th>Production</th> <th>Production</th>
<td>{formatBytes(bytesInPerSec)}</td> <td>{formatBytes(bytesInPerSec || 0)}</td>
</tr> </tr>
<tr> <tr>
<th>Consumption</th> <th>Consumption</th>
<td>{formatBytes(bytesOutPerSec)}</td> <td>{formatBytes(bytesOutPerSec || 0)}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import { chunk } from 'lodash'; import { chunk } from 'lodash';
import { Cluster } from 'redux/interfaces';
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper'; import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
import Indicator from 'components/common/Dashboard/Indicator'; import Indicator from 'components/common/Dashboard/Indicator';
import ClusterWidget from './ClusterWidget'; import ClusterWidget from './ClusterWidget';
import { Cluster } from 'generated-sources';
interface Props { interface Props {
clusters: Cluster[]; clusters: Cluster[];

View file

@ -1,11 +1,11 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { Cluster, ClusterStatus } from 'redux/interfaces';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { import {
clusterBrokersPath, clusterBrokersPath,
clusterTopicsPath, clusterTopicsPath,
clusterConsumerGroupsPath, clusterConsumerGroupsPath,
} from 'lib/paths'; } from 'lib/paths';
import { Cluster, ServerStatus } from 'generated-sources';
interface Props { interface Props {
cluster: Cluster; cluster: Cluster;
@ -42,7 +42,7 @@ const StatusIcon: React.FC<Props> = ({ cluster }) => {
return ( return (
<span <span
className={`tag ${ className={`tag ${
cluster.status === ClusterStatus.Online ? 'is-primary' : 'is-danger' cluster.status === ServerStatus.Online ? 'is-primary' : 'is-danger'
}`} }`}
title={cluster.status} title={cluster.status}
style={style} style={style}

View file

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Cluster } from 'redux/interfaces';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import cx from 'classnames'; import cx from 'classnames';
import ClusterMenu from './ClusterMenu'; import ClusterMenu from './ClusterMenu';
import { Cluster } from 'generated-sources';
interface Props { interface Props {
isClusterListFetched: boolean; isClusterListFetched: boolean;
@ -29,7 +29,7 @@ const Nav: React.FC<Props> = ({
{isClusterListFetched && {isClusterListFetched &&
clusters.map((cluster) => ( clusters.map((cluster) => (
<ClusterMenu cluster={cluster} key={cluster.id} /> <ClusterMenu cluster={cluster} key={cluster.name} />
))} ))}
</aside> </aside>
); );

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { ClusterName, Topic, TopicDetails, TopicName } from 'redux/interfaces'; import { ClusterName, TopicName } from 'redux/interfaces';
import { Topic, TopicDetails } from 'generated-sources';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { NavLink, Switch, Route } from 'react-router-dom'; import { NavLink, Switch, Route } from 'react-router-dom';
import { import {

View file

@ -1,11 +1,15 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Details from './Details'; import Details from './Details';
import {ClusterName, RootState} from 'redux/interfaces'; import {
ClusterName,
RootState,
TopicName
} from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withRouter, RouteComponentProps } from 'react-router-dom';
interface RouteProps { interface RouteProps {
clusterName: ClusterName; clusterName: ClusterName;
topicName: string; topicName: TopicName;
} }
interface OwnProps extends RouteComponentProps<RouteProps> { } interface OwnProps extends RouteComponentProps<RouteProps> { }

View file

@ -1,13 +1,14 @@
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { import {
ClusterName, ClusterName,
SeekType,
SeekTypes,
TopicMessage,
TopicMessageQueryParams, TopicMessageQueryParams,
TopicName, TopicName
TopicPartition,
} from 'redux/interfaces'; } from 'redux/interfaces';
import {
TopicMessage,
Partition,
SeekType
} from 'generated-sources';
import PageLoader from 'components/common/PageLoader/PageLoader'; import PageLoader from 'components/common/PageLoader/PageLoader';
import { format } from 'date-fns'; import { format } from 'date-fns';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
@ -33,12 +34,12 @@ interface Props {
queryParams: Partial<TopicMessageQueryParams> queryParams: Partial<TopicMessageQueryParams>
) => void; ) => void;
messages: TopicMessage[]; messages: TopicMessage[];
partitions: TopicPartition[]; partitions: Partition[];
} }
interface FilterProps { interface FilterProps {
offset: number; offset: TopicMessage['offset'];
partition: number; partition: TopicMessage['partition'];
} }
function usePrevious(value: any) { function usePrevious(value: any) {
@ -63,7 +64,7 @@ const Messages: React.FC<Props> = ({
); );
const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]); const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]);
const [selectedSeekType, setSelectedSeekType] = React.useState<SeekType>( const [selectedSeekType, setSelectedSeekType] = React.useState<SeekType>(
SeekTypes.OFFSET SeekType.OFFSET
); );
const [searchOffset, setSearchOffset] = React.useState<string>('0'); const [searchOffset, setSearchOffset] = React.useState<string>('0');
const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>( const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(
@ -105,7 +106,7 @@ const Messages: React.FC<Props> = ({
const foundedValues = filterProps.find( const foundedValues = filterProps.find(
(prop) => prop.partition === partition.value (prop) => prop.partition === partition.value
); );
if (selectedSeekType === SeekTypes.OFFSET) { if (selectedSeekType === SeekType.OFFSET) {
return foundedValues ? foundedValues.offset : 0; return foundedValues ? foundedValues.offset : 0;
} }
return searchTimestamp ? searchTimestamp.getTime() : null; return searchTimestamp ? searchTimestamp.getTime() : null;
@ -134,7 +135,7 @@ const Messages: React.FC<Props> = ({
setSearchTimestamp(searchTimestamp); setSearchTimestamp(searchTimestamp);
setQueryParams({ setQueryParams({
...queryParams, ...queryParams,
seekType: SeekTypes.TIMESTAMP, seekType: SeekType.TIMESTAMP,
seekTo: selectedPartitions.map((p) => `${p.value}::${timestamp}`), seekTo: selectedPartitions.map((p) => `${p.value}::${timestamp}`),
}); });
} else { } else {
@ -155,7 +156,7 @@ const Messages: React.FC<Props> = ({
const offset = event.target.value || '0'; const offset = event.target.value || '0';
setSearchOffset(offset); setSearchOffset(offset);
debouncedCallback({ debouncedCallback({
seekType: SeekTypes.OFFSET, seekType: SeekType.OFFSET,
seekTo: selectedPartitions.map((p) => `${p.value}::${offset}`), seekTo: selectedPartitions.map((p) => `${p.value}::${offset}`),
}); });
}; };
@ -176,9 +177,9 @@ const Messages: React.FC<Props> = ({
fetchTopicMessages(clusterName, topicName, queryParams); fetchTopicMessages(clusterName, topicName, queryParams);
}, [clusterName, topicName, queryParams]); }, [clusterName, topicName, queryParams]);
const getTimestampDate = (timestamp: string) => { const getFormattedDate = (date: Date) => {
if (!Date.parse(timestamp)) return; if (!date) return null;
return format(Date.parse(timestamp), 'yyyy-MM-dd HH:mm:ss'); return format(date, 'yyyy-MM-dd HH:mm:ss');
}; };
const getMessageContentBody = (content: any) => { const getMessageContentBody = (content: any) => {
@ -224,7 +225,7 @@ const Messages: React.FC<Props> = ({
setQueryParams({ setQueryParams({
...queryParams, ...queryParams,
seekType: SeekTypes.OFFSET, seekType: SeekType.OFFSET,
seekTo, seekTo,
}); });
fetchTopicMessages(clusterName, topicName, queryParams); fetchTopicMessages(clusterName, topicName, queryParams);
@ -255,7 +256,7 @@ const Messages: React.FC<Props> = ({
{messages.map((message) => ( {messages.map((message) => (
<tr key={`${message.timestamp}${Math.random()}`}> <tr key={`${message.timestamp}${Math.random()}`}>
<td style={{ width: 200 }}> <td style={{ width: 200 }}>
{getTimestampDate(message.timestamp)} {getFormattedDate(message.timestamp)}
</td> </td>
<td style={{ width: 150 }}>{message.offset}</td> <td style={{ width: 150 }}>{message.offset}</td>
<td style={{ width: 100 }}>{message.partition}</td> <td style={{ width: 100 }}>{message.partition}</td>
@ -309,16 +310,16 @@ const Messages: React.FC<Props> = ({
id="selectSeekType" id="selectSeekType"
name="selectSeekType" name="selectSeekType"
onChange={handleSeekTypeChange} onChange={handleSeekTypeChange}
defaultValue={SeekTypes.OFFSET} defaultValue={SeekType.OFFSET}
value={selectedSeekType} value={selectedSeekType}
> >
<option value={SeekTypes.OFFSET}>Offset</option> <option value={SeekType.OFFSET}>Offset</option>
<option value={SeekTypes.TIMESTAMP}>Timestamp</option> <option value={SeekType.TIMESTAMP}>Timestamp</option>
</select> </select>
</div> </div>
</div> </div>
<div className="column is-one-fifth"> <div className="column is-one-fifth">
{selectedSeekType === SeekTypes.OFFSET ? ( {selectedSeekType === SeekType.OFFSET ? (
<> <>
<label className="label">Offset</label> <label className="label">Offset</label>
<input <input
@ -335,7 +336,7 @@ const Messages: React.FC<Props> = ({
<label className="label">Timestamp</label> <label className="label">Timestamp</label>
<DatePicker <DatePicker
selected={searchTimestamp} selected={searchTimestamp}
onChange={(date) => setSearchTimestamp(date)} onChange={(date: Date | null) => setSearchTimestamp(date)}
onCalendarClose={handleDateTimeChange} onCalendarClose={handleDateTimeChange}
showTimeInput showTimeInput
timeInputLabel="Time:" timeInputLabel="Time:"

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { ClusterName, Topic, TopicDetails, TopicName } from 'redux/interfaces'; import { ClusterName, TopicName } from 'redux/interfaces';
import { Topic, TopicDetails } from 'generated-sources';
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper'; import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
import Indicator from 'components/common/Dashboard/Indicator'; import Indicator from 'components/common/Dashboard/Indicator';

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { ClusterName, TopicName, TopicConfig } from 'redux/interfaces'; import { ClusterName, TopicName } from 'redux/interfaces';
import { TopicConfig } from 'generated-sources';
interface Props { interface Props {
clusterName: ClusterName; clusterName: ClusterName;

View file

@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import { import {
ClusterName, ClusterName,
TopicFormData, TopicFormDataRaw,
TopicName, TopicName,
TopicConfigByName, TopicConfigByName,
TopicWithDetailedInfo, TopicWithDetailedInfo,
CleanupPolicy, CleanupPolicy,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { TopicConfig } from 'generated-sources';
import { useForm, FormContext } from 'react-hook-form'; import { useForm, FormContext } from 'react-hook-form';
import { camelCase } from 'lodash'; import { camelCase } from 'lodash';
@ -22,7 +23,7 @@ interface Props {
isTopicUpdated: boolean; isTopicUpdated: boolean;
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void; fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void; fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
updateTopic: (clusterName: ClusterName, form: TopicFormData) => void; updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void; redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
resetUploadedState: () => void; resetUploadedState: () => void;
} }
@ -44,7 +45,7 @@ const topicParams = (topic: TopicWithDetailedInfo | undefined) => {
const { name, replicationFactor } = topic; const { name, replicationFactor } = topic;
const configs = topic.config?.reduce( const configs = topic.config?.reduce(
(result: { [name: string]: string }, param) => { (result: { [key: string]: TopicConfig['value'] }, param) => {
result[camelCase(param.name)] = param.value || param.defaultValue; result[camelCase(param.name)] = param.value || param.defaultValue;
return result; return result;
}, },
@ -76,7 +77,7 @@ const Edit: React.FC<Props> = ({
}) => { }) => {
const defaultValues = topicParams(topic); const defaultValues = topicParams(topic);
const methods = useForm<TopicFormData>({ defaultValues }); const methods = useForm<TopicFormDataRaw>({ defaultValues });
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false); const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
@ -109,7 +110,7 @@ const Edit: React.FC<Props> = ({
config.byName[param.name] = param; config.byName[param.name] = param;
}); });
const onSubmit = async (data: TopicFormData) => { const onSubmit = async (data: TopicFormDataRaw) => {
updateTopic(clusterName, data); updateTopic(clusterName, data);
setIsSubmitting(true); // Keep this action after updateTopic to prevent redirect before update. setIsSubmitting(true); // Keep this action after updateTopic to prevent redirect before update.
}; };

View file

@ -2,7 +2,6 @@ import { connect } from 'react-redux';
import { import {
RootState, RootState,
ClusterName, ClusterName,
TopicFormData,
TopicName, TopicName,
Action, Action,
} from 'redux/interfaces'; } from 'redux/interfaces';
@ -21,6 +20,7 @@ import {
import { clusterTopicPath } from 'lib/paths'; import { clusterTopicPath } from 'lib/paths';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import Edit from './Edit'; import Edit from './Edit';
import { TopicFormDataRaw } from 'redux/interfaces';
interface RouteProps { interface RouteProps {
clusterName: ClusterName; clusterName: ClusterName;
@ -53,7 +53,7 @@ const mapDispatchToProps = (
dispatch(fetchTopicDetails(clusterName, topicName)), dispatch(fetchTopicDetails(clusterName, topicName)),
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) =>
dispatch(fetchTopicConfig(clusterName, topicName)), dispatch(fetchTopicConfig(clusterName, topicName)),
updateTopic: (clusterName: ClusterName, form: TopicFormData) => updateTopic: (clusterName: ClusterName, form: TopicFormDataRaw) =>
dispatch(updateTopic(clusterName, form)), dispatch(updateTopic(clusterName, form)),
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => { redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => {
history.push(clusterTopicPath(clusterName, topicName)); history.push(clusterTopicPath(clusterName, topicName));

View file

@ -14,8 +14,8 @@ const ListItem: React.FC<TopicWithDetailedInfo> = ({
} }
return partitions.reduce((memo: number, { replicas }) => { return partitions.reduce((memo: number, { replicas }) => {
const outOfSync = replicas.filter(({ inSync }) => !inSync) const outOfSync = replicas?.filter(({ inSync }) => !inSync)
return memo + outOfSync.length; return memo + (outOfSync?.length || 0);
}, 0); }, 0);
}, [partitions]) }, [partitions])
@ -26,7 +26,7 @@ const ListItem: React.FC<TopicWithDetailedInfo> = ({
{name} {name}
</NavLink> </NavLink>
</td> </td>
<td>{partitions.length}</td> <td>{partitions?.length}</td>
<td>{outOfSyncReplicas}</td> <td>{outOfSyncReplicas}</td>
<td> <td>
<div className={cx('tag is-small', internal ? 'is-light' : 'is-success')}> <div className={cx('tag is-small', internal ? 'is-light' : 'is-success')}>

View file

@ -1,15 +1,16 @@
import React from 'react'; import React from 'react';
import { ClusterName, TopicFormData, TopicName } from 'redux/interfaces'; import { ClusterName, TopicName } from 'redux/interfaces';
import { useForm, FormContext } from 'react-hook-form'; import { useForm, FormContext } from 'react-hook-form';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { clusterTopicsPath } from 'lib/paths'; import { clusterTopicsPath } from 'lib/paths';
import TopicForm from 'components/Topics/shared/Form/TopicForm'; import TopicForm from 'components/Topics/shared/Form/TopicForm';
import { TopicFormDataRaw } from 'redux/interfaces';
interface Props { interface Props {
clusterName: ClusterName; clusterName: ClusterName;
isTopicCreated: boolean; isTopicCreated: boolean;
createTopic: (clusterName: ClusterName, form: TopicFormData) => void; createTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void; redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
resetUploadedState: () => void; resetUploadedState: () => void;
} }
@ -21,7 +22,7 @@ const New: React.FC<Props> = ({
redirectToTopicPath, redirectToTopicPath,
resetUploadedState, resetUploadedState,
}) => { }) => {
const methods = useForm<TopicFormData>(); const methods = useForm<TopicFormDataRaw>();
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false); const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
React.useEffect(() => { React.useEffect(() => {
@ -31,7 +32,7 @@ const New: React.FC<Props> = ({
} }
}, [isSubmitting, isTopicCreated, redirectToTopicPath, clusterName, methods]); }, [isSubmitting, isTopicCreated, redirectToTopicPath, clusterName, methods]);
const onSubmit = async (data: TopicFormData) => { const onSubmit = async (data: TopicFormDataRaw) => {
// TODO: need to fix loader. After success loading the first time, we won't wait for creation any more, because state is // TODO: need to fix loader. After success loading the first time, we won't wait for creation any more, because state is
// loaded, and we will try to get entity immediately after pressing the button, and we will receive null // loaded, and we will try to get entity immediately after pressing the button, and we will receive null
// going to object page on the second creation. Setting of isSubmitting after createTopic is a workaround, need to tweak loader logic // going to object page on the second creation. Setting of isSubmitting after createTopic is a workaround, need to tweak loader logic

View file

@ -1,35 +1,48 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RootState, ClusterName, TopicFormData, TopicName, Action } from 'redux/interfaces'; import {
import New from './New'; RootState,
ClusterName,
TopicName,
Action,
TopicFormDataRaw,
} from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withRouter, RouteComponentProps } from 'react-router-dom';
import { createTopic } from 'redux/actions'; import { createTopic } from 'redux/actions';
import { getTopicCreated } from 'redux/reducers/topics/selectors'; import { getTopicCreated } from 'redux/reducers/topics/selectors';
import { clusterTopicPath } from 'lib/paths'; import { clusterTopicPath } from 'lib/paths';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import * as actions from "../../../redux/actions/actions"; import * as actions from 'redux/actions';
import New from './New';
interface RouteProps { interface RouteProps {
clusterName: ClusterName; clusterName: ClusterName;
} }
interface OwnProps extends RouteComponentProps<RouteProps> { } type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (state: RootState, { match: { params: { clusterName } } }: OwnProps) => ({ const mapStateToProps = (
state: RootState,
{
match: {
params: { clusterName },
},
}: OwnProps
) => ({
clusterName, clusterName,
isTopicCreated: getTopicCreated(state), isTopicCreated: getTopicCreated(state),
}); });
const mapDispatchToProps = (dispatch: ThunkDispatch<RootState, undefined, Action>, { history }: OwnProps) => ({ const mapDispatchToProps = (
createTopic: (clusterName: ClusterName, form: TopicFormData) => { dispatch: ThunkDispatch<RootState, undefined, Action>,
{ history }: OwnProps
) => ({
createTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => {
dispatch(createTopic(clusterName, form)); dispatch(createTopic(clusterName, form));
}, },
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => { redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => {
history.push(clusterTopicPath(clusterName, topicName)); history.push(clusterTopicPath(clusterName, topicName));
}, },
resetUploadedState: (() => dispatch(actions.createTopicAction.failure())) resetUploadedState: () => dispatch(actions.createTopicAction.failure()),
}); });
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(New));
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(New)
);

View file

@ -11,17 +11,17 @@ interface Props {
clusterName: ClusterName; clusterName: ClusterName;
isFetched: boolean; isFetched: boolean;
fetchBrokers: (clusterName: ClusterName) => void; fetchBrokers: (clusterName: ClusterName) => void;
fetchTopicList: (clusterName: ClusterName) => void; fetchTopicsList: (clusterName: ClusterName) => void;
} }
const Topics: React.FC<Props> = ({ const Topics: React.FC<Props> = ({
clusterName, clusterName,
isFetched, isFetched,
fetchTopicList, fetchTopicsList,
}) => { }) => {
React.useEffect(() => { React.useEffect(() => {
fetchTopicList(clusterName); fetchTopicsList(clusterName);
}, [fetchTopicList, clusterName]); }, [fetchTopicsList, clusterName]);
if (isFetched) { if (isFetched) {
return ( return (

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchTopicList } from 'redux/actions'; import { fetchTopicsList } from 'redux/actions';
import Topics from './Topics'; import Topics from './Topics';
import { getIsTopicListFetched } from 'redux/reducers/topics/selectors'; import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
import { RootState, ClusterName } from 'redux/interfaces'; import { RootState, ClusterName } from 'redux/interfaces';
@ -17,7 +17,7 @@ const mapStateToProps = (state: RootState, { match: { params: { clusterName } }}
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
fetchTopicList: (clusterName: ClusterName) => fetchTopicList(clusterName), fetchTopicsList: (clusterName: ClusterName) => fetchTopicsList(clusterName),
}; };
export default connect(mapStateToProps, mapDispatchToProps)(Topics); export default connect(mapStateToProps, mapDispatchToProps)(Topics);

View file

@ -2,13 +2,14 @@ import React from 'react';
import CustomParamSelect from 'components/Topics/shared/Form/CustomParams/CustomParamSelect'; import CustomParamSelect from 'components/Topics/shared/Form/CustomParams/CustomParamSelect';
import CustomParamValue from 'components/Topics/shared/Form/CustomParams/CustomParamValue'; import CustomParamValue from 'components/Topics/shared/Form/CustomParams/CustomParamValue';
import CustomParamAction from 'components/Topics/shared/Form/CustomParams/CustomParamAction'; import CustomParamAction from 'components/Topics/shared/Form/CustomParams/CustomParamAction';
import { TopicConfig } from 'generated-sources';
interface Props { interface Props {
isDisabled: boolean; isDisabled: boolean;
index: string; index: string;
name: string; name: TopicConfig['name'];
existingFields: string[]; existingFields: string[];
defaultValue: string; defaultValue: TopicConfig['defaultValue'];
onNameChange: (inputName: string, name: string) => void; onNameChange: (inputName: string, name: string) => void;
onRemove: (index: string) => void; onRemove: (index: string) => void;
} }

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { TopicCustomParamOption } from 'redux/interfaces'; import { TopicConfigOption } from 'redux/interfaces';
import { omitBy } from 'lodash'; import { omitBy } from 'lodash';
import CUSTOM_PARAMS_OPTIONS from './customParamsOptions'; import CUSTOM_PARAMS_OPTIONS from './customParamsOptions';
@ -15,7 +15,7 @@ const CustomParamOptions: React.FC<Props> = ({ existingFields }) => {
return ( return (
<> <>
<option value="">Select</option> <option value="">Select</option>
{Object.values(fields).map((opt: TopicCustomParamOption) => ( {Object.values(fields).map((opt: TopicConfigOption) => (
<option key={opt.name} value={opt.name}> <option key={opt.name} value={opt.name}>
{opt.name} {opt.name}
</option> </option>

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useFormContext, ErrorMessage } from 'react-hook-form'; import { useFormContext, ErrorMessage } from 'react-hook-form';
import { TopicFormCustomParam } from 'redux/interfaces'; import { TopicConfigValue } from 'redux/interfaces';
import CustomParamOptions from './CustomParamOptions'; import CustomParamOptions from './CustomParamOptions';
import { INDEX_PREFIX } from './CustomParams'; import { INDEX_PREFIX } from './CustomParams';
@ -24,7 +24,7 @@ const CustomParamSelect: React.FC<Props> = ({
const selectedMustBeUniq = (selected: string) => { const selectedMustBeUniq = (selected: string) => {
const values = getValues({ nest: true }); const values = getValues({ nest: true });
const customParamsValues: TopicFormCustomParam = values.customParams; const customParamsValues: TopicConfigValue = values.customParams;
const valid = !Object.entries(customParamsValues).some( const valid = !Object.entries(customParamsValues).some(
([key, customParam]) => { ([key, customParam]) => {

View file

@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import { useFormContext, ErrorMessage } from 'react-hook-form'; import { useFormContext, ErrorMessage } from 'react-hook-form';
import CUSTOM_PARAMS_OPTIONS from './customParamsOptions'; import CUSTOM_PARAMS_OPTIONS from './customParamsOptions';
import { TopicConfig } from 'generated-sources';
interface Props { interface Props {
isDisabled: boolean; isDisabled: boolean;
index: string; index: string;
name: string; name: TopicConfig['name'];
defaultValue: string; defaultValue: TopicConfig['defaultValue'];
} }
const CustomParamValue: React.FC<Props> = ({ const CustomParamValue: React.FC<Props> = ({

View file

@ -1,7 +1,11 @@
import React from 'react'; import React from 'react';
import { omit, reject, reduce, remove } from 'lodash'; import { omit, reject, reduce, remove } from 'lodash';
import { TopicFormCustomParams, TopicConfigByName } from 'redux/interfaces'; import {
TopicFormCustomParams,
TopicConfigByName,
TopicConfigParams
} from 'redux/interfaces';
import CustomParamButton, { CustomParamButtonType } from './CustomParamButton'; import CustomParamButton, { CustomParamButtonType } from './CustomParamButton';
import CustomParamField from './CustomParamField'; import CustomParamField from './CustomParamField';
@ -12,20 +16,13 @@ interface Props {
config?: TopicConfigByName; config?: TopicConfigByName;
} }
interface Param {
[index: string]: {
name: string;
value: string;
};
}
const existingFields: string[] = []; const existingFields: string[] = [];
const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => { const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
const byIndex = config const byIndex = config
? reduce( ? reduce(
config.byName, config.byName,
(result: Param, param, paramName) => { (result: TopicConfigParams, param, paramName) => {
result[`${INDEX_PREFIX}.${new Date().getTime()}ts`] = { result[`${INDEX_PREFIX}.${new Date().getTime()}ts`] = {
name: paramName, name: paramName,
value: param.value, value: param.value,

View file

@ -1,10 +1,10 @@
import { TopicCustomParamOption } from 'redux/interfaces'; import { TopicConfigOption } from 'redux/interfaces';
interface CustomParamOption { interface TopicConfigOptions {
[optionName: string]: TopicCustomParamOption; [optionName: string]: TopicConfigOption;
} }
const CUSTOM_PARAMS_OPTIONS: CustomParamOption = { const CUSTOM_PARAMS_OPTIONS: TopicConfigOptions = {
'compression.type': { 'compression.type': {
name: 'compression.type', name: 'compression.type',
defaultValue: 'producer', defaultValue: 'producer',

View file

@ -1,13 +1,13 @@
export const BASE_PARAMS: RequestInit = { import { ConfigurationParameters } from 'generated-sources';
export const BASE_PARAMS: ConfigurationParameters = {
basePath: process.env.REACT_APP_API_URL,
credentials: 'include', credentials: 'include',
mode: 'cors',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}; };
export const BASE_URL = process.env.REACT_APP_API_URL;
export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/); export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
export const MILLISECONDS_IN_WEEK = 604_800_000; export const MILLISECONDS_IN_WEEK = 604_800_000;

View file

@ -3,6 +3,14 @@ enum ActionType {
GET_CLUSTERS__SUCCESS = 'GET_CLUSTERS__SUCCESS', GET_CLUSTERS__SUCCESS = 'GET_CLUSTERS__SUCCESS',
GET_CLUSTERS__FAILURE = 'GET_CLUSTERS__FAILURE', GET_CLUSTERS__FAILURE = 'GET_CLUSTERS__FAILURE',
GET_CLUSTER_STATS__REQUEST = 'GET_CLUSTER_STATUS__REQUEST',
GET_CLUSTER_STATS__SUCCESS = 'GET_CLUSTER_STATUS__SUCCESS',
GET_CLUSTER_STATS__FAILURE = 'GET_CLUSTER_STATUS__FAILURE',
GET_CLUSTER_METRICS__REQUEST = 'GET_CLUSTER_METRICS__REQUEST',
GET_CLUSTER_METRICS__SUCCESS = 'GET_CLUSTER_METRICS__SUCCESS',
GET_CLUSTER_METRICS__FAILURE = 'GET_CLUSTER_METRICS__FAILURE',
GET_BROKERS__REQUEST = 'GET_BROKERS__REQUEST', GET_BROKERS__REQUEST = 'GET_BROKERS__REQUEST',
GET_BROKERS__SUCCESS = 'GET_BROKERS__SUCCESS', GET_BROKERS__SUCCESS = 'GET_BROKERS__SUCCESS',
GET_BROKERS__FAILURE = 'GET_BROKERS__FAILURE', GET_BROKERS__FAILURE = 'GET_BROKERS__FAILURE',

View file

@ -1,18 +1,32 @@
import { createAsyncAction } from 'typesafe-actions'; import { createAsyncAction } from 'typesafe-actions';
import ActionType from 'redux/actionType'; import ActionType from 'redux/actionType';
import { TopicName, ConsumerGroupID } from 'redux/interfaces';
import { import {
Cluster,
ClusterStats,
ClusterMetrics,
Broker, Broker,
BrokerMetrics, BrokerMetrics,
Cluster,
Topic, Topic,
TopicConfig,
TopicDetails, TopicDetails,
TopicConfig,
TopicMessage, TopicMessage,
TopicName,
ConsumerGroup, ConsumerGroup,
ConsumerGroupDetails, ConsumerGroupDetails,
ConsumerGroupID, } from 'generated-sources';
} from 'redux/interfaces';
export const fetchClusterStatsAction = createAsyncAction(
ActionType.GET_CLUSTER_STATS__REQUEST,
ActionType.GET_CLUSTER_STATS__SUCCESS,
ActionType.GET_CLUSTER_STATS__FAILURE
)<undefined, ClusterStats, undefined>();
export const fetchClusterMetricsAction = createAsyncAction(
ActionType.GET_CLUSTER_METRICS__REQUEST,
ActionType.GET_CLUSTER_METRICS__SUCCESS,
ActionType.GET_CLUSTER_METRICS__FAILURE
)<undefined, ClusterMetrics, undefined>();
export const fetchBrokersAction = createAsyncAction( export const fetchBrokersAction = createAsyncAction(
ActionType.GET_BROKERS__REQUEST, ActionType.GET_BROKERS__REQUEST,
@ -32,7 +46,7 @@ export const fetchClusterListAction = createAsyncAction(
ActionType.GET_CLUSTERS__FAILURE ActionType.GET_CLUSTERS__FAILURE
)<undefined, Cluster[], undefined>(); )<undefined, Cluster[], undefined>();
export const fetchTopicListAction = createAsyncAction( export const fetchTopicsListAction = createAsyncAction(
ActionType.GET_TOPICS__REQUEST, ActionType.GET_TOPICS__REQUEST,
ActionType.GET_TOPICS__SUCCESS, ActionType.GET_TOPICS__SUCCESS,
ActionType.GET_TOPICS__FAILURE ActionType.GET_TOPICS__FAILURE

View file

@ -1,23 +1,68 @@
import * as api from 'redux/api'; import {
ApiClustersApi,
Configuration,
Cluster,
Topic,
TopicFormData,
TopicConfig,
} from 'generated-sources';
import { import {
ConsumerGroupID, ConsumerGroupID,
PromiseThunk, PromiseThunk,
Cluster,
ClusterName, ClusterName,
TopicFormData, BrokerId,
TopicName, TopicName,
Topic,
TopicMessageQueryParams, TopicMessageQueryParams,
TopicFormFormattedParams,
TopicFormDataRaw,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { BASE_PARAMS } from 'lib/constants';
import * as actions from './actions'; import * as actions from './actions';
const apiClientConf = new Configuration(BASE_PARAMS);
const apiClient = new ApiClustersApi(apiClientConf);
export const fetchClustersList = (): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchClusterListAction.request());
try {
const clusters: Cluster[] = await apiClient.getClusters();
dispatch(actions.fetchClusterListAction.success(clusters));
} catch (e) {
dispatch(actions.fetchClusterListAction.failure());
}
};
export const fetchClusterStats = (
clusterName: ClusterName
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchClusterStatsAction.request());
try {
const payload = await apiClient.getClusterStats({ clusterName });
dispatch(actions.fetchClusterStatsAction.success(payload));
} catch (e) {
dispatch(actions.fetchClusterStatsAction.failure());
}
};
export const fetchClusterMetrics = (
clusterName: ClusterName
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchClusterMetricsAction.request());
try {
const payload = await apiClient.getClusterMetrics({ clusterName });
dispatch(actions.fetchClusterMetricsAction.success(payload));
} catch (e) {
dispatch(actions.fetchClusterMetricsAction.failure());
}
};
export const fetchBrokers = ( export const fetchBrokers = (
clusterName: ClusterName clusterName: ClusterName
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchBrokersAction.request()); dispatch(actions.fetchBrokersAction.request());
try { try {
const payload = await api.getBrokers(clusterName); const payload = await apiClient.getBrokers({ clusterName });
dispatch(actions.fetchBrokersAction.success(payload)); dispatch(actions.fetchBrokersAction.success(payload));
} catch (e) { } catch (e) {
dispatch(actions.fetchBrokersAction.failure()); dispatch(actions.fetchBrokersAction.failure());
@ -25,36 +70,30 @@ export const fetchBrokers = (
}; };
export const fetchBrokerMetrics = ( export const fetchBrokerMetrics = (
clusterName: ClusterName clusterName: ClusterName,
brokerId: BrokerId
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchBrokerMetricsAction.request()); dispatch(actions.fetchBrokerMetricsAction.request());
try { try {
const payload = await api.getBrokerMetrics(clusterName); const payload = await apiClient.getBrokersMetrics({
clusterName,
id: brokerId,
});
dispatch(actions.fetchBrokerMetricsAction.success(payload)); dispatch(actions.fetchBrokerMetricsAction.success(payload));
} catch (e) { } catch (e) {
dispatch(actions.fetchBrokerMetricsAction.failure()); dispatch(actions.fetchBrokerMetricsAction.failure());
} }
}; };
export const fetchClustersList = (): PromiseThunk<void> => async (dispatch) => { export const fetchTopicsList = (
dispatch(actions.fetchClusterListAction.request());
try {
const clusters: Cluster[] = await api.getClusters();
dispatch(actions.fetchClusterListAction.success(clusters));
} catch (e) {
dispatch(actions.fetchClusterListAction.failure());
}
};
export const fetchTopicList = (
clusterName: ClusterName clusterName: ClusterName
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicListAction.request()); dispatch(actions.fetchTopicsListAction.request());
try { try {
const topics = await api.getTopics(clusterName); const topics = await apiClient.getTopics({ clusterName });
dispatch(actions.fetchTopicListAction.success(topics)); dispatch(actions.fetchTopicsListAction.success(topics));
} catch (e) { } catch (e) {
dispatch(actions.fetchTopicListAction.failure()); dispatch(actions.fetchTopicsListAction.failure());
} }
}; };
@ -65,11 +104,11 @@ export const fetchTopicMessages = (
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicMessagesAction.request()); dispatch(actions.fetchTopicMessagesAction.request());
try { try {
const messages = await api.getTopicMessages( const messages = await apiClient.getTopicMessages({
clusterName, clusterName,
topicName, topicName,
queryParams ...queryParams,
); });
dispatch(actions.fetchTopicMessagesAction.success(messages)); dispatch(actions.fetchTopicMessagesAction.success(messages));
} catch (e) { } catch (e) {
dispatch(actions.fetchTopicMessagesAction.failure()); dispatch(actions.fetchTopicMessagesAction.failure());
@ -82,7 +121,10 @@ export const fetchTopicDetails = (
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicDetailsAction.request()); dispatch(actions.fetchTopicDetailsAction.request());
try { try {
const topicDetails = await api.getTopicDetails(clusterName, topicName); const topicDetails = await apiClient.getTopicDetails({
clusterName,
topicName,
});
dispatch( dispatch(
actions.fetchTopicDetailsAction.success({ actions.fetchTopicDetailsAction.success({
topicName, topicName,
@ -100,20 +142,59 @@ export const fetchTopicConfig = (
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicConfigAction.request()); dispatch(actions.fetchTopicConfigAction.request());
try { try {
const config = await api.getTopicConfig(clusterName, topicName); const config = await apiClient.getTopicConfigs({ clusterName, topicName });
dispatch(actions.fetchTopicConfigAction.success({ topicName, config })); dispatch(actions.fetchTopicConfigAction.success({ topicName, config }));
} catch (e) { } catch (e) {
dispatch(actions.fetchTopicConfigAction.failure()); dispatch(actions.fetchTopicConfigAction.failure());
} }
}; };
const formatTopicFormData = (form: TopicFormDataRaw): TopicFormData => {
const {
name,
partitions,
replicationFactor,
cleanupPolicy,
retentionBytes,
retentionMs,
maxMessageBytes,
minInSyncReplicas,
customParams,
} = form;
return {
name,
partitions,
replicationFactor,
configs: {
'cleanup.policy': cleanupPolicy,
'retention.ms': retentionMs,
'retention.bytes': retentionBytes,
'max.message.bytes': maxMessageBytes,
'min.insync.replicas': minInSyncReplicas,
...Object.values(customParams || {}).reduce(
(result: TopicFormFormattedParams, customParam: TopicConfig) => {
return {
...result,
[customParam.name]: customParam.value,
};
},
{}
),
},
};
};
export const createTopic = ( export const createTopic = (
clusterName: ClusterName, clusterName: ClusterName,
form: TopicFormData form: TopicFormDataRaw
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.createTopicAction.request()); dispatch(actions.createTopicAction.request());
try { try {
const topic: Topic = await api.postTopic(clusterName, form); const topic: Topic = await apiClient.createTopic({
clusterName,
topicFormData: formatTopicFormData(form),
});
dispatch(actions.createTopicAction.success(topic)); dispatch(actions.createTopicAction.success(topic));
} catch (e) { } catch (e) {
dispatch(actions.createTopicAction.failure()); dispatch(actions.createTopicAction.failure());
@ -122,11 +203,15 @@ export const createTopic = (
export const updateTopic = ( export const updateTopic = (
clusterName: ClusterName, clusterName: ClusterName,
form: TopicFormData form: TopicFormDataRaw
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.updateTopicAction.request()); dispatch(actions.updateTopicAction.request());
try { try {
const topic: Topic = await api.patchTopic(clusterName, form); const topic: Topic = await apiClient.updateTopic({
clusterName,
topicName: form.name,
topicFormData: formatTopicFormData(form),
});
dispatch(actions.updateTopicAction.success(topic)); dispatch(actions.updateTopicAction.success(topic));
} catch (e) { } catch (e) {
dispatch(actions.updateTopicAction.failure()); dispatch(actions.updateTopicAction.failure());
@ -138,7 +223,7 @@ export const fetchConsumerGroupsList = (
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchConsumerGroupsAction.request()); dispatch(actions.fetchConsumerGroupsAction.request());
try { try {
const consumerGroups = await api.getConsumerGroups(clusterName); const consumerGroups = await apiClient.getConsumerGroups({ clusterName });
dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups)); dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups));
} catch (e) { } catch (e) {
dispatch(actions.fetchConsumerGroupsAction.failure()); dispatch(actions.fetchConsumerGroupsAction.failure());
@ -151,10 +236,10 @@ export const fetchConsumerGroupDetails = (
): PromiseThunk<void> => async (dispatch) => { ): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchConsumerGroupDetailsAction.request()); dispatch(actions.fetchConsumerGroupDetailsAction.request());
try { try {
const consumerGroupDetails = await api.getConsumerGroupDetails( const consumerGroupDetails = await apiClient.getConsumerGroup({
clusterName, clusterName,
consumerGroupID id: consumerGroupID,
); });
dispatch( dispatch(
actions.fetchConsumerGroupDetailsAction.success({ actions.fetchConsumerGroupDetailsAction.success({
consumerGroupID, consumerGroupID,

View file

@ -1,14 +0,0 @@
import { Broker, ClusterName, BrokerMetrics } from 'redux/interfaces';
import { BASE_URL, BASE_PARAMS } from 'lib/constants';
export const getBrokers = (clusterName: ClusterName): Promise<Broker[]> =>
fetch(`${BASE_URL}/clusters/${clusterName}/brokers`, {
...BASE_PARAMS,
}).then((res) => res.json());
export const getBrokerMetrics = (
clusterName: ClusterName
): Promise<BrokerMetrics> =>
fetch(`${BASE_URL}/clusters/${clusterName}/metrics`, {
...BASE_PARAMS,
}).then((res) => res.json());

View file

@ -1,11 +0,0 @@
import {
Cluster,
} from 'redux/interfaces';
import {
BASE_URL,
BASE_PARAMS,
} from 'lib/constants';
export const getClusters = (): Promise<Cluster[]> =>
fetch(`${BASE_URL}/clusters`, { ...BASE_PARAMS })
.then(res => res.json());

View file

@ -1,12 +0,0 @@
import { ClusterName } from '../interfaces/cluster';
import { ConsumerGroup, ConsumerGroupID, ConsumerGroupDetails } from '../interfaces/consumerGroup';
import { BASE_PARAMS, BASE_URL } from '../../lib/constants';
export const getConsumerGroups = (clusterName: ClusterName): Promise<ConsumerGroup[]> =>
fetch(`${BASE_URL}/clusters/${clusterName}/consumerGroups`, { ...BASE_PARAMS })
.then(res => res.json());
export const getConsumerGroupDetails = (clusterName: ClusterName, consumerGroupID: ConsumerGroupID): Promise<ConsumerGroupDetails> =>
fetch(`${BASE_URL}/clusters/${clusterName}/consumer-groups/${consumerGroupID}`, { ...BASE_PARAMS })
.then(res => res.json());

View file

@ -1,4 +0,0 @@
export * from './topics';
export * from './clusters';
export * from './brokers';
export * from './consumerGroups';

View file

@ -1,142 +0,0 @@
import {
TopicName,
TopicMessage,
Topic,
ClusterName,
TopicDetails,
TopicConfig,
TopicFormData,
TopicFormCustomParam,
TopicFormFormattedParams,
TopicFormCustomParams,
TopicMessageQueryParams,
} from 'redux/interfaces';
import { BASE_URL, BASE_PARAMS } from 'lib/constants';
const formatCustomParams = (
customParams: TopicFormCustomParams
): TopicFormFormattedParams => {
return Object.values(customParams || {}).reduce(
(result: TopicFormFormattedParams, customParam: TopicFormCustomParam) => {
return {
...result,
[customParam.name]: customParam.value,
};
},
{} as TopicFormFormattedParams
);
};
export const getTopicConfig = (
clusterName: ClusterName,
topicName: TopicName
): Promise<TopicConfig[]> =>
fetch(`${BASE_URL}/clusters/${clusterName}/topics/${topicName}/config`, {
...BASE_PARAMS,
}).then((res) => res.json());
export const getTopicDetails = (
clusterName: ClusterName,
topicName: TopicName
): Promise<TopicDetails> =>
fetch(`${BASE_URL}/clusters/${clusterName}/topics/${topicName}`, {
...BASE_PARAMS,
}).then((res) => res.json());
export const getTopics = (clusterName: ClusterName): Promise<Topic[]> =>
fetch(`${BASE_URL}/clusters/${clusterName}/topics`, {
...BASE_PARAMS,
}).then((res) => res.json());
export const getTopicMessages = (
clusterName: ClusterName,
topicName: TopicName,
queryParams: Partial<TopicMessageQueryParams>
): Promise<TopicMessage[]> => {
let searchParams = '';
Object.entries({ ...queryParams }).forEach((entry) => {
const key = entry[0];
const value = entry[1];
if (value) {
if (Array.isArray(value)) {
value.forEach((v) => {
searchParams += `${key}=${v}&`;
});
} else {
searchParams += `${key}=${value}&`;
}
}
});
return fetch(
`${BASE_URL}/clusters/${clusterName}/topics/${topicName}/messages?${searchParams}`,
{
...BASE_PARAMS,
}
).then((res) => res.json());
};
export const postTopic = (
clusterName: ClusterName,
form: TopicFormData
): Promise<Topic> => {
const {
name,
partitions,
replicationFactor,
cleanupPolicy,
retentionBytes,
retentionMs,
maxMessageBytes,
minInSyncReplicas,
} = form;
const body = JSON.stringify({
name,
partitions,
replicationFactor,
configs: {
'cleanup.policy': cleanupPolicy,
'retention.ms': retentionMs,
'retention.bytes': retentionBytes,
'max.message.bytes': maxMessageBytes,
'min.insync.replicas': minInSyncReplicas,
...formatCustomParams(form.customParams),
},
});
return fetch(`${BASE_URL}/clusters/${clusterName}/topics`, {
...BASE_PARAMS,
method: 'POST',
body,
}).then((res) => res.json());
};
export const patchTopic = (
clusterName: ClusterName,
form: TopicFormData
): Promise<Topic> => {
const {
cleanupPolicy,
retentionBytes,
retentionMs,
maxMessageBytes,
minInSyncReplicas,
} = form;
const body = JSON.stringify({
configs: {
'cleanup.policy': cleanupPolicy,
'retention.ms': retentionMs,
'retention.bytes': retentionBytes,
'max.message.bytes': maxMessageBytes,
'min.insync.replicas': minInSyncReplicas,
...formatCustomParams(form.customParams),
},
});
return fetch(`${BASE_URL}/clusters/${clusterName}/topics/${form.name}`, {
...BASE_PARAMS,
method: 'PATCH',
body,
}).then((res) => res.json());
};

View file

@ -1,32 +1,9 @@
export type BrokerId = string; import { ClusterStats, Broker } from 'generated-sources';
export interface Broker { export type BrokerId = Broker['id'];
brokerId: BrokerId;
bytesInPerSec: number;
segmentSize: number;
partitionReplicas: number;
bytesOutPerSec: number;
};
export enum ZooKeeperStatus { offline, online }; export enum ZooKeeperStatus { offline, online };
export interface BrokerDiskUsage { export interface BrokersState extends ClusterStats {
brokerId: BrokerId;
segmentSize: number;
}
export interface BrokerMetrics {
brokerCount: number;
zooKeeperStatus: ZooKeeperStatus;
activeControllers: number;
onlinePartitionCount: number;
offlinePartitionCount: number;
inSyncReplicasCount: number,
outOfSyncReplicasCount: number,
underReplicatedPartitionCount: number;
diskUsage: BrokerDiskUsage[];
}
export interface BrokersState extends BrokerMetrics {
items: Broker[]; items: Broker[];
} }

View file

@ -1,18 +1,5 @@
export enum ClusterStatus { import { Cluster } from 'generated-sources';
Online = 'online',
Offline = 'offline',
}
export type ClusterName = string; export type ClusterName = Cluster['name'];
export interface Cluster { export type ClusterState = Cluster[];
id: string;
name: ClusterName;
defaultCluster: boolean;
status: ClusterStatus;
brokerCount: number;
onlinePartitionCount: number;
topicCount: number;
bytesInPerSec: number;
bytesOutPerSec: number;
}

View file

@ -1,27 +1,6 @@
export type ConsumerGroupID = string; import { ConsumerGroup, ConsumerGroupDetails } from 'generated-sources';
export interface ConsumerGroup { export type ConsumerGroupID = ConsumerGroup['consumerGroupId'];
consumerGroupId: ConsumerGroupID;
numConsumers: number;
numTopics: number;
}
export interface ConsumerGroupDetails {
consumerGroupId: ConsumerGroupID;
numConsumers: number;
numTopics: number;
consumers: Consumer[];
}
export interface Consumer {
consumerId: string;
topic: string;
host: string;
partition: number;
messagesBehind: number;
currentOffset: number;
endOffset: number;
}
export interface ConsumerGroupDetailedInfo export interface ConsumerGroupDetailedInfo
extends ConsumerGroup, extends ConsumerGroup,
@ -29,5 +8,5 @@ export interface ConsumerGroupDetailedInfo
export interface ConsumerGroupsState { export interface ConsumerGroupsState {
byID: { [consumerGroupID: string]: ConsumerGroupDetailedInfo }; byID: { [consumerGroupID: string]: ConsumerGroupDetailedInfo };
allIDs: string[]; allIDs: ConsumerGroupID[];
} }

View file

@ -5,7 +5,7 @@ import { ThunkAction } from 'redux-thunk';
import * as actions from 'redux/actions/actions'; import * as actions from 'redux/actions/actions';
import { TopicsState } from './topic'; import { TopicsState } from './topic';
import { Cluster } from './cluster'; import { ClusterState } from './cluster';
import { BrokersState } from './broker'; import { BrokersState } from './broker';
import { LoaderState } from './loader'; import { LoaderState } from './loader';
import { ConsumerGroupsState } from './consumerGroup'; import { ConsumerGroupsState } from './consumerGroup';
@ -25,7 +25,7 @@ export enum FetchStatus {
export interface RootState { export interface RootState {
topics: TopicsState; topics: TopicsState;
clusters: Cluster[]; clusters: ClusterState;
brokers: BrokersState; brokers: BrokersState;
consumerGroups: ConsumerGroupsState; consumerGroups: ConsumerGroupsState;
loader: LoaderState; loader: LoaderState;

View file

@ -1,90 +1,47 @@
export type TopicName = string; import {
Topic,
TopicDetails,
TopicMessage,
TopicConfig,
TopicFormData,
GetTopicMessagesRequest,
} from 'generated-sources';
export type TopicName = Topic['name'];
export enum CleanupPolicy { export enum CleanupPolicy {
Delete = 'delete', Delete = 'delete',
Compact = 'compact', Compact = 'compact',
} }
export interface TopicConfig {
name: string;
value: string;
defaultValue: string;
}
export interface TopicConfigByName { export interface TopicConfigByName {
byName: { byName: TopicConfigParams;
}
export interface TopicConfigParams {
[paramName: string]: TopicConfig; [paramName: string]: TopicConfig;
};
} }
export interface TopicReplica { export interface TopicConfigOption {
broker: number; name: TopicConfig['name'];
leader: boolean; defaultValue: TopicConfig['defaultValue'];
inSync: true;
} }
export interface TopicPartition { export interface TopicConfigValue {
partition: number; name: TopicConfig['name'];
leader: number; value: TopicConfig['value'];
offsetMin: number;
offsetMax: number;
replicas: TopicReplica[];
} }
export interface TopicCustomParamOption {
name: string;
defaultValue: string;
}
export interface TopicDetails {
partitions: TopicPartition[];
}
export interface Topic {
name: TopicName;
internal: boolean;
partitionCount?: number;
replicationFactor?: number;
replicas?: number;
inSyncReplicas?: number;
segmentSize?: number;
segmentCount?: number;
underReplicatedPartitions?: number;
partitions: TopicPartition[];
}
export interface TopicMessage {
partition: number;
offset: number;
timestamp: string;
timestampType: string;
key: string;
headers: Record<string, string>;
content: any;
}
export enum SeekTypes {
OFFSET = 'OFFSET',
TIMESTAMP = 'TIMESTAMP',
}
export type SeekType = keyof typeof SeekTypes;
export interface TopicMessageQueryParams { export interface TopicMessageQueryParams {
q: string; q: GetTopicMessagesRequest['q'];
limit: number; limit: GetTopicMessagesRequest['limit'];
seekType: SeekType; seekType: GetTopicMessagesRequest['seekType'];
seekTo: string[]; seekTo: GetTopicMessagesRequest['seekTo'];
}
export interface TopicFormCustomParam {
name: string;
value: string;
} }
export interface TopicFormCustomParams { export interface TopicFormCustomParams {
byIndex: { [paramIndex: string]: TopicFormCustomParam }; byIndex: TopicConfigParams;
allIndexes: string[]; allIndexes: TopicName[];
} }
export interface TopicWithDetailedInfo extends Topic, TopicDetails { export interface TopicWithDetailedInfo extends Topic, TopicDetails {
@ -97,11 +54,9 @@ export interface TopicsState {
messages: TopicMessage[]; messages: TopicMessage[];
} }
export interface TopicFormFormattedParams { export type TopicFormFormattedParams = TopicFormData['configs'];
[name: string]: string;
}
export interface TopicFormData { export interface TopicFormDataRaw {
name: string; name: string;
partitions: number; partitions: number;
replicationFactor: number; replicationFactor: number;

View file

@ -1,9 +1,5 @@
import { import { Action, BrokersState, ZooKeeperStatus } from 'redux/interfaces';
Action, import { ClusterStats } from 'generated-sources';
BrokersState,
ZooKeeperStatus,
BrokerMetrics,
} from 'redux/interfaces';
import ActionType from 'redux/actionType'; import ActionType from 'redux/actionType';
export const initialState: BrokersState = { export const initialState: BrokersState = {
@ -21,15 +17,14 @@ export const initialState: BrokersState = {
const updateBrokerSegmentSize = ( const updateBrokerSegmentSize = (
state: BrokersState, state: BrokersState,
payload: BrokerMetrics payload: ClusterStats
) => { ) => {
const brokers = state.items; const brokers = state.items;
const { diskUsage } = payload; const { diskUsage } = payload;
const items = brokers.map((broker) => { const items = brokers.map((broker) => {
const brokerMetrics = diskUsage.find( const brokerMetrics =
({ brokerId }) => brokerId === broker.brokerId diskUsage && diskUsage.find(({ brokerId }) => brokerId === broker.id);
);
if (brokerMetrics !== undefined) { if (brokerMetrics !== undefined) {
return { ...broker, ...brokerMetrics }; return { ...broker, ...brokerMetrics };
} }
@ -48,7 +43,7 @@ const reducer = (state = initialState, action: Action): BrokersState => {
...state, ...state,
items: action.payload, items: action.payload,
}; };
case ActionType.GET_BROKER_METRICS__SUCCESS: case ActionType.GET_CLUSTER_STATS__SUCCESS:
return updateBrokerSegmentSize(state, action.payload); return updateBrokerSegmentSize(state, action.payload);
default: default:
return state; return state;

View file

@ -1,4 +1,5 @@
import { Cluster, Action } from 'redux/interfaces'; import { Action } from 'redux/interfaces';
import { Cluster } from 'generated-sources';
import ActionType from 'redux/actionType'; import ActionType from 'redux/actionType';
export const initialState: Cluster[] = []; export const initialState: Cluster[] = [];

View file

@ -1,6 +1,7 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Cluster, RootState, FetchStatus, ClusterStatus } from 'redux/interfaces'; import { RootState, FetchStatus } from 'redux/interfaces';
import { createFetchingSelector } from 'redux/reducers/loader/selectors'; import { createFetchingSelector } from 'redux/reducers/loader/selectors';
import { Cluster, ServerStatus } from 'generated-sources';
const clustersState = ({ clusters }: RootState): Cluster[] => clusters; const clustersState = ({ clusters }: RootState): Cluster[] => clusters;
@ -16,13 +17,13 @@ export const getClusterList = createSelector(clustersState, (clusters) => cluste
export const getOnlineClusters = createSelector( export const getOnlineClusters = createSelector(
getClusterList, getClusterList,
(clusters) => clusters.filter( (clusters) => clusters.filter(
({ status }) => status === ClusterStatus.Online, ({ status }) => status === ServerStatus.Online,
), ),
); );
export const getOfflineClusters = createSelector( export const getOfflineClusters = createSelector(
getClusterList, getClusterList,
(clusters) => clusters.filter( (clusters) => clusters.filter(
({ status }) => status === ClusterStatus.Offline, ({ status }) => status === ServerStatus.Offline,
), ),
); );

View file

@ -1,4 +1,5 @@
import { Action, ConsumerGroup, ConsumerGroupsState } from 'redux/interfaces'; import { Action, ConsumerGroupsState } from 'redux/interfaces';
import { ConsumerGroup } from 'generated-sources';
import ActionType from 'redux/actionType'; import ActionType from 'redux/actionType';
export const initialState: ConsumerGroupsState = { export const initialState: ConsumerGroupsState = {

View file

@ -1,4 +1,5 @@
import { Action, TopicsState, Topic } from 'redux/interfaces'; import { Action, TopicsState } from 'redux/interfaces';
import { Topic } from 'generated-sources';
import ActionType from 'redux/actionType'; import ActionType from 'redux/actionType';
export const initialState: TopicsState = { export const initialState: TopicsState = {

View file

@ -7,6 +7,7 @@ import {
TopicConfigByName, TopicConfigByName,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { createFetchingSelector } from 'redux/reducers/loader/selectors'; import { createFetchingSelector } from 'redux/reducers/loader/selectors';
import { Partition } from 'generated-sources';
const topicsState = ({ topics }: RootState): TopicsState => topics; const topicsState = ({ topics }: RootState): TopicsState => topics;
@ -83,7 +84,7 @@ export const getTopicByName = createSelector(
export const getPartitionsByTopicName = createSelector( export const getPartitionsByTopicName = createSelector(
getTopicMap, getTopicMap,
getTopicName, getTopicName,
(topics, topicName) => topics[topicName].partitions (topics, topicName) => (topics[topicName].partitions) as Partition[]
); );
export const getFullTopic = createSelector(getTopicByName, (topic) => export const getFullTopic = createSelector(getTopicByName, (topic) =>