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
.DS_Store
*.code-workspace

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
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">
<parent>
<artifactId>kafka-ui</artifactId>
<groupId>com.provectus</groupId>
@ -70,8 +69,57 @@
</configOptions>
</configuration>
</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>
</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>
</build>
</profile>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,10 @@
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 ListItem from './ListItem';

View file

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

View file

@ -1,8 +1,8 @@
import React from 'react';
import { Cluster, ClusterStatus } from 'redux/interfaces';
import formatBytes from 'lib/utils/formatBytes';
import { NavLink } from 'react-router-dom';
import { clusterBrokersPath } from 'lib/paths';
import { Cluster, ServerStatus } from 'generated-sources';
const ClusterWidget: React.FC<Cluster> = ({
name,
@ -20,7 +20,7 @@ const ClusterWidget: React.FC<Cluster> = ({
title={name}
>
<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}
</div>
@ -43,11 +43,11 @@ const ClusterWidget: React.FC<Cluster> = ({
</tr>
<tr>
<th>Production</th>
<td>{formatBytes(bytesInPerSec)}</td>
<td>{formatBytes(bytesInPerSec || 0)}</td>
</tr>
<tr>
<th>Consumption</th>
<td>{formatBytes(bytesOutPerSec)}</td>
<td>{formatBytes(bytesOutPerSec || 0)}</td>
</tr>
</tbody>
</table>

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
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 { NavLink, Switch, Route } from 'react-router-dom';
import {

View file

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

View file

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

View file

@ -1,5 +1,6 @@
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 Indicator from 'components/common/Dashboard/Indicator';

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,16 @@
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 Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { clusterTopicsPath } from 'lib/paths';
import TopicForm from 'components/Topics/shared/Form/TopicForm';
import { TopicFormDataRaw } from 'redux/interfaces';
interface Props {
clusterName: ClusterName;
isTopicCreated: boolean;
createTopic: (clusterName: ClusterName, form: TopicFormData) => void;
createTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => void;
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
resetUploadedState: () => void;
}
@ -21,7 +22,7 @@ const New: React.FC<Props> = ({
redirectToTopicPath,
resetUploadedState,
}) => {
const methods = useForm<TopicFormData>();
const methods = useForm<TopicFormDataRaw>();
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
React.useEffect(() => {
@ -31,7 +32,7 @@ const New: React.FC<Props> = ({
}
}, [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
// 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

View file

@ -1,35 +1,48 @@
import { connect } from 'react-redux';
import { RootState, ClusterName, TopicFormData, TopicName, Action } from 'redux/interfaces';
import New from './New';
import {
RootState,
ClusterName,
TopicName,
Action,
TopicFormDataRaw,
} from 'redux/interfaces';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { createTopic } from 'redux/actions';
import { getTopicCreated } from 'redux/reducers/topics/selectors';
import { clusterTopicPath } from 'lib/paths';
import { ThunkDispatch } from 'redux-thunk';
import * as actions from "../../../redux/actions/actions";
import * as actions from 'redux/actions';
import New from './New';
interface RouteProps {
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,
isTopicCreated: getTopicCreated(state),
});
const mapDispatchToProps = (dispatch: ThunkDispatch<RootState, undefined, Action>, { history }: OwnProps) => ({
createTopic: (clusterName: ClusterName, form: TopicFormData) => {
const mapDispatchToProps = (
dispatch: ThunkDispatch<RootState, undefined, Action>,
{ history }: OwnProps
) => ({
createTopic: (clusterName: ClusterName, form: TopicFormDataRaw) => {
dispatch(createTopic(clusterName, form));
},
redirectToTopicPath: (clusterName: ClusterName, topicName: 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;
isFetched: boolean;
fetchBrokers: (clusterName: ClusterName) => void;
fetchTopicList: (clusterName: ClusterName) => void;
fetchTopicsList: (clusterName: ClusterName) => void;
}
const Topics: React.FC<Props> = ({
clusterName,
isFetched,
fetchTopicList,
fetchTopicsList,
}) => {
React.useEffect(() => {
fetchTopicList(clusterName);
}, [fetchTopicList, clusterName]);
fetchTopicsList(clusterName);
}, [fetchTopicsList, clusterName]);
if (isFetched) {
return (

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { fetchTopicList } from 'redux/actions';
import { fetchTopicsList } from 'redux/actions';
import Topics from './Topics';
import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
import { RootState, ClusterName } from 'redux/interfaces';
@ -17,7 +17,7 @@ const mapStateToProps = (state: RootState, { match: { params: { clusterName } }}
});
const mapDispatchToProps = {
fetchTopicList: (clusterName: ClusterName) => fetchTopicList(clusterName),
fetchTopicsList: (clusterName: ClusterName) => fetchTopicsList(clusterName),
};
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 CustomParamValue from 'components/Topics/shared/Form/CustomParams/CustomParamValue';
import CustomParamAction from 'components/Topics/shared/Form/CustomParams/CustomParamAction';
import { TopicConfig } from 'generated-sources';
interface Props {
isDisabled: boolean;
index: string;
name: string;
name: TopicConfig['name'];
existingFields: string[];
defaultValue: string;
defaultValue: TopicConfig['defaultValue'];
onNameChange: (inputName: string, name: string) => void;
onRemove: (index: string) => void;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
import { TopicCustomParamOption } from 'redux/interfaces';
import { TopicConfigOption } from 'redux/interfaces';
interface CustomParamOption {
[optionName: string]: TopicCustomParamOption;
interface TopicConfigOptions {
[optionName: string]: TopicConfigOption;
}
const CUSTOM_PARAMS_OPTIONS: CustomParamOption = {
const CUSTOM_PARAMS_OPTIONS: TopicConfigOptions = {
'compression.type': {
name: 'compression.type',
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',
mode: 'cors',
headers: {
'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 MILLISECONDS_IN_WEEK = 604_800_000;

View file

@ -3,6 +3,14 @@ enum ActionType {
GET_CLUSTERS__SUCCESS = 'GET_CLUSTERS__SUCCESS',
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__SUCCESS = 'GET_BROKERS__SUCCESS',
GET_BROKERS__FAILURE = 'GET_BROKERS__FAILURE',

View file

@ -1,18 +1,32 @@
import { createAsyncAction } from 'typesafe-actions';
import ActionType from 'redux/actionType';
import { TopicName, ConsumerGroupID } from 'redux/interfaces';
import {
Cluster,
ClusterStats,
ClusterMetrics,
Broker,
BrokerMetrics,
Cluster,
Topic,
TopicConfig,
TopicDetails,
TopicConfig,
TopicMessage,
TopicName,
ConsumerGroup,
ConsumerGroupDetails,
ConsumerGroupID,
} from 'redux/interfaces';
} from 'generated-sources';
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(
ActionType.GET_BROKERS__REQUEST,
@ -32,7 +46,7 @@ export const fetchClusterListAction = createAsyncAction(
ActionType.GET_CLUSTERS__FAILURE
)<undefined, Cluster[], undefined>();
export const fetchTopicListAction = createAsyncAction(
export const fetchTopicsListAction = createAsyncAction(
ActionType.GET_TOPICS__REQUEST,
ActionType.GET_TOPICS__SUCCESS,
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 {
ConsumerGroupID,
PromiseThunk,
Cluster,
ClusterName,
TopicFormData,
BrokerId,
TopicName,
Topic,
TopicMessageQueryParams,
TopicFormFormattedParams,
TopicFormDataRaw,
} from 'redux/interfaces';
import { BASE_PARAMS } from 'lib/constants';
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 = (
clusterName: ClusterName
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchBrokersAction.request());
try {
const payload = await api.getBrokers(clusterName);
const payload = await apiClient.getBrokers({ clusterName });
dispatch(actions.fetchBrokersAction.success(payload));
} catch (e) {
dispatch(actions.fetchBrokersAction.failure());
@ -25,36 +70,30 @@ export const fetchBrokers = (
};
export const fetchBrokerMetrics = (
clusterName: ClusterName
clusterName: ClusterName,
brokerId: BrokerId
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchBrokerMetricsAction.request());
try {
const payload = await api.getBrokerMetrics(clusterName);
const payload = await apiClient.getBrokersMetrics({
clusterName,
id: brokerId,
});
dispatch(actions.fetchBrokerMetricsAction.success(payload));
} catch (e) {
dispatch(actions.fetchBrokerMetricsAction.failure());
}
};
export const fetchClustersList = (): PromiseThunk<void> => async (dispatch) => {
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 = (
export const fetchTopicsList = (
clusterName: ClusterName
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicListAction.request());
dispatch(actions.fetchTopicsListAction.request());
try {
const topics = await api.getTopics(clusterName);
dispatch(actions.fetchTopicListAction.success(topics));
const topics = await apiClient.getTopics({ clusterName });
dispatch(actions.fetchTopicsListAction.success(topics));
} catch (e) {
dispatch(actions.fetchTopicListAction.failure());
dispatch(actions.fetchTopicsListAction.failure());
}
};
@ -65,11 +104,11 @@ export const fetchTopicMessages = (
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicMessagesAction.request());
try {
const messages = await api.getTopicMessages(
const messages = await apiClient.getTopicMessages({
clusterName,
topicName,
queryParams
);
...queryParams,
});
dispatch(actions.fetchTopicMessagesAction.success(messages));
} catch (e) {
dispatch(actions.fetchTopicMessagesAction.failure());
@ -82,7 +121,10 @@ export const fetchTopicDetails = (
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicDetailsAction.request());
try {
const topicDetails = await api.getTopicDetails(clusterName, topicName);
const topicDetails = await apiClient.getTopicDetails({
clusterName,
topicName,
});
dispatch(
actions.fetchTopicDetailsAction.success({
topicName,
@ -100,20 +142,59 @@ export const fetchTopicConfig = (
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchTopicConfigAction.request());
try {
const config = await api.getTopicConfig(clusterName, topicName);
const config = await apiClient.getTopicConfigs({ clusterName, topicName });
dispatch(actions.fetchTopicConfigAction.success({ topicName, config }));
} catch (e) {
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 = (
clusterName: ClusterName,
form: TopicFormData
form: TopicFormDataRaw
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.createTopicAction.request());
try {
const topic: Topic = await api.postTopic(clusterName, form);
const topic: Topic = await apiClient.createTopic({
clusterName,
topicFormData: formatTopicFormData(form),
});
dispatch(actions.createTopicAction.success(topic));
} catch (e) {
dispatch(actions.createTopicAction.failure());
@ -122,11 +203,15 @@ export const createTopic = (
export const updateTopic = (
clusterName: ClusterName,
form: TopicFormData
form: TopicFormDataRaw
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.updateTopicAction.request());
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));
} catch (e) {
dispatch(actions.updateTopicAction.failure());
@ -138,7 +223,7 @@ export const fetchConsumerGroupsList = (
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchConsumerGroupsAction.request());
try {
const consumerGroups = await api.getConsumerGroups(clusterName);
const consumerGroups = await apiClient.getConsumerGroups({ clusterName });
dispatch(actions.fetchConsumerGroupsAction.success(consumerGroups));
} catch (e) {
dispatch(actions.fetchConsumerGroupsAction.failure());
@ -151,10 +236,10 @@ export const fetchConsumerGroupDetails = (
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchConsumerGroupDetailsAction.request());
try {
const consumerGroupDetails = await api.getConsumerGroupDetails(
const consumerGroupDetails = await apiClient.getConsumerGroup({
clusterName,
consumerGroupID
);
id: consumerGroupID,
});
dispatch(
actions.fetchConsumerGroupDetailsAction.success({
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 {
brokerId: BrokerId;
bytesInPerSec: number;
segmentSize: number;
partitionReplicas: number;
bytesOutPerSec: number;
};
export type BrokerId = Broker['id'];
export enum ZooKeeperStatus { offline, online };
export interface BrokerDiskUsage {
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 {
export interface BrokersState extends ClusterStats {
items: Broker[];
}

View file

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

View file

@ -1,27 +1,6 @@
export type ConsumerGroupID = string;
import { ConsumerGroup, ConsumerGroupDetails } from 'generated-sources';
export interface ConsumerGroup {
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 type ConsumerGroupID = ConsumerGroup['consumerGroupId'];
export interface ConsumerGroupDetailedInfo
extends ConsumerGroup,
@ -29,5 +8,5 @@ export interface ConsumerGroupDetailedInfo
export interface ConsumerGroupsState {
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 { TopicsState } from './topic';
import { Cluster } from './cluster';
import { ClusterState } from './cluster';
import { BrokersState } from './broker';
import { LoaderState } from './loader';
import { ConsumerGroupsState } from './consumerGroup';
@ -25,7 +25,7 @@ export enum FetchStatus {
export interface RootState {
topics: TopicsState;
clusters: Cluster[];
clusters: ClusterState;
brokers: BrokersState;
consumerGroups: ConsumerGroupsState;
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 {
Delete = 'delete',
Compact = 'compact',
}
export interface TopicConfig {
name: string;
value: string;
defaultValue: string;
}
export interface TopicConfigByName {
byName: {
byName: TopicConfigParams;
}
export interface TopicConfigParams {
[paramName: string]: TopicConfig;
};
}
export interface TopicReplica {
broker: number;
leader: boolean;
inSync: true;
export interface TopicConfigOption {
name: TopicConfig['name'];
defaultValue: TopicConfig['defaultValue'];
}
export interface TopicPartition {
partition: number;
leader: number;
offsetMin: number;
offsetMax: number;
replicas: TopicReplica[];
export interface TopicConfigValue {
name: TopicConfig['name'];
value: TopicConfig['value'];
}
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 {
q: string;
limit: number;
seekType: SeekType;
seekTo: string[];
}
export interface TopicFormCustomParam {
name: string;
value: string;
q: GetTopicMessagesRequest['q'];
limit: GetTopicMessagesRequest['limit'];
seekType: GetTopicMessagesRequest['seekType'];
seekTo: GetTopicMessagesRequest['seekTo'];
}
export interface TopicFormCustomParams {
byIndex: { [paramIndex: string]: TopicFormCustomParam };
allIndexes: string[];
byIndex: TopicConfigParams;
allIndexes: TopicName[];
}
export interface TopicWithDetailedInfo extends Topic, TopicDetails {
@ -97,11 +54,9 @@ export interface TopicsState {
messages: TopicMessage[];
}
export interface TopicFormFormattedParams {
[name: string]: string;
}
export type TopicFormFormattedParams = TopicFormData['configs'];
export interface TopicFormData {
export interface TopicFormDataRaw {
name: string;
partitions: number;
replicationFactor: number;

View file

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

View file

@ -1,6 +1,7 @@
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 { Cluster, ServerStatus } from 'generated-sources';
const clustersState = ({ clusters }: RootState): Cluster[] => clusters;
@ -16,13 +17,13 @@ export const getClusterList = createSelector(clustersState, (clusters) => cluste
export const getOnlineClusters = createSelector(
getClusterList,
(clusters) => clusters.filter(
({ status }) => status === ClusterStatus.Online,
({ status }) => status === ServerStatus.Online,
),
);
export const getOfflineClusters = createSelector(
getClusterList,
(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';
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';
export const initialState: TopicsState = {

View file

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