Topic settings editing (#55)
* Topic editing * Remove old code * Implement unique field name select * Final changes to topic editing * Cleanup eslint.json
This commit is contained in:
parent
5a0b23ed59
commit
128c0d2e92
39 changed files with 5883 additions and 3552 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -30,3 +30,5 @@ build/
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
/kafka-ui-api/app/node
|
/kafka-ui-api/app/node
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
7816
kafka-ui-react-app/package-lock.json
generated
7816
kafka-ui-react-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,37 +3,21 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
|
||||||
"@testing-library/react": "^9.3.2",
|
|
||||||
"@testing-library/user-event": "^7.1.2",
|
|
||||||
"@types/classnames": "^2.2.9",
|
|
||||||
"@types/jest": "^24.0.25",
|
|
||||||
"@types/lodash": "^4.14.149",
|
|
||||||
"@types/node": "^12.12.24",
|
|
||||||
"@types/react": "^16.9.17",
|
|
||||||
"@types/react-dom": "^16.9.0",
|
|
||||||
"@types/react-redux": "^7.1.5",
|
|
||||||
"@types/react-router-dom": "^5.1.3",
|
|
||||||
"@types/redux": "^3.6.0",
|
|
||||||
"@types/redux-thunk": "^2.1.0",
|
|
||||||
"bulma": "^0.8.0",
|
"bulma": "^0.8.0",
|
||||||
"bulma-switch": "^2.0.0",
|
"bulma-switch": "^2.0.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"json-server": "^0.15.1",
|
"immer": "^6.0.5",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"node-sass": "^4.13.1",
|
|
||||||
"pretty-ms": "^6.0.1",
|
"pretty-ms": "^6.0.1",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-hook-form": "^4.5.5",
|
"react-hook-form": "^4.5.5",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.3.0",
|
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"typesafe-actions": "^5.1.0",
|
"typesafe-actions": "^5.1.0"
|
||||||
"typescript": "~3.7.4"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,jsx,tsx}": [
|
"*.{js,ts,jsx,tsx}": [
|
||||||
|
@ -70,6 +54,19 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.3.2",
|
||||||
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
|
"@types/jest": "^24.0.25",
|
||||||
|
"@types/lodash": "^4.14.149",
|
||||||
|
"@types/node": "^12.12.24",
|
||||||
|
"@types/react": "^16.9.17",
|
||||||
|
"@types/react-dom": "^16.9.0",
|
||||||
|
"@types/react-redux": "^7.1.5",
|
||||||
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
"@types/redux": "^3.6.0",
|
||||||
|
"@types/redux-thunk": "^2.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
||||||
"@typescript-eslint/parser": "^2.27.0",
|
"@typescript-eslint/parser": "^2.27.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
|
@ -82,7 +79,12 @@
|
||||||
"eslint-plugin-react-hooks": "^2.5.1",
|
"eslint-plugin-react-hooks": "^2.5.1",
|
||||||
"esprint": "^0.6.0",
|
"esprint": "^0.6.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
|
"json-server": "^0.15.1",
|
||||||
"lint-staged": ">=10",
|
"lint-staged": ">=10",
|
||||||
"prettier": "^2.0.4"
|
"node-sass": "^4.13.1",
|
||||||
}
|
"prettier": "^2.0.4",
|
||||||
|
"react-scripts": "3.4.0",
|
||||||
|
"typescript": "~3.7.4"
|
||||||
|
},
|
||||||
|
"proxy": "http://localhost:8080"
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,12 @@ import {
|
||||||
clusterTopicSettingsPath,
|
clusterTopicSettingsPath,
|
||||||
clusterTopicPath,
|
clusterTopicPath,
|
||||||
clusterTopicMessagesPath,
|
clusterTopicMessagesPath,
|
||||||
|
clusterTopicsTopicEditPath,
|
||||||
} from 'lib/paths';
|
} from 'lib/paths';
|
||||||
import OverviewContainer from './Overview/OverviewContainer';
|
import OverviewContainer from './Overview/OverviewContainer';
|
||||||
import MessagesContainer from './Messages/MessagesContainer';
|
import MessagesContainer from './Messages/MessagesContainer';
|
||||||
import SettingsContainer from './Settings/SettingsContainer';
|
import SettingsContainer from './Settings/SettingsContainer';
|
||||||
|
import SettingsEditButton from './Settings/SettingsEditButton';
|
||||||
|
|
||||||
interface Props extends Topic, TopicDetails {
|
interface Props extends Topic, TopicDetails {
|
||||||
clusterName: ClusterName;
|
clusterName: ClusterName;
|
||||||
|
@ -30,6 +32,9 @@ const Details: React.FC<Props> = ({ clusterName, topicName }) => {
|
||||||
{topicName}
|
{topicName}
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
<SettingsEditButton
|
||||||
|
to={clusterTopicsTopicEditPath(clusterName, topicName)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="box">
|
<div className="box">
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsEditButton: React.FC<Props> = ({ to }) => (
|
||||||
|
<Link to={to}>
|
||||||
|
<button type="button" className="button is-small is-warning">
|
||||||
|
Edit settings
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SettingsEditButton;
|
143
kafka-ui-react-app/src/components/Topics/Edit/Edit.tsx
Normal file
143
kafka-ui-react-app/src/components/Topics/Edit/Edit.tsx
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
ClusterName,
|
||||||
|
TopicFormData,
|
||||||
|
TopicName,
|
||||||
|
TopicConfigByName,
|
||||||
|
TopicWithDetailedInfo,
|
||||||
|
CleanupPolicy,
|
||||||
|
} from 'redux/interfaces';
|
||||||
|
import { useForm, FormContext } from 'react-hook-form';
|
||||||
|
import { camelCase } from 'lodash';
|
||||||
|
|
||||||
|
import TopicForm from '../shared/Form/TopicForm';
|
||||||
|
import FormBreadcrumbs from '../shared/Form/FormBreadcrumbs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
clusterName: ClusterName;
|
||||||
|
topicName: TopicName;
|
||||||
|
topic?: TopicWithDetailedInfo;
|
||||||
|
isFetched: boolean;
|
||||||
|
isTopicDetailsFetched: boolean;
|
||||||
|
isTopicUpdated: boolean;
|
||||||
|
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||||
|
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||||
|
updateTopic: (clusterName: ClusterName, form: TopicFormData) => void;
|
||||||
|
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => void;
|
||||||
|
resetUploadedState: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
partitions: 1,
|
||||||
|
replicationFactor: 1,
|
||||||
|
minInSyncReplicas: 1,
|
||||||
|
cleanupPolicy: CleanupPolicy.Delete,
|
||||||
|
retentionBytes: -1,
|
||||||
|
maxMessageBytes: 1000012,
|
||||||
|
};
|
||||||
|
|
||||||
|
const topicParams = (topic: TopicWithDetailedInfo | undefined) => {
|
||||||
|
if (!topic) {
|
||||||
|
return DEFAULTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, replicationFactor } = topic;
|
||||||
|
|
||||||
|
const configs = topic.config?.reduce(
|
||||||
|
(result: { [name: string]: string }, param) => {
|
||||||
|
result[camelCase(param.name)] = param.value || param.defaultValue;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULTS,
|
||||||
|
name,
|
||||||
|
partitions: topic.partitionCount || DEFAULTS.partitions,
|
||||||
|
replicationFactor,
|
||||||
|
...configs,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let formInit = false;
|
||||||
|
|
||||||
|
const Edit: React.FC<Props> = ({
|
||||||
|
clusterName,
|
||||||
|
topicName,
|
||||||
|
topic,
|
||||||
|
isFetched,
|
||||||
|
isTopicDetailsFetched,
|
||||||
|
isTopicUpdated,
|
||||||
|
fetchTopicDetails,
|
||||||
|
fetchTopicConfig,
|
||||||
|
updateTopic,
|
||||||
|
redirectToTopicPath,
|
||||||
|
}) => {
|
||||||
|
const defaultValues = topicParams(topic);
|
||||||
|
|
||||||
|
const methods = useForm<TopicFormData>({ defaultValues });
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchTopicConfig(clusterName, topicName);
|
||||||
|
fetchTopicDetails(clusterName, topicName);
|
||||||
|
}, [fetchTopicConfig, fetchTopicDetails, clusterName, topicName]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isSubmitting && isTopicUpdated) {
|
||||||
|
const { name } = methods.getValues();
|
||||||
|
redirectToTopicPath(clusterName, name);
|
||||||
|
}
|
||||||
|
}, [isSubmitting, isTopicUpdated, redirectToTopicPath, clusterName, methods]);
|
||||||
|
|
||||||
|
if (!isFetched || !isTopicDetailsFetched || !topic || !topic.config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formInit) {
|
||||||
|
methods.reset(defaultValues);
|
||||||
|
formInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: TopicConfigByName = {
|
||||||
|
byName: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
topic.config.forEach((param) => {
|
||||||
|
config.byName[param.name] = param;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: TopicFormData) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
updateTopic(clusterName, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="section">
|
||||||
|
<div className="level">
|
||||||
|
<FormBreadcrumbs
|
||||||
|
clusterName={clusterName}
|
||||||
|
topicName={topicName}
|
||||||
|
current="Edit Topic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box">
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<FormContext {...methods}>
|
||||||
|
<TopicForm
|
||||||
|
topicName={topicName}
|
||||||
|
config={config}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
isEditing
|
||||||
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
|
/>
|
||||||
|
</FormContext>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Edit;
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
RootState,
|
||||||
|
ClusterName,
|
||||||
|
TopicFormData,
|
||||||
|
TopicName,
|
||||||
|
Action,
|
||||||
|
} from 'redux/interfaces';
|
||||||
|
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
updateTopic,
|
||||||
|
fetchTopicConfig,
|
||||||
|
fetchTopicDetails,
|
||||||
|
} from 'redux/actions';
|
||||||
|
import {
|
||||||
|
getTopicConfigFetched,
|
||||||
|
getTopicUpdated,
|
||||||
|
getIsTopicDetailsFetched,
|
||||||
|
getFullTopic,
|
||||||
|
} from 'redux/reducers/topics/selectors';
|
||||||
|
import { clusterTopicPath } from 'lib/paths';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import Edit from './Edit';
|
||||||
|
|
||||||
|
interface RouteProps {
|
||||||
|
clusterName: ClusterName;
|
||||||
|
topicName: TopicName;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OwnProps = RouteComponentProps<RouteProps>;
|
||||||
|
|
||||||
|
const mapStateToProps = (
|
||||||
|
state: RootState,
|
||||||
|
{
|
||||||
|
match: {
|
||||||
|
params: { topicName, clusterName },
|
||||||
|
},
|
||||||
|
}: OwnProps
|
||||||
|
) => ({
|
||||||
|
clusterName,
|
||||||
|
topicName,
|
||||||
|
topic: getFullTopic(state, topicName),
|
||||||
|
isFetched: getTopicConfigFetched(state),
|
||||||
|
isTopicDetailsFetched: getIsTopicDetailsFetched(state),
|
||||||
|
isTopicUpdated: getTopicUpdated(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<RootState, undefined, Action>,
|
||||||
|
{ history }: OwnProps
|
||||||
|
) => ({
|
||||||
|
fetchTopicDetails: (clusterName: ClusterName, topicName: TopicName) =>
|
||||||
|
dispatch(fetchTopicDetails(clusterName, topicName)),
|
||||||
|
fetchTopicConfig: (clusterName: ClusterName, topicName: TopicName) =>
|
||||||
|
dispatch(fetchTopicConfig(clusterName, topicName)),
|
||||||
|
updateTopic: (clusterName: ClusterName, form: TopicFormData) =>
|
||||||
|
dispatch(updateTopic(clusterName, form)),
|
||||||
|
redirectToTopicPath: (clusterName: ClusterName, topicName: TopicName) => {
|
||||||
|
history.push(clusterTopicPath(clusterName, topicName));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));
|
|
@ -1,20 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { TopicCustomParamOption } from 'redux/interfaces';
|
|
||||||
import { CUSTOM_PARAMS_OPTIONS } from './customParamsOptions';
|
|
||||||
|
|
||||||
interface Props {};
|
|
||||||
|
|
||||||
const CustomParamOptions: React.FC<Props> = () => (
|
|
||||||
<>
|
|
||||||
<option value=''>Select</option>
|
|
||||||
{
|
|
||||||
Object.values(CUSTOM_PARAMS_OPTIONS).map((opt: TopicCustomParamOption) => (
|
|
||||||
<option key={opt.name} value={opt.name}>
|
|
||||||
{opt.name}
|
|
||||||
</option>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default React.memo(CustomParamOptions);
|
|
|
@ -1,87 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { omit, reject } from 'lodash';
|
|
||||||
|
|
||||||
import { TopicFormCustomParams } from 'redux/interfaces';
|
|
||||||
import CustomParamSelect from './CustomParamSelect';
|
|
||||||
import CustomParamValue from './CustomParamValue';
|
|
||||||
import CustomParamAction from './CustomParamAction';
|
|
||||||
import CustomParamButton, { CustomParamButtonType } from './CustomParamButton';
|
|
||||||
|
|
||||||
export const INDEX_PREFIX = 'customParams';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isSubmitting: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomParams: React.FC<Props> = ({ isSubmitting }) => {
|
|
||||||
const [formCustomParams, setFormCustomParams] = React.useState<
|
|
||||||
TopicFormCustomParams
|
|
||||||
>({
|
|
||||||
byIndex: {},
|
|
||||||
allIndexes: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const onAdd = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const newIndex = `${INDEX_PREFIX}.${new Date().getTime()}ts`;
|
|
||||||
|
|
||||||
setFormCustomParams({
|
|
||||||
...formCustomParams,
|
|
||||||
byIndex: {
|
|
||||||
...formCustomParams.byIndex,
|
|
||||||
[newIndex]: { name: '', value: '' },
|
|
||||||
},
|
|
||||||
allIndexes: [newIndex, ...formCustomParams.allIndexes],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemove = (index: string) => {
|
|
||||||
setFormCustomParams({
|
|
||||||
...formCustomParams,
|
|
||||||
byIndex: omit(formCustomParams.byIndex, index),
|
|
||||||
allIndexes: reject(formCustomParams.allIndexes, (i) => i === index),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column">
|
|
||||||
<CustomParamButton
|
|
||||||
className="is-success"
|
|
||||||
type={CustomParamButtonType.plus}
|
|
||||||
onClick={onAdd}
|
|
||||||
btnText="Add Custom Parameter"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{formCustomParams.allIndexes.map((index) => (
|
|
||||||
<div className="columns is-centered" key={index}>
|
|
||||||
<div className="column">
|
|
||||||
<CustomParamSelect
|
|
||||||
index={index}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
name={formCustomParams.byIndex[index].name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="column">
|
|
||||||
<CustomParamValue
|
|
||||||
index={index}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
name={formCustomParams.byIndex[index].name}
|
|
||||||
defaultValue={formCustomParams.byIndex[index].value}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="column is-narrow">
|
|
||||||
<CustomParamAction index={index} onRemove={onRemove} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CustomParams;
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { RootState } from 'redux/interfaces';
|
|
||||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
|
||||||
import CustomParams from './CustomParams';
|
|
||||||
|
|
||||||
interface RouteProps {};
|
|
||||||
|
|
||||||
interface OwnProps extends RouteComponentProps<RouteProps> {
|
|
||||||
isSubmitting: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: RootState, { isSubmitting }: OwnProps) => ({
|
|
||||||
isSubmitting,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default withRouter(
|
|
||||||
connect(mapStateToProps)(CustomParams)
|
|
||||||
);
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { TopicCustomParamOption } from 'redux/interfaces';
|
|
||||||
|
|
||||||
interface CustomParamOption {
|
|
||||||
[optionName: string]: TopicCustomParamOption;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CUSTOM_PARAMS_OPTIONS: CustomParamOption = {
|
|
||||||
"compression.type": {
|
|
||||||
"name": "compression.type",
|
|
||||||
"defaultValue": "producer"
|
|
||||||
},
|
|
||||||
"leader.replication.throttled.replicas": {
|
|
||||||
"name": "leader.replication.throttled.replicas",
|
|
||||||
"defaultValue": ""
|
|
||||||
},
|
|
||||||
"message.downconversion.enable": {
|
|
||||||
"name": "message.downconversion.enable",
|
|
||||||
"defaultValue": "true"
|
|
||||||
},
|
|
||||||
"segment.jitter.ms": {
|
|
||||||
"name": "segment.jitter.ms",
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
"flush.ms": {
|
|
||||||
"name": "flush.ms",
|
|
||||||
"defaultValue": "9223372036854775807"
|
|
||||||
},
|
|
||||||
"follower.replication.throttled.replicas": {
|
|
||||||
"name": "follower.replication.throttled.replicas",
|
|
||||||
"defaultValue": ""
|
|
||||||
},
|
|
||||||
"segment.bytes": {
|
|
||||||
"name": "segment.bytes",
|
|
||||||
"defaultValue": "1073741824"
|
|
||||||
},
|
|
||||||
"flush.messages": {
|
|
||||||
"name": "flush.messages",
|
|
||||||
"defaultValue": "9223372036854775807"
|
|
||||||
},
|
|
||||||
"message.format.version": {
|
|
||||||
"name": "message.format.version",
|
|
||||||
"defaultValue": "2.3-IV1"
|
|
||||||
},
|
|
||||||
"file.delete.delay.ms": {
|
|
||||||
"name": "file.delete.delay.ms",
|
|
||||||
"defaultValue": "60000"
|
|
||||||
},
|
|
||||||
"max.compaction.lag.ms": {
|
|
||||||
"name": "max.compaction.lag.ms",
|
|
||||||
"defaultValue": "9223372036854775807"
|
|
||||||
},
|
|
||||||
"min.compaction.lag.ms": {
|
|
||||||
"name": "min.compaction.lag.ms",
|
|
||||||
"defaultValue": "0"
|
|
||||||
},
|
|
||||||
"message.timestamp.type": {
|
|
||||||
"name": "message.timestamp.type",
|
|
||||||
"defaultValue": "CreateTime"
|
|
||||||
},
|
|
||||||
"preallocate": {
|
|
||||||
"name": "preallocate",
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
"min.cleanable.dirty.ratio": {
|
|
||||||
"name": "min.cleanable.dirty.ratio",
|
|
||||||
"defaultValue": "0.5"
|
|
||||||
},
|
|
||||||
"index.interval.bytes": {
|
|
||||||
"name": "index.interval.bytes",
|
|
||||||
"defaultValue": "4096"
|
|
||||||
},
|
|
||||||
"unclean.leader.election.enable": {
|
|
||||||
"name": "unclean.leader.election.enable",
|
|
||||||
"defaultValue": "true"
|
|
||||||
},
|
|
||||||
"retention.bytes": {
|
|
||||||
"name": "retention.bytes",
|
|
||||||
"defaultValue": "-1"
|
|
||||||
},
|
|
||||||
"delete.retention.ms": {
|
|
||||||
"name": "delete.retention.ms",
|
|
||||||
"defaultValue": "86400000"
|
|
||||||
},
|
|
||||||
"segment.ms": {
|
|
||||||
"name": "segment.ms",
|
|
||||||
"defaultValue": "604800000"
|
|
||||||
},
|
|
||||||
"message.timestamp.difference.max.ms": {
|
|
||||||
"name": "message.timestamp.difference.max.ms",
|
|
||||||
"defaultValue": "9223372036854775807"
|
|
||||||
},
|
|
||||||
"segment.index.bytes": {
|
|
||||||
"name": "segment.index.bytes",
|
|
||||||
"defaultValue": "10485760"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { ClusterName, TopicFormData, TopicName } from 'redux/interfaces';
|
||||||
ClusterName,
|
import { useForm, FormContext } from 'react-hook-form';
|
||||||
CleanupPolicy,
|
|
||||||
TopicFormData,
|
|
||||||
TopicName,
|
|
||||||
} from 'redux/interfaces';
|
|
||||||
import { useForm, FormContext, ErrorMessage } 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 { TOPIC_NAME_VALIDATION_PATTERN, BYTES_IN_GB } from 'lib/constants';
|
import TopicForm from 'components/Topics/shared/Form/TopicForm';
|
||||||
import CustomParamsContainer from './CustomParams/CustomParamsContainer';
|
|
||||||
import TimeToRetain from './TimeToRetain';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clusterName: ClusterName;
|
clusterName: ClusterName;
|
||||||
|
@ -36,13 +29,7 @@ const New: React.FC<Props> = ({
|
||||||
const { name } = methods.getValues();
|
const { name } = methods.getValues();
|
||||||
redirectToTopicPath(clusterName, name);
|
redirectToTopicPath(clusterName, name);
|
||||||
}
|
}
|
||||||
}, [
|
}, [isSubmitting, isTopicCreated, redirectToTopicPath, clusterName, methods]);
|
||||||
isSubmitting,
|
|
||||||
isTopicCreated,
|
|
||||||
redirectToTopicPath,
|
|
||||||
clusterName,
|
|
||||||
methods.getValues,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: TopicFormData) => {
|
const onSubmit = async (data: TopicFormData) => {
|
||||||
// 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
|
||||||
|
@ -70,161 +57,10 @@ const New: React.FC<Props> = ({
|
||||||
<div className="box">
|
<div className="box">
|
||||||
{/* eslint-disable react/jsx-props-no-spreading */}
|
{/* eslint-disable react/jsx-props-no-spreading */}
|
||||||
<FormContext {...methods}>
|
<FormContext {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
<TopicForm
|
||||||
<div className="columns">
|
isSubmitting={isSubmitting}
|
||||||
<div className="column is-three-quarters">
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
<label className="label">Topic Name *</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
placeholder="Topic Name"
|
|
||||||
ref={methods.register({
|
|
||||||
required: 'Topic Name is required.',
|
|
||||||
pattern: {
|
|
||||||
value: TOPIC_NAME_VALIDATION_PATTERN,
|
|
||||||
message: 'Only alphanumeric, _, -, and . allowed',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
name="name"
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
/>
|
||||||
<p className="help is-danger">
|
|
||||||
<ErrorMessage errors={methods.errors} name="name" />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="column">
|
|
||||||
<label className="label">Number of partitions *</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
placeholder="Number of partitions"
|
|
||||||
defaultValue="1"
|
|
||||||
ref={methods.register({
|
|
||||||
required: 'Number of partitions is required.',
|
|
||||||
})}
|
|
||||||
name="partitions"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<p className="help is-danger">
|
|
||||||
<ErrorMessage errors={methods.errors} name="partitions" />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column">
|
|
||||||
<label className="label">Replication Factor *</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
placeholder="Replication Factor"
|
|
||||||
defaultValue="1"
|
|
||||||
ref={methods.register({
|
|
||||||
required: 'Replication Factor is required.',
|
|
||||||
})}
|
|
||||||
name="replicationFactor"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<p className="help is-danger">
|
|
||||||
<ErrorMessage
|
|
||||||
errors={methods.errors}
|
|
||||||
name="replicationFactor"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="column">
|
|
||||||
<label className="label">Min In Sync Replicas *</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
placeholder="Replication Factor"
|
|
||||||
defaultValue="1"
|
|
||||||
ref={methods.register({
|
|
||||||
required: 'Min In Sync Replicas is required.',
|
|
||||||
})}
|
|
||||||
name="minInSyncReplicas"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<p className="help is-danger">
|
|
||||||
<ErrorMessage
|
|
||||||
errors={methods.errors}
|
|
||||||
name="minInSyncReplicas"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column is-one-third">
|
|
||||||
<label className="label">Cleanup policy</label>
|
|
||||||
<div className="select is-block">
|
|
||||||
<select
|
|
||||||
defaultValue={CleanupPolicy.Delete}
|
|
||||||
name="cleanupPolicy"
|
|
||||||
ref={methods.register}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<option value={CleanupPolicy.Delete}>Delete</option>
|
|
||||||
<option value={CleanupPolicy.Compact}>Compact</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="column is-one-third">
|
|
||||||
<TimeToRetain isSubmitting={isSubmitting} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="column is-one-third">
|
|
||||||
<label className="label">Max size on disk in GB</label>
|
|
||||||
<div className="select is-block">
|
|
||||||
<select
|
|
||||||
defaultValue={-1}
|
|
||||||
name="retentionBytes"
|
|
||||||
ref={methods.register}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<option value={-1}>Not Set</option>
|
|
||||||
<option value={BYTES_IN_GB}>1 GB</option>
|
|
||||||
<option value={BYTES_IN_GB * 10}>10 GB</option>
|
|
||||||
<option value={BYTES_IN_GB * 20}>20 GB</option>
|
|
||||||
<option value={BYTES_IN_GB * 50}>50 GB</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column">
|
|
||||||
<label className="label">Maximum message size in bytes *</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
defaultValue="1000012"
|
|
||||||
ref={methods.register({
|
|
||||||
required: 'Maximum message size in bytes is required',
|
|
||||||
})}
|
|
||||||
name="maxMessageBytes"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<p className="help is-danger">
|
|
||||||
<ErrorMessage
|
|
||||||
errors={methods.errors}
|
|
||||||
name="maxMessageBytes"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CustomParamsContainer isSubmitting={isSubmitting} />
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
className="button is-primary"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</FormContext>
|
</FormContext>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import { ClusterName } from 'redux/interfaces';
|
import { ClusterName } from 'redux/interfaces';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
import EditContainer from 'components/Topics/Edit/EditContainer';
|
||||||
import ListContainer from './List/ListContainer';
|
import ListContainer from './List/ListContainer';
|
||||||
import DetailsContainer from './Details/DetailsContainer';
|
import DetailsContainer from './Details/DetailsContainer';
|
||||||
import NewContainer from './New/NewContainer';
|
import NewContainer from './New/NewContainer';
|
||||||
|
@ -35,6 +36,11 @@ const Topics: React.FC<Props> = ({
|
||||||
path="/ui/clusters/:clusterName/topics/new"
|
path="/ui/clusters/:clusterName/topics/new"
|
||||||
component={NewContainer}
|
component={NewContainer}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/ui/clusters/:clusterName/topics/:topicName/edit"
|
||||||
|
component={EditContainer}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/ui/clusters/:clusterName/topics/:topicName"
|
path="/ui/clusters/:clusterName/topics/:topicName"
|
||||||
component={DetailsContainer}
|
component={DetailsContainer}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useFormContext, ErrorMessage } from 'react-hook-form';
|
||||||
|
import { TopicFormCustomParam } from 'redux/interfaces';
|
||||||
|
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 { INDEX_PREFIX } from './CustomParams';
|
||||||
|
import CustomParamOptions from './CustomParamOptions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isDisabled: boolean;
|
||||||
|
index: string;
|
||||||
|
name: string;
|
||||||
|
existingFields: string[];
|
||||||
|
defaultValue: string;
|
||||||
|
onNameChange: (inputName: string, name: string) => void;
|
||||||
|
onRemove: (index: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomParamField: React.FC<Props> = ({
|
||||||
|
isDisabled,
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
existingFields,
|
||||||
|
defaultValue,
|
||||||
|
onNameChange,
|
||||||
|
onRemove,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="columns is-centered">
|
||||||
|
<div className="column">
|
||||||
|
<CustomParamSelect
|
||||||
|
index={index}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
name={name}
|
||||||
|
existingFields={existingFields}
|
||||||
|
onNameChange={onNameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column">
|
||||||
|
<CustomParamValue
|
||||||
|
index={index}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-narrow">
|
||||||
|
<CustomParamAction index={index} onRemove={onRemove} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(CustomParamField);
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { TopicCustomParamOption } from 'redux/interfaces';
|
||||||
|
import { omitBy } from 'lodash';
|
||||||
|
import CUSTOM_PARAMS_OPTIONS from './customParamsOptions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
existingFields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomParamOptions: React.FC<Props> = ({ existingFields }) => {
|
||||||
|
const fields = omitBy(Object.values(CUSTOM_PARAMS_OPTIONS), (field) =>
|
||||||
|
existingFields.includes(field.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<option value="">Select</option>
|
||||||
|
{Object.values(fields).map((opt: TopicCustomParamOption) => (
|
||||||
|
<option key={opt.name} value={opt.name}>
|
||||||
|
{opt.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(CustomParamOptions);
|
|
@ -8,9 +8,17 @@ interface Props {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
index: string;
|
index: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
existingFields: string[];
|
||||||
|
onNameChange: (inputName: string, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomParamSelect: React.FC<Props> = ({ isDisabled, index, name }) => {
|
const CustomParamSelect: React.FC<Props> = ({
|
||||||
|
isDisabled,
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
existingFields,
|
||||||
|
onNameChange,
|
||||||
|
}) => {
|
||||||
const { register, errors, getValues, triggerValidation } = useFormContext();
|
const { register, errors, getValues, triggerValidation } = useFormContext();
|
||||||
const optInputName = `${index}[name]`;
|
const optInputName = `${index}[name]`;
|
||||||
|
|
||||||
|
@ -18,18 +26,20 @@ const CustomParamSelect: React.FC<Props> = ({ isDisabled, index, name }) => {
|
||||||
const values = getValues({ nest: true });
|
const values = getValues({ nest: true });
|
||||||
const customParamsValues: TopicFormCustomParam = values.customParams;
|
const customParamsValues: TopicFormCustomParam = values.customParams;
|
||||||
|
|
||||||
let valid = true;
|
const valid = !Object.entries(customParamsValues).some(
|
||||||
|
([key, customParam]) => {
|
||||||
|
return (
|
||||||
|
`${INDEX_PREFIX}.${key}` !== index && selected === customParam.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
for (const [key, customParam] of Object.entries(customParamsValues)) {
|
return valid || 'Custom Parameter must be unique';
|
||||||
if (`${INDEX_PREFIX}.${key}` !== index) {
|
};
|
||||||
if (selected === customParam.name) {
|
|
||||||
valid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid ? true : 'Custom Parameter must be unique';
|
const onChange = (inputName: string) => (event: any) => {
|
||||||
|
triggerValidation(inputName);
|
||||||
|
onNameChange(index, event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -42,11 +52,11 @@ const CustomParamSelect: React.FC<Props> = ({ isDisabled, index, name }) => {
|
||||||
required: 'Custom Parameter is required.',
|
required: 'Custom Parameter is required.',
|
||||||
validate: { unique: (selected) => selectedMustBeUniq(selected) },
|
validate: { unique: (selected) => selectedMustBeUniq(selected) },
|
||||||
})}
|
})}
|
||||||
onChange={() => triggerValidation(optInputName)}
|
onChange={onChange(optInputName)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
defaultValue={name}
|
defaultValue={name}
|
||||||
>
|
>
|
||||||
<CustomParamOptions />
|
<CustomParamOptions existingFields={existingFields} />
|
||||||
</select>
|
</select>
|
||||||
<p className="help is-danger">
|
<p className="help is-danger">
|
||||||
<ErrorMessage errors={errors} name={optInputName} />
|
<ErrorMessage errors={errors} name={optInputName} />
|
|
@ -1,6 +1,7 @@
|
||||||
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 { camelCase } from 'lodash';
|
||||||
|
import CUSTOM_PARAMS_OPTIONS from './customParamsOptions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
@ -21,14 +22,14 @@ const CustomParamValue: React.FC<Props> = ({
|
||||||
const selectedParamName = watch(selectInputName, name);
|
const selectedParamName = watch(selectInputName, name);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (selectedParamName) {
|
if (selectedParamName && !defaultValue) {
|
||||||
setValue(
|
setValue(
|
||||||
valInputName,
|
valInputName,
|
||||||
CUSTOM_PARAMS_OPTIONS[selectedParamName].defaultValue,
|
CUSTOM_PARAMS_OPTIONS[selectedParamName].defaultValue,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [selectedParamName]);
|
}, [selectedParamName, setValue, valInputName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -45,7 +46,7 @@ const CustomParamValue: React.FC<Props> = ({
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<p className="help is-danger">
|
<p className="help is-danger">
|
||||||
<ErrorMessage errors={errors} name={valInputName} />
|
<ErrorMessage errors={errors} name={name} />
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { omit, reject, reduce, remove } from 'lodash';
|
||||||
|
|
||||||
|
import { TopicFormCustomParams, TopicConfigByName } from 'redux/interfaces';
|
||||||
|
import CustomParamButton, { CustomParamButtonType } from './CustomParamButton';
|
||||||
|
import CustomParamField from './CustomParamField';
|
||||||
|
|
||||||
|
export const INDEX_PREFIX = 'customParams';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isSubmitting: boolean;
|
||||||
|
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[`${INDEX_PREFIX}.${new Date().getTime()}ts`] = {
|
||||||
|
name: paramName,
|
||||||
|
value: param.value,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const [formCustomParams, setFormCustomParams] = React.useState<
|
||||||
|
TopicFormCustomParams
|
||||||
|
>({
|
||||||
|
byIndex,
|
||||||
|
allIndexes: Object.keys(byIndex),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onAdd = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const newIndex = `${INDEX_PREFIX}.${new Date().getTime()}ts`;
|
||||||
|
|
||||||
|
setFormCustomParams({
|
||||||
|
...formCustomParams,
|
||||||
|
byIndex: {
|
||||||
|
...formCustomParams.byIndex,
|
||||||
|
[newIndex]: { name: '', value: '' },
|
||||||
|
},
|
||||||
|
allIndexes: [newIndex, ...formCustomParams.allIndexes],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemove = (index: string) => {
|
||||||
|
const fieldName = formCustomParams.byIndex[index].name;
|
||||||
|
remove(existingFields, (el) => el === fieldName);
|
||||||
|
setFormCustomParams({
|
||||||
|
...formCustomParams,
|
||||||
|
byIndex: omit(formCustomParams.byIndex, index),
|
||||||
|
allIndexes: reject(formCustomParams.allIndexes, (i) => i === index),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFieldNameChange = (index: string, name: string) => {
|
||||||
|
formCustomParams.byIndex[index].name = name;
|
||||||
|
existingFields.push(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<CustomParamButton
|
||||||
|
className="is-success"
|
||||||
|
type={CustomParamButtonType.plus}
|
||||||
|
onClick={onAdd}
|
||||||
|
btnText="Add Custom Parameter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formCustomParams.allIndexes.map((index) => (
|
||||||
|
<CustomParamField
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
name={formCustomParams.byIndex[index].name}
|
||||||
|
defaultValue={formCustomParams.byIndex[index].value}
|
||||||
|
existingFields={existingFields}
|
||||||
|
onNameChange={onFieldNameChange}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomParams;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RootState, TopicConfigByName } from 'redux/interfaces';
|
||||||
|
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
import CustomParams from './CustomParams';
|
||||||
|
|
||||||
|
interface OwnProps extends RouteComponentProps {
|
||||||
|
isSubmitting: boolean;
|
||||||
|
config?: TopicConfigByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (
|
||||||
|
state: RootState,
|
||||||
|
{ isSubmitting, config }: OwnProps
|
||||||
|
) => ({
|
||||||
|
isSubmitting,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withRouter(connect(mapStateToProps)(CustomParams));
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { TopicCustomParamOption } from 'redux/interfaces';
|
||||||
|
|
||||||
|
interface CustomParamOption {
|
||||||
|
[optionName: string]: TopicCustomParamOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CUSTOM_PARAMS_OPTIONS: CustomParamOption = {
|
||||||
|
'compression.type': {
|
||||||
|
name: 'compression.type',
|
||||||
|
defaultValue: 'producer',
|
||||||
|
},
|
||||||
|
'leader.replication.throttled.replicas': {
|
||||||
|
name: 'leader.replication.throttled.replicas',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
'message.downconversion.enable': {
|
||||||
|
name: 'message.downconversion.enable',
|
||||||
|
defaultValue: 'true',
|
||||||
|
},
|
||||||
|
'segment.jitter.ms': {
|
||||||
|
name: 'segment.jitter.ms',
|
||||||
|
defaultValue: '0',
|
||||||
|
},
|
||||||
|
'flush.ms': {
|
||||||
|
name: 'flush.ms',
|
||||||
|
defaultValue: '9223372036854775807',
|
||||||
|
},
|
||||||
|
'follower.replication.throttled.replicas': {
|
||||||
|
name: 'follower.replication.throttled.replicas',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
'segment.bytes': {
|
||||||
|
name: 'segment.bytes',
|
||||||
|
defaultValue: '1073741824',
|
||||||
|
},
|
||||||
|
'flush.messages': {
|
||||||
|
name: 'flush.messages',
|
||||||
|
defaultValue: '9223372036854775807',
|
||||||
|
},
|
||||||
|
'message.format.version': {
|
||||||
|
name: 'message.format.version',
|
||||||
|
defaultValue: '2.3-IV1',
|
||||||
|
},
|
||||||
|
'file.delete.delay.ms': {
|
||||||
|
name: 'file.delete.delay.ms',
|
||||||
|
defaultValue: '60000',
|
||||||
|
},
|
||||||
|
'max.compaction.lag.ms': {
|
||||||
|
name: 'max.compaction.lag.ms',
|
||||||
|
defaultValue: '9223372036854775807',
|
||||||
|
},
|
||||||
|
'min.compaction.lag.ms': {
|
||||||
|
name: 'min.compaction.lag.ms',
|
||||||
|
defaultValue: '0',
|
||||||
|
},
|
||||||
|
'message.timestamp.type': {
|
||||||
|
name: 'message.timestamp.type',
|
||||||
|
defaultValue: 'CreateTime',
|
||||||
|
},
|
||||||
|
preallocate: {
|
||||||
|
name: 'preallocate',
|
||||||
|
defaultValue: 'false',
|
||||||
|
},
|
||||||
|
'min.cleanable.dirty.ratio': {
|
||||||
|
name: 'min.cleanable.dirty.ratio',
|
||||||
|
defaultValue: '0.5',
|
||||||
|
},
|
||||||
|
'index.interval.bytes': {
|
||||||
|
name: 'index.interval.bytes',
|
||||||
|
defaultValue: '4096',
|
||||||
|
},
|
||||||
|
'unclean.leader.election.enable': {
|
||||||
|
name: 'unclean.leader.election.enable',
|
||||||
|
defaultValue: 'true',
|
||||||
|
},
|
||||||
|
'retention.bytes': {
|
||||||
|
name: 'retention.bytes',
|
||||||
|
defaultValue: '-1',
|
||||||
|
},
|
||||||
|
'delete.retention.ms': {
|
||||||
|
name: 'delete.retention.ms',
|
||||||
|
defaultValue: '86400000',
|
||||||
|
},
|
||||||
|
'segment.ms': {
|
||||||
|
name: 'segment.ms',
|
||||||
|
defaultValue: '604800000',
|
||||||
|
},
|
||||||
|
'message.timestamp.difference.max.ms': {
|
||||||
|
name: 'message.timestamp.difference.max.ms',
|
||||||
|
defaultValue: '9223372036854775807',
|
||||||
|
},
|
||||||
|
'segment.index.bytes': {
|
||||||
|
name: 'segment.index.bytes',
|
||||||
|
defaultValue: '10485760',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CUSTOM_PARAMS_OPTIONS;
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||||
|
import { clusterTopicsPath, clusterTopicPath } from 'lib/paths';
|
||||||
|
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
clusterName: ClusterName;
|
||||||
|
topicName?: TopicName;
|
||||||
|
current: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormBreadcrumbs: React.FC<Props> = ({
|
||||||
|
clusterName,
|
||||||
|
topicName,
|
||||||
|
current,
|
||||||
|
}) => {
|
||||||
|
const allTopicsLink = {
|
||||||
|
href: clusterTopicsPath(clusterName),
|
||||||
|
label: 'All Topics',
|
||||||
|
};
|
||||||
|
const links = topicName
|
||||||
|
? [
|
||||||
|
allTopicsLink,
|
||||||
|
{ href: clusterTopicPath(clusterName, topicName), label: topicName },
|
||||||
|
]
|
||||||
|
: [allTopicsLink];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="level-item level-left">
|
||||||
|
<Breadcrumb links={links}>{current}</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormBreadcrumbs;
|
|
@ -0,0 +1,170 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useFormContext, ErrorMessage } from 'react-hook-form';
|
||||||
|
import { TOPIC_NAME_VALIDATION_PATTERN, BYTES_IN_GB } from 'lib/constants';
|
||||||
|
import { CleanupPolicy, TopicName, TopicConfigByName } from 'redux/interfaces';
|
||||||
|
import CustomParamsContainer from './CustomParams/CustomParamsContainer';
|
||||||
|
import TimeToRetain from './TimeToRetain';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
topicName?: TopicName;
|
||||||
|
config?: TopicConfigByName;
|
||||||
|
isEditing?: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopicForm: React.FC<Props> = ({
|
||||||
|
topicName,
|
||||||
|
config,
|
||||||
|
isEditing,
|
||||||
|
isSubmitting,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const { register, errors } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-three-quarters">
|
||||||
|
<label className="label">Topic Name *</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="Topic Name"
|
||||||
|
ref={register({
|
||||||
|
required: 'Topic Name is required.',
|
||||||
|
pattern: {
|
||||||
|
value: TOPIC_NAME_VALIDATION_PATTERN,
|
||||||
|
message: 'Only alphanumeric, _, -, and . allowed',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
defaultValue={topicName}
|
||||||
|
name="name"
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={isEditing || isSubmitting}
|
||||||
|
/>
|
||||||
|
<p className="help is-danger">
|
||||||
|
<ErrorMessage errors={errors} name="name" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column">
|
||||||
|
<label className="label">Number of partitions *</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
placeholder="Number of partitions"
|
||||||
|
defaultValue="1"
|
||||||
|
ref={register({ required: 'Number of partitions is required.' })}
|
||||||
|
name="partitions"
|
||||||
|
disabled={isEditing || isSubmitting}
|
||||||
|
/>
|
||||||
|
<p className="help is-danger">
|
||||||
|
<ErrorMessage errors={errors} name="partitions" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<label className="label">Replication Factor *</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
placeholder="Replication Factor"
|
||||||
|
defaultValue="1"
|
||||||
|
ref={register({ required: 'Replication Factor is required.' })}
|
||||||
|
name="replicationFactor"
|
||||||
|
disabled={isEditing || isSubmitting}
|
||||||
|
/>
|
||||||
|
<p className="help is-danger">
|
||||||
|
<ErrorMessage errors={errors} name="replicationFactor" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column">
|
||||||
|
<label className="label">Min In Sync Replicas *</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
placeholder="Min In Sync Replicas"
|
||||||
|
defaultValue="1"
|
||||||
|
ref={register({ required: 'Min In Sync Replicas is required.' })}
|
||||||
|
name="minInSyncReplicas"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<p className="help is-danger">
|
||||||
|
<ErrorMessage errors={errors} name="minInSyncReplicas" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-one-third">
|
||||||
|
<label className="label">Cleanup policy</label>
|
||||||
|
<div className="select is-block">
|
||||||
|
<select
|
||||||
|
defaultValue={CleanupPolicy.Delete}
|
||||||
|
name="cleanupPolicy"
|
||||||
|
ref={register}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
<option value={CleanupPolicy.Delete}>Delete</option>
|
||||||
|
<option value={CleanupPolicy.Compact}>Compact</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-one-third">
|
||||||
|
<TimeToRetain isSubmitting={isSubmitting} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-one-third">
|
||||||
|
<label className="label">Max size on disk in GB</label>
|
||||||
|
<div className="select is-block">
|
||||||
|
<select
|
||||||
|
defaultValue={-1}
|
||||||
|
name="retentionBytes"
|
||||||
|
ref={register}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
<option value={-1}>Not Set</option>
|
||||||
|
<option value={BYTES_IN_GB}>1 GB</option>
|
||||||
|
<option value={BYTES_IN_GB * 10}>10 GB</option>
|
||||||
|
<option value={BYTES_IN_GB * 20}>20 GB</option>
|
||||||
|
<option value={BYTES_IN_GB * 50}>50 GB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<label className="label">Maximum message size in bytes *</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
defaultValue="1000012"
|
||||||
|
ref={register({
|
||||||
|
required: 'Maximum message size in bytes is required',
|
||||||
|
})}
|
||||||
|
name="maxMessageBytes"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<p className="help is-danger">
|
||||||
|
<ErrorMessage errors={errors} name="maxMessageBytes" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomParamsContainer isSubmitting={isSubmitting} config={config} />
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
className="button is-primary"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopicForm;
|
|
@ -23,3 +23,8 @@ export const clusterTopicMessagesPath = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
topicName: TopicName
|
topicName: TopicName
|
||||||
) => `${clusterTopicsPath(clusterName)}/${topicName}/messages`;
|
) => `${clusterTopicsPath(clusterName)}/${topicName}/messages`;
|
||||||
|
|
||||||
|
export const clusterTopicsTopicEditPath = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
topicName: TopicName
|
||||||
|
) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export enum ActionType {
|
enum ActionType {
|
||||||
GET_CLUSTERS__REQUEST = 'GET_CLUSTERS__REQUEST',
|
GET_CLUSTERS__REQUEST = 'GET_CLUSTERS__REQUEST',
|
||||||
GET_CLUSTERS__SUCCESS = 'GET_CLUSTERS__SUCCESS',
|
GET_CLUSTERS__SUCCESS = 'GET_CLUSTERS__SUCCESS',
|
||||||
GET_CLUSTERS__FAILURE = 'GET_CLUSTERS__FAILURE',
|
GET_CLUSTERS__FAILURE = 'GET_CLUSTERS__FAILURE',
|
||||||
|
@ -27,6 +27,10 @@ export enum ActionType {
|
||||||
POST_TOPIC__SUCCESS = 'POST_TOPIC__SUCCESS',
|
POST_TOPIC__SUCCESS = 'POST_TOPIC__SUCCESS',
|
||||||
POST_TOPIC__FAILURE = 'POST_TOPIC__FAILURE',
|
POST_TOPIC__FAILURE = 'POST_TOPIC__FAILURE',
|
||||||
|
|
||||||
|
PATCH_TOPIC__REQUEST = 'PATCH_TOPIC__REQUEST',
|
||||||
|
PATCH_TOPIC__SUCCESS = 'PATCH_TOPIC__SUCCESS',
|
||||||
|
PATCH_TOPIC__FAILURE = 'PATCH_TOPIC__FAILURE',
|
||||||
|
|
||||||
GET_CONSUMER_GROUPS__REQUEST = 'GET_CONSUMER_GROUPS__REQUEST',
|
GET_CONSUMER_GROUPS__REQUEST = 'GET_CONSUMER_GROUPS__REQUEST',
|
||||||
GET_CONSUMER_GROUPS__SUCCESS = 'GET_CONSUMER_GROUPS__SUCCESS',
|
GET_CONSUMER_GROUPS__SUCCESS = 'GET_CONSUMER_GROUPS__SUCCESS',
|
||||||
GET_CONSUMER_GROUPS__FAILURE = 'GET_CONSUMER_GROUPS__FAILURE',
|
GET_CONSUMER_GROUPS__FAILURE = 'GET_CONSUMER_GROUPS__FAILURE',
|
||||||
|
@ -34,4 +38,6 @@ export enum ActionType {
|
||||||
GET_CONSUMER_GROUP_DETAILS__REQUEST = 'GET_CONSUMER_GROUP_DETAILS__REQUEST',
|
GET_CONSUMER_GROUP_DETAILS__REQUEST = 'GET_CONSUMER_GROUP_DETAILS__REQUEST',
|
||||||
GET_CONSUMER_GROUP_DETAILS__SUCCESS = 'GET_CONSUMER_GROUP_DETAILS__SUCCESS',
|
GET_CONSUMER_GROUP_DETAILS__SUCCESS = 'GET_CONSUMER_GROUP_DETAILS__SUCCESS',
|
||||||
GET_CONSUMER_GROUP_DETAILS__FAILURE = 'GET_CONSUMER_GROUP_DETAILS__FAILURE',
|
GET_CONSUMER_GROUP_DETAILS__FAILURE = 'GET_CONSUMER_GROUP_DETAILS__FAILURE',
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default ActionType;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { createAsyncAction } from 'typesafe-actions';
|
import { createAsyncAction } from 'typesafe-actions';
|
||||||
import { ActionType } from 'redux/actionType';
|
import ActionType from 'redux/actionType';
|
||||||
import { ConsumerGroup, ConsumerGroupID, ConsumerGroupDetails } from '../interfaces/consumerGroup';
|
|
||||||
import {
|
import {
|
||||||
Broker,
|
Broker,
|
||||||
BrokerMetrics,
|
BrokerMetrics,
|
||||||
|
@ -9,58 +8,71 @@ import {
|
||||||
TopicConfig,
|
TopicConfig,
|
||||||
TopicDetails,
|
TopicDetails,
|
||||||
TopicName,
|
TopicName,
|
||||||
|
ConsumerGroup,
|
||||||
|
ConsumerGroupDetails,
|
||||||
|
ConsumerGroupID,
|
||||||
} from 'redux/interfaces';
|
} from 'redux/interfaces';
|
||||||
|
|
||||||
export const fetchBrokersAction = createAsyncAction(
|
export const fetchBrokersAction = createAsyncAction(
|
||||||
ActionType.GET_BROKERS__REQUEST,
|
ActionType.GET_BROKERS__REQUEST,
|
||||||
ActionType.GET_BROKERS__SUCCESS,
|
ActionType.GET_BROKERS__SUCCESS,
|
||||||
ActionType.GET_BROKERS__FAILURE,
|
ActionType.GET_BROKERS__FAILURE
|
||||||
)<undefined, Broker[], undefined>();
|
)<undefined, Broker[], undefined>();
|
||||||
|
|
||||||
export const fetchBrokerMetricsAction = createAsyncAction(
|
export const fetchBrokerMetricsAction = createAsyncAction(
|
||||||
ActionType.GET_BROKER_METRICS__REQUEST,
|
ActionType.GET_BROKER_METRICS__REQUEST,
|
||||||
ActionType.GET_BROKER_METRICS__SUCCESS,
|
ActionType.GET_BROKER_METRICS__SUCCESS,
|
||||||
ActionType.GET_BROKER_METRICS__FAILURE,
|
ActionType.GET_BROKER_METRICS__FAILURE
|
||||||
)<undefined, BrokerMetrics, undefined>();
|
)<undefined, BrokerMetrics, undefined>();
|
||||||
|
|
||||||
export const fetchClusterListAction = createAsyncAction(
|
export const fetchClusterListAction = createAsyncAction(
|
||||||
ActionType.GET_CLUSTERS__REQUEST,
|
ActionType.GET_CLUSTERS__REQUEST,
|
||||||
ActionType.GET_CLUSTERS__SUCCESS,
|
ActionType.GET_CLUSTERS__SUCCESS,
|
||||||
ActionType.GET_CLUSTERS__FAILURE,
|
ActionType.GET_CLUSTERS__FAILURE
|
||||||
)<undefined, Cluster[], undefined>();
|
)<undefined, Cluster[], undefined>();
|
||||||
|
|
||||||
export const fetchTopicListAction = createAsyncAction(
|
export const fetchTopicListAction = 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
|
||||||
)<undefined, Topic[], undefined>();
|
)<undefined, Topic[], undefined>();
|
||||||
|
|
||||||
export const fetchTopicDetailsAction = createAsyncAction(
|
export const fetchTopicDetailsAction = createAsyncAction(
|
||||||
ActionType.GET_TOPIC_DETAILS__REQUEST,
|
ActionType.GET_TOPIC_DETAILS__REQUEST,
|
||||||
ActionType.GET_TOPIC_DETAILS__SUCCESS,
|
ActionType.GET_TOPIC_DETAILS__SUCCESS,
|
||||||
ActionType.GET_TOPIC_DETAILS__FAILURE,
|
ActionType.GET_TOPIC_DETAILS__FAILURE
|
||||||
)<undefined, { topicName: TopicName, details: TopicDetails }, undefined>();
|
)<undefined, { topicName: TopicName; details: TopicDetails }, undefined>();
|
||||||
|
|
||||||
export const fetchTopicConfigAction = createAsyncAction(
|
export const fetchTopicConfigAction = createAsyncAction(
|
||||||
ActionType.GET_TOPIC_CONFIG__REQUEST,
|
ActionType.GET_TOPIC_CONFIG__REQUEST,
|
||||||
ActionType.GET_TOPIC_CONFIG__SUCCESS,
|
ActionType.GET_TOPIC_CONFIG__SUCCESS,
|
||||||
ActionType.GET_TOPIC_CONFIG__FAILURE,
|
ActionType.GET_TOPIC_CONFIG__FAILURE
|
||||||
)<undefined, { topicName: TopicName, config: TopicConfig[] }, undefined>();
|
)<undefined, { topicName: TopicName; config: TopicConfig[] }, undefined>();
|
||||||
|
|
||||||
export const createTopicAction = createAsyncAction(
|
export const createTopicAction = createAsyncAction(
|
||||||
ActionType.POST_TOPIC__REQUEST,
|
ActionType.POST_TOPIC__REQUEST,
|
||||||
ActionType.POST_TOPIC__SUCCESS,
|
ActionType.POST_TOPIC__SUCCESS,
|
||||||
ActionType.POST_TOPIC__FAILURE,
|
ActionType.POST_TOPIC__FAILURE
|
||||||
|
)<undefined, Topic, undefined>();
|
||||||
|
|
||||||
|
export const updateTopicAction = createAsyncAction(
|
||||||
|
ActionType.PATCH_TOPIC__REQUEST,
|
||||||
|
ActionType.PATCH_TOPIC__SUCCESS,
|
||||||
|
ActionType.PATCH_TOPIC__FAILURE
|
||||||
)<undefined, Topic, undefined>();
|
)<undefined, Topic, undefined>();
|
||||||
|
|
||||||
export const fetchConsumerGroupsAction = createAsyncAction(
|
export const fetchConsumerGroupsAction = createAsyncAction(
|
||||||
ActionType.GET_CONSUMER_GROUPS__REQUEST,
|
ActionType.GET_CONSUMER_GROUPS__REQUEST,
|
||||||
ActionType.GET_CONSUMER_GROUPS__SUCCESS,
|
ActionType.GET_CONSUMER_GROUPS__SUCCESS,
|
||||||
ActionType.GET_CONSUMER_GROUPS__FAILURE,
|
ActionType.GET_CONSUMER_GROUPS__FAILURE
|
||||||
)<undefined, ConsumerGroup[], undefined>();
|
)<undefined, ConsumerGroup[], undefined>();
|
||||||
|
|
||||||
export const fetchConsumerGroupDetailsAction = createAsyncAction(
|
export const fetchConsumerGroupDetailsAction = createAsyncAction(
|
||||||
ActionType.GET_CONSUMER_GROUP_DETAILS__REQUEST,
|
ActionType.GET_CONSUMER_GROUP_DETAILS__REQUEST,
|
||||||
ActionType.GET_CONSUMER_GROUP_DETAILS__SUCCESS,
|
ActionType.GET_CONSUMER_GROUP_DETAILS__SUCCESS,
|
||||||
ActionType.GET_CONSUMER_GROUP_DETAILS__FAILURE,
|
ActionType.GET_CONSUMER_GROUP_DETAILS__FAILURE
|
||||||
)<undefined, { consumerGroupID: ConsumerGroupID, details: ConsumerGroupDetails }, undefined>();
|
)<
|
||||||
|
undefined,
|
||||||
|
{ consumerGroupID: ConsumerGroupID; details: ConsumerGroupDetails },
|
||||||
|
undefined
|
||||||
|
>();
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
import * as api from 'redux/api';
|
import * as api from 'redux/api';
|
||||||
import * as actions from './actions';
|
|
||||||
import { ConsumerGroupID } from '../interfaces/consumerGroup';
|
|
||||||
import {
|
import {
|
||||||
|
ConsumerGroupID,
|
||||||
PromiseThunk,
|
PromiseThunk,
|
||||||
Cluster,
|
Cluster,
|
||||||
ClusterName,
|
ClusterName,
|
||||||
TopicFormData,
|
TopicFormData,
|
||||||
TopicName, Topic,
|
TopicName,
|
||||||
|
Topic,
|
||||||
} from 'redux/interfaces';
|
} from 'redux/interfaces';
|
||||||
|
|
||||||
export const fetchBrokers = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
export const fetchBrokers = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(actions.fetchBrokersAction.request());
|
dispatch(actions.fetchBrokersAction.request());
|
||||||
try {
|
try {
|
||||||
const payload = await api.getBrokers(clusterName);
|
const payload = await api.getBrokers(clusterName);
|
||||||
|
@ -19,7 +23,9 @@ export const fetchBrokers = (clusterName: ClusterName): PromiseThunk<void> => as
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchBrokerMetrics = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
|
export const fetchBrokerMetrics = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(actions.fetchBrokerMetricsAction.request());
|
dispatch(actions.fetchBrokerMetricsAction.request());
|
||||||
try {
|
try {
|
||||||
const payload = await api.getBrokerMetrics(clusterName);
|
const payload = await api.getBrokerMetrics(clusterName);
|
||||||
|
@ -39,7 +45,9 @@ export const fetchClustersList = (): PromiseThunk<void> => async (dispatch) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTopicList = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
|
export const fetchTopicList = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(actions.fetchTopicListAction.request());
|
dispatch(actions.fetchTopicListAction.request());
|
||||||
try {
|
try {
|
||||||
const topics = await api.getTopics(clusterName);
|
const topics = await api.getTopics(clusterName);
|
||||||
|
@ -49,17 +57,28 @@ export const fetchTopicList = (clusterName: ClusterName): PromiseThunk<void> =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTopicDetails = (clusterName: ClusterName, topicName: TopicName): PromiseThunk<void> => async (dispatch) => {
|
export const fetchTopicDetails = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
topicName: TopicName
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(actions.fetchTopicDetailsAction.request());
|
dispatch(actions.fetchTopicDetailsAction.request());
|
||||||
try {
|
try {
|
||||||
const topicDetails = await api.getTopicDetails(clusterName, topicName);
|
const topicDetails = await api.getTopicDetails(clusterName, topicName);
|
||||||
dispatch(actions.fetchTopicDetailsAction.success({ topicName, details: topicDetails }));
|
dispatch(
|
||||||
|
actions.fetchTopicDetailsAction.success({
|
||||||
|
topicName,
|
||||||
|
details: topicDetails,
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(actions.fetchTopicDetailsAction.failure());
|
dispatch(actions.fetchTopicDetailsAction.failure());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTopicConfig = (clusterName: ClusterName, topicName: TopicName): PromiseThunk<void> => async (dispatch) => {
|
export const fetchTopicConfig = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
topicName: TopicName
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(actions.fetchTopicConfigAction.request());
|
dispatch(actions.fetchTopicConfigAction.request());
|
||||||
try {
|
try {
|
||||||
const config = await api.getTopicConfig(clusterName, topicName);
|
const config = await api.getTopicConfig(clusterName, topicName);
|
||||||
|
@ -69,7 +88,10 @@ export const fetchTopicConfig = (clusterName: ClusterName, topicName: TopicName)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTopic = (clusterName: ClusterName, form: TopicFormData): PromiseThunk<void> => async (dispatch) => {
|
export const createTopic = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
form: TopicFormData
|
||||||
|
): 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 api.postTopic(clusterName, form);
|
||||||
|
@ -79,7 +101,22 @@ export const createTopic = (clusterName: ClusterName, form: TopicFormData): Prom
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchConsumerGroupsList = (clusterName: ClusterName): PromiseThunk<void> => async (dispatch) => {
|
export const updateTopic = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
form: TopicFormData
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
|
dispatch(actions.updateTopicAction.request());
|
||||||
|
try {
|
||||||
|
const topic: Topic = await api.patchTopic(clusterName, form);
|
||||||
|
dispatch(actions.updateTopicAction.success(topic));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(actions.updateTopicAction.failure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchConsumerGroupsList = (
|
||||||
|
clusterName: ClusterName
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(actions.fetchConsumerGroupsAction.request());
|
dispatch(actions.fetchConsumerGroupsAction.request());
|
||||||
try {
|
try {
|
||||||
const consumerGroups = await api.getConsumerGroups(clusterName);
|
const consumerGroups = await api.getConsumerGroups(clusterName);
|
||||||
|
@ -89,11 +126,22 @@ export const fetchConsumerGroupsList = (clusterName: ClusterName): PromiseThunk<
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchConsumerGroupDetails = (clusterName: ClusterName, consumerGroupID: ConsumerGroupID): PromiseThunk<void> => async (dispatch) => {
|
export const fetchConsumerGroupDetails = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
consumerGroupID: ConsumerGroupID
|
||||||
|
): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(actions.fetchConsumerGroupDetailsAction.request());
|
dispatch(actions.fetchConsumerGroupDetailsAction.request());
|
||||||
try {
|
try {
|
||||||
const consumerGroupDetails = await api.getConsumerGroupDetails(clusterName, consumerGroupID);
|
const consumerGroupDetails = await api.getConsumerGroupDetails(
|
||||||
dispatch(actions.fetchConsumerGroupDetailsAction.success({ consumerGroupID, details: consumerGroupDetails }));
|
clusterName,
|
||||||
|
consumerGroupID
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
actions.fetchConsumerGroupDetailsAction.success({
|
||||||
|
consumerGroupID,
|
||||||
|
details: consumerGroupDetails,
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(actions.fetchConsumerGroupDetailsAction.failure());
|
dispatch(actions.fetchConsumerGroupDetailsAction.failure());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,29 @@
|
||||||
import { reduce } from 'lodash';
|
|
||||||
import {
|
import {
|
||||||
ClusterName,
|
|
||||||
Topic,
|
|
||||||
TopicConfig,
|
|
||||||
TopicDetails,
|
|
||||||
TopicFormCustomParam,
|
|
||||||
TopicFormData,
|
|
||||||
TopicName,
|
TopicName,
|
||||||
|
Topic,
|
||||||
|
ClusterName,
|
||||||
|
TopicDetails,
|
||||||
|
TopicConfig,
|
||||||
|
TopicFormData,
|
||||||
|
TopicFormCustomParam,
|
||||||
|
TopicFormFormattedParams,
|
||||||
|
TopicFormCustomParams,
|
||||||
} from 'redux/interfaces';
|
} from 'redux/interfaces';
|
||||||
import { BASE_PARAMS, BASE_URL } from 'lib/constants';
|
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 = (
|
export const getTopicConfig = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
|
@ -31,10 +46,6 @@ export const getTopics = (clusterName: ClusterName): Promise<Topic[]> =>
|
||||||
...BASE_PARAMS,
|
...BASE_PARAMS,
|
||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
|
|
||||||
interface Result {
|
|
||||||
[index: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const postTopic = (
|
export const postTopic = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
form: TopicFormData
|
form: TopicFormData
|
||||||
|
@ -50,18 +61,6 @@ export const postTopic = (
|
||||||
minInSyncReplicas,
|
minInSyncReplicas,
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const customParams =
|
|
||||||
(form.customParams &&
|
|
||||||
reduce(
|
|
||||||
Object.values(form.customParams),
|
|
||||||
(result: Result, customParam: TopicFormCustomParam) => {
|
|
||||||
result[customParam.name] = customParam.value;
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)) ||
|
|
||||||
{};
|
|
||||||
|
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
name,
|
name,
|
||||||
partitions,
|
partitions,
|
||||||
|
@ -72,7 +71,7 @@ export const postTopic = (
|
||||||
'retention.bytes': retentionBytes,
|
'retention.bytes': retentionBytes,
|
||||||
'max.message.bytes': maxMessageBytes,
|
'max.message.bytes': maxMessageBytes,
|
||||||
'min.insync.replicas': minInSyncReplicas,
|
'min.insync.replicas': minInSyncReplicas,
|
||||||
...customParams,
|
...formatCustomParams(form.customParams),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,3 +81,33 @@ export const postTopic = (
|
||||||
body,
|
body,
|
||||||
}).then((res) => res.json());
|
}).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());
|
||||||
|
};
|
||||||
|
|
|
@ -11,6 +11,12 @@ export interface TopicConfig {
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TopicConfigByName {
|
||||||
|
byName: {
|
||||||
|
[paramName: string]: TopicConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface TopicReplica {
|
export interface TopicReplica {
|
||||||
broker: number;
|
broker: number;
|
||||||
leader: boolean;
|
leader: boolean;
|
||||||
|
@ -63,6 +69,10 @@ export interface TopicsState {
|
||||||
allNames: TopicName[];
|
allNames: TopicName[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TopicFormFormattedParams {
|
||||||
|
[name: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TopicFormData {
|
export interface TopicFormData {
|
||||||
name: string;
|
name: string;
|
||||||
partitions: number;
|
partitions: number;
|
||||||
|
@ -72,7 +82,5 @@ export interface TopicFormData {
|
||||||
retentionMs: number;
|
retentionMs: number;
|
||||||
retentionBytes: number;
|
retentionBytes: number;
|
||||||
maxMessageBytes: number;
|
maxMessageBytes: number;
|
||||||
customParams: {
|
customParams: TopicFormCustomParams;
|
||||||
[index: string]: TopicFormCustomParam;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,7 @@ import {
|
||||||
ZooKeeperStatus,
|
ZooKeeperStatus,
|
||||||
BrokerMetrics,
|
BrokerMetrics,
|
||||||
} from 'redux/interfaces';
|
} from 'redux/interfaces';
|
||||||
import {
|
import ActionType from 'redux/actionType';
|
||||||
ActionType,
|
|
||||||
} from 'redux/actionType';
|
|
||||||
|
|
||||||
export const initialState: BrokersState = {
|
export const initialState: BrokersState = {
|
||||||
items: [],
|
items: [],
|
||||||
|
@ -21,12 +19,17 @@ export const initialState: BrokersState = {
|
||||||
diskUsage: [],
|
diskUsage: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateBrokerSegmentSize = (state: BrokersState, payload: BrokerMetrics) => {
|
const updateBrokerSegmentSize = (
|
||||||
|
state: BrokersState,
|
||||||
|
payload: BrokerMetrics
|
||||||
|
) => {
|
||||||
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(({ brokerId }) => brokerId === broker.brokerId);
|
const brokerMetrics = diskUsage.find(
|
||||||
|
({ brokerId }) => brokerId === broker.brokerId
|
||||||
|
);
|
||||||
if (brokerMetrics !== undefined) {
|
if (brokerMetrics !== undefined) {
|
||||||
return { ...broker, ...brokerMetrics };
|
return { ...broker, ...brokerMetrics };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Cluster, Action } from 'redux/interfaces';
|
import { Cluster, Action } from 'redux/interfaces';
|
||||||
import { ActionType } from 'redux/actionType';
|
import ActionType from 'redux/actionType';
|
||||||
|
|
||||||
export const initialState: Cluster[] = [];
|
export const initialState: Cluster[] = [];
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
import { Action, ConsumerGroup } from 'redux/interfaces';
|
import { Action, ConsumerGroup, ConsumerGroupsState } from 'redux/interfaces';
|
||||||
import { ActionType } from 'redux/actionType';
|
import ActionType from 'redux/actionType';
|
||||||
import { ConsumerGroupsState } from '../../interfaces/consumerGroup';
|
|
||||||
|
|
||||||
export const initialState: ConsumerGroupsState = {
|
export const initialState: ConsumerGroupsState = {
|
||||||
byID: {},
|
byID: {},
|
||||||
allIDs: []
|
allIDs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConsumerGroupsList = (state: ConsumerGroupsState, payload: ConsumerGroup[]): ConsumerGroupsState => {
|
const updateConsumerGroupsList = (
|
||||||
|
state: ConsumerGroupsState,
|
||||||
|
payload: ConsumerGroup[]
|
||||||
|
): ConsumerGroupsState => {
|
||||||
const initialMemo: ConsumerGroupsState = {
|
const initialMemo: ConsumerGroupsState = {
|
||||||
...state,
|
...state,
|
||||||
allIDs: []
|
allIDs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return payload.reduce(
|
return payload.reduce((memo: ConsumerGroupsState, consumerGroup) => {
|
||||||
(memo: ConsumerGroupsState, consumerGroup) => {
|
const { consumerGroupId } = consumerGroup;
|
||||||
const {consumerGroupId} = consumerGroup;
|
|
||||||
memo.byID[consumerGroupId] = {
|
memo.byID[consumerGroupId] = {
|
||||||
...memo.byID[consumerGroupId],
|
...memo.byID[consumerGroupId],
|
||||||
...consumerGroup,
|
...consumerGroup,
|
||||||
|
@ -23,9 +24,7 @@ const updateConsumerGroupsList = (state: ConsumerGroupsState, payload: ConsumerG
|
||||||
memo.allIDs.push(consumerGroupId);
|
memo.allIDs.push(consumerGroupId);
|
||||||
|
|
||||||
return memo;
|
return memo;
|
||||||
},
|
}, initialMemo);
|
||||||
initialMemo,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state = initialState, action: Action): ConsumerGroupsState => {
|
const reducer = (state = initialState, action: Action): ConsumerGroupsState => {
|
||||||
|
@ -40,8 +39,8 @@ const reducer = (state = initialState, action: Action): ConsumerGroupsState => {
|
||||||
[action.payload.consumerGroupID]: {
|
[action.payload.consumerGroupID]: {
|
||||||
...state.byID[action.payload.consumerGroupID],
|
...state.byID[action.payload.consumerGroupID],
|
||||||
...action.payload.details,
|
...action.payload.details,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Action, TopicsState, Topic } from 'redux/interfaces';
|
import { Action, TopicsState, Topic } from 'redux/interfaces';
|
||||||
import { ActionType } from 'redux/actionType';
|
import ActionType from 'redux/actionType';
|
||||||
|
|
||||||
export const initialState: TopicsState = {
|
export const initialState: TopicsState = {
|
||||||
byName: {},
|
byName: {},
|
||||||
|
@ -12,9 +12,8 @@ const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => {
|
||||||
allNames: [],
|
allNames: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return payload.reduce(
|
return payload.reduce((memo: TopicsState, topic) => {
|
||||||
(memo: TopicsState, topic) => {
|
const { name } = topic;
|
||||||
const {name} = topic;
|
|
||||||
memo.byName[name] = {
|
memo.byName[name] = {
|
||||||
...memo.byName[name],
|
...memo.byName[name],
|
||||||
...topic,
|
...topic,
|
||||||
|
@ -22,14 +21,12 @@ const updateTopicList = (state: TopicsState, payload: Topic[]): TopicsState => {
|
||||||
memo.allNames.push(name);
|
memo.allNames.push(name);
|
||||||
|
|
||||||
return memo;
|
return memo;
|
||||||
},
|
}, initialMemo);
|
||||||
initialMemo,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
|
const addToTopicList = (state: TopicsState, payload: Topic): TopicsState => {
|
||||||
const newState: TopicsState = {
|
const newState: TopicsState = {
|
||||||
...state
|
...state,
|
||||||
};
|
};
|
||||||
newState.allNames.push(payload.name);
|
newState.allNames.push(payload.name);
|
||||||
newState.byName[payload.name] = payload;
|
newState.byName[payload.name] = payload;
|
||||||
|
@ -48,8 +45,8 @@ const reducer = (state = initialState, action: Action): TopicsState => {
|
||||||
[action.payload.topicName]: {
|
[action.payload.topicName]: {
|
||||||
...state.byName[action.payload.topicName],
|
...state.byName[action.payload.topicName],
|
||||||
...action.payload.details,
|
...action.payload.details,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
case ActionType.GET_TOPIC_CONFIG__SUCCESS:
|
case ActionType.GET_TOPIC_CONFIG__SUCCESS:
|
||||||
return {
|
return {
|
||||||
|
@ -59,8 +56,8 @@ const reducer = (state = initialState, action: Action): TopicsState => {
|
||||||
[action.payload.topicName]: {
|
[action.payload.topicName]: {
|
||||||
...state.byName[action.payload.topicName],
|
...state.byName[action.payload.topicName],
|
||||||
config: action.payload.config,
|
config: action.payload.config,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
case ActionType.POST_TOPIC__SUCCESS:
|
case ActionType.POST_TOPIC__SUCCESS:
|
||||||
return addToTopicList(state, action.payload);
|
return addToTopicList(state, action.payload);
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { RootState, TopicName, FetchStatus, TopicsState } from 'redux/interfaces';
|
import {
|
||||||
|
RootState,
|
||||||
|
TopicName,
|
||||||
|
FetchStatus,
|
||||||
|
TopicsState,
|
||||||
|
TopicConfigByName,
|
||||||
|
} from 'redux/interfaces';
|
||||||
import { createFetchingSelector } from 'redux/reducers/loader/selectors';
|
import { createFetchingSelector } from 'redux/reducers/loader/selectors';
|
||||||
|
|
||||||
const topicsState = ({ topics }: RootState): TopicsState => topics;
|
const topicsState = ({ topics }: RootState): TopicsState => topics;
|
||||||
|
@ -8,28 +14,36 @@ const getAllNames = (state: RootState) => topicsState(state).allNames;
|
||||||
const getTopicMap = (state: RootState) => topicsState(state).byName;
|
const getTopicMap = (state: RootState) => topicsState(state).byName;
|
||||||
|
|
||||||
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
|
const getTopicListFetchingStatus = createFetchingSelector('GET_TOPICS');
|
||||||
const getTopicDetailsFetchingStatus = createFetchingSelector('GET_TOPIC_DETAILS');
|
const getTopicDetailsFetchingStatus = createFetchingSelector(
|
||||||
|
'GET_TOPIC_DETAILS'
|
||||||
|
);
|
||||||
const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
|
const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
|
||||||
const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
|
const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
|
||||||
|
const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
|
||||||
|
|
||||||
export const getIsTopicListFetched = createSelector(
|
export const getIsTopicListFetched = createSelector(
|
||||||
getTopicListFetchingStatus,
|
getTopicListFetchingStatus,
|
||||||
(status) => status === FetchStatus.fetched,
|
(status) => status === FetchStatus.fetched
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getIsTopicDetailsFetched = createSelector(
|
export const getIsTopicDetailsFetched = createSelector(
|
||||||
getTopicDetailsFetchingStatus,
|
getTopicDetailsFetchingStatus,
|
||||||
(status) => status === FetchStatus.fetched,
|
(status) => status === FetchStatus.fetched
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getTopicConfigFetched = createSelector(
|
export const getTopicConfigFetched = createSelector(
|
||||||
getTopicConfigFetchingStatus,
|
getTopicConfigFetchingStatus,
|
||||||
(status) => status === FetchStatus.fetched,
|
(status) => status === FetchStatus.fetched
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getTopicCreated = createSelector(
|
export const getTopicCreated = createSelector(
|
||||||
getTopicCreationStatus,
|
getTopicCreationStatus,
|
||||||
(status) => status === FetchStatus.fetched,
|
(status) => status === FetchStatus.fetched
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getTopicUpdated = createSelector(
|
||||||
|
getTopicUpdateStatus,
|
||||||
|
(status) => status === FetchStatus.fetched
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getTopicList = createSelector(
|
export const getTopicList = createSelector(
|
||||||
|
@ -40,13 +54,12 @@ export const getTopicList = createSelector(
|
||||||
if (!isFetched) {
|
if (!isFetched) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return allNames.map((name) => byName[name])
|
return allNames.map((name) => byName[name]);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getExternalTopicList = createSelector(
|
export const getExternalTopicList = createSelector(getTopicList, (topics) =>
|
||||||
getTopicList,
|
topics.filter(({ internal }) => !internal)
|
||||||
(topics) => topics.filter(({ internal }) => !internal),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTopicName = (_: RootState, topicName: TopicName) => topicName;
|
const getTopicName = (_: RootState, topicName: TopicName) => topicName;
|
||||||
|
@ -54,7 +67,31 @@ const getTopicName = (_: RootState, topicName: TopicName) => topicName;
|
||||||
export const getTopicByName = createSelector(
|
export const getTopicByName = createSelector(
|
||||||
getTopicMap,
|
getTopicMap,
|
||||||
getTopicName,
|
getTopicName,
|
||||||
(topics, topicName) => topics[topicName],
|
(topics, topicName) => topics[topicName]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getTopicConfig = createSelector(getTopicByName, ({ config }) => config);
|
export const getFullTopic = createSelector(getTopicByName, (topic) =>
|
||||||
|
topic && topic.config && !!topic.partitionCount ? topic : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getTopicConfig = createSelector(
|
||||||
|
getTopicByName,
|
||||||
|
({ config }) => config
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getTopicConfigByParamName = createSelector(
|
||||||
|
getTopicConfig,
|
||||||
|
(config) => {
|
||||||
|
const byParamName: TopicConfigByName = {
|
||||||
|
byName: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
config.forEach((param) => {
|
||||||
|
byParamName.byName[param.name] = param;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return byParamName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
3
package-lock.json
generated
Normal file
3
package-lock.json
generated
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue