|
@@ -1,32 +1,47 @@
|
|
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
|
|
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
|
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
|
|
|
+import {
|
|
|
|
+ CreateTopicMessage,
|
|
|
|
+ Partition,
|
|
|
|
+ TopicMessageSchema,
|
|
|
|
+} from 'generated-sources';
|
|
import React from 'react';
|
|
import React from 'react';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
-import { useHistory, useParams } from 'react-router';
|
|
|
|
|
|
+import { useHistory } from 'react-router';
|
|
import { clusterTopicMessagesPath } from 'lib/paths';
|
|
import { clusterTopicMessagesPath } from 'lib/paths';
|
|
import jsf from 'json-schema-faker';
|
|
import jsf from 'json-schema-faker';
|
|
-import { fetchTopicMessageSchema, messagesApiClient } from 'redux/actions';
|
|
|
|
-import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
|
|
|
-import { alertAdded } from 'redux/reducers/alerts/alertsSlice';
|
|
|
|
-import { now } from 'lodash';
|
|
|
|
-import { Button } from 'components/common/Button/Button';
|
|
|
|
-import { ClusterName, TopicName } from 'redux/interfaces';
|
|
|
|
-import {
|
|
|
|
- getMessageSchemaByTopicName,
|
|
|
|
- getPartitionsByTopicName,
|
|
|
|
- getTopicMessageSchemaFetched,
|
|
|
|
-} from 'redux/reducers/topics/selectors';
|
|
|
|
|
|
|
|
import validateMessage from './validateMessage';
|
|
import validateMessage from './validateMessage';
|
|
|
|
|
|
-interface RouterParams {
|
|
|
|
- clusterName: ClusterName;
|
|
|
|
- topicName: TopicName;
|
|
|
|
|
|
+export interface Props {
|
|
|
|
+ clusterName: string;
|
|
|
|
+ topicName: string;
|
|
|
|
+ fetchTopicMessageSchema: (clusterName: string, topicName: string) => void;
|
|
|
|
+ sendTopicMessage: (
|
|
|
|
+ clusterName: string,
|
|
|
|
+ topicName: string,
|
|
|
|
+ payload: CreateTopicMessage
|
|
|
|
+ ) => void;
|
|
|
|
+ messageSchema: TopicMessageSchema | undefined;
|
|
|
|
+ schemaIsFetched: boolean;
|
|
|
|
+ messageIsSending: boolean;
|
|
|
|
+ partitions: Partition[];
|
|
}
|
|
}
|
|
|
|
|
|
-const SendMessage: React.FC = () => {
|
|
|
|
- const dispatch = useAppDispatch();
|
|
|
|
- const { clusterName, topicName } = useParams<RouterParams>();
|
|
|
|
|
|
+const SendMessage: React.FC<Props> = ({
|
|
|
|
+ clusterName,
|
|
|
|
+ topicName,
|
|
|
|
+ fetchTopicMessageSchema,
|
|
|
|
+ sendTopicMessage,
|
|
|
|
+ messageSchema,
|
|
|
|
+ schemaIsFetched,
|
|
|
|
+ messageIsSending,
|
|
|
|
+ partitions,
|
|
|
|
+}) => {
|
|
|
|
+ const [keyExampleValue, setKeyExampleValue] = React.useState('');
|
|
|
|
+ const [contentExampleValue, setContentExampleValue] = React.useState('');
|
|
|
|
+ const [schemaIsReady, setSchemaIsReady] = React.useState(false);
|
|
|
|
+ const [schemaErrors, setSchemaErrors] = React.useState<string[]>([]);
|
|
const {
|
|
const {
|
|
register,
|
|
register,
|
|
handleSubmit,
|
|
handleSubmit,
|
|
@@ -39,38 +54,27 @@ const SendMessage: React.FC = () => {
|
|
jsf.option('alwaysFakeOptionals', true);
|
|
jsf.option('alwaysFakeOptionals', true);
|
|
|
|
|
|
React.useEffect(() => {
|
|
React.useEffect(() => {
|
|
- dispatch(fetchTopicMessageSchema(clusterName, topicName));
|
|
|
|
|
|
+ fetchTopicMessageSchema(clusterName, topicName);
|
|
}, []);
|
|
}, []);
|
|
-
|
|
|
|
- const messageSchema = useAppSelector((state) =>
|
|
|
|
- getMessageSchemaByTopicName(state, topicName)
|
|
|
|
- );
|
|
|
|
- const partitions = useAppSelector((state) =>
|
|
|
|
- getPartitionsByTopicName(state, topicName)
|
|
|
|
- );
|
|
|
|
- const schemaIsFetched = useAppSelector(getTopicMessageSchemaFetched);
|
|
|
|
-
|
|
|
|
- const keyDefaultValue = React.useMemo(() => {
|
|
|
|
- if (!schemaIsFetched || !messageSchema) {
|
|
|
|
- return undefined;
|
|
|
|
- }
|
|
|
|
- return JSON.stringify(
|
|
|
|
- jsf.generate(JSON.parse(messageSchema.key.schema)),
|
|
|
|
- null,
|
|
|
|
- '\t'
|
|
|
|
- );
|
|
|
|
- }, [messageSchema, schemaIsFetched]);
|
|
|
|
-
|
|
|
|
- const contentDefaultValue = React.useMemo(() => {
|
|
|
|
- if (!schemaIsFetched || !messageSchema) {
|
|
|
|
- return undefined;
|
|
|
|
|
|
+ React.useEffect(() => {
|
|
|
|
+ if (schemaIsFetched && messageSchema) {
|
|
|
|
+ setKeyExampleValue(
|
|
|
|
+ JSON.stringify(
|
|
|
|
+ jsf.generate(JSON.parse(messageSchema.key.schema)),
|
|
|
|
+ null,
|
|
|
|
+ '\t'
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+ setContentExampleValue(
|
|
|
|
+ JSON.stringify(
|
|
|
|
+ jsf.generate(JSON.parse(messageSchema.value.schema)),
|
|
|
|
+ null,
|
|
|
|
+ '\t'
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+ setSchemaIsReady(true);
|
|
}
|
|
}
|
|
- return JSON.stringify(
|
|
|
|
- jsf.generate(JSON.parse(messageSchema.value.schema)),
|
|
|
|
- null,
|
|
|
|
- '\t'
|
|
|
|
- );
|
|
|
|
- }, [messageSchema, schemaIsFetched]);
|
|
|
|
|
|
+ }, [schemaIsFetched]);
|
|
|
|
|
|
const onSubmit = async (data: {
|
|
const onSubmit = async (data: {
|
|
key: string;
|
|
key: string;
|
|
@@ -79,55 +83,30 @@ const SendMessage: React.FC = () => {
|
|
partition: number;
|
|
partition: number;
|
|
}) => {
|
|
}) => {
|
|
if (messageSchema) {
|
|
if (messageSchema) {
|
|
- const { partition, key, content } = data;
|
|
|
|
|
|
+ const key = data.key || keyExampleValue;
|
|
|
|
+ const content = data.content || contentExampleValue;
|
|
|
|
+ const { partition } = data;
|
|
const headers = data.headers ? JSON.parse(data.headers) : undefined;
|
|
const headers = data.headers ? JSON.parse(data.headers) : undefined;
|
|
- const errors = validateMessage(key, content, messageSchema);
|
|
|
|
- if (errors.length > 0) {
|
|
|
|
- dispatch(
|
|
|
|
- alertAdded({
|
|
|
|
- id: `${clusterName}-${topicName}-createTopicMessageError`,
|
|
|
|
- type: 'error',
|
|
|
|
- title: 'Validation Error',
|
|
|
|
- message: (
|
|
|
|
- <ul>
|
|
|
|
- {errors.map((e) => (
|
|
|
|
- <li>{e}</li>
|
|
|
|
- ))}
|
|
|
|
- </ul>
|
|
|
|
- ),
|
|
|
|
- createdAt: now(),
|
|
|
|
- })
|
|
|
|
- );
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
|
|
+ const messageIsValid = await validateMessage(
|
|
|
|
+ key,
|
|
|
|
+ content,
|
|
|
|
+ messageSchema,
|
|
|
|
+ setSchemaErrors
|
|
|
|
+ );
|
|
|
|
|
|
- try {
|
|
|
|
- await messagesApiClient.sendTopicMessages({
|
|
|
|
- clusterName,
|
|
|
|
- topicName,
|
|
|
|
- createTopicMessage: {
|
|
|
|
- key: !key ? null : key,
|
|
|
|
- content: !content ? null : content,
|
|
|
|
- headers,
|
|
|
|
- partition,
|
|
|
|
- },
|
|
|
|
|
|
+ if (messageIsValid) {
|
|
|
|
+ sendTopicMessage(clusterName, topicName, {
|
|
|
|
+ key,
|
|
|
|
+ content,
|
|
|
|
+ headers,
|
|
|
|
+ partition,
|
|
});
|
|
});
|
|
- } catch (e) {
|
|
|
|
- dispatch(
|
|
|
|
- alertAdded({
|
|
|
|
- id: `${clusterName}-${topicName}-sendTopicMessagesError`,
|
|
|
|
- type: 'error',
|
|
|
|
- title: `Error in sending a message to ${topicName}`,
|
|
|
|
- message: e?.message,
|
|
|
|
- createdAt: now(),
|
|
|
|
- })
|
|
|
|
- );
|
|
|
|
|
|
+ history.push(clusterTopicMessagesPath(clusterName, topicName));
|
|
}
|
|
}
|
|
- history.push(clusterTopicMessagesPath(clusterName, topicName));
|
|
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
- if (!schemaIsFetched) {
|
|
|
|
|
|
+ if (!schemaIsReady) {
|
|
return <PageLoader />;
|
|
return <PageLoader />;
|
|
}
|
|
}
|
|
return (
|
|
return (
|
|
@@ -142,7 +121,7 @@ const SendMessage: React.FC = () => {
|
|
<select
|
|
<select
|
|
id="select"
|
|
id="select"
|
|
defaultValue={partitions[0].partition}
|
|
defaultValue={partitions[0].partition}
|
|
- disabled={isSubmitting}
|
|
|
|
|
|
+ disabled={isSubmitting || messageIsSending}
|
|
{...register('partition')}
|
|
{...register('partition')}
|
|
>
|
|
>
|
|
{partitions.map((partition) => (
|
|
{partitions.map((partition) => (
|
|
@@ -163,8 +142,8 @@ const SendMessage: React.FC = () => {
|
|
name="key"
|
|
name="key"
|
|
render={({ field: { name, onChange } }) => (
|
|
render={({ field: { name, onChange } }) => (
|
|
<JSONEditor
|
|
<JSONEditor
|
|
- readOnly={isSubmitting}
|
|
|
|
- defaultValue={keyDefaultValue}
|
|
|
|
|
|
+ readOnly={isSubmitting || messageIsSending}
|
|
|
|
+ defaultValue={keyExampleValue}
|
|
name={name}
|
|
name={name}
|
|
onChange={onChange}
|
|
onChange={onChange}
|
|
/>
|
|
/>
|
|
@@ -178,8 +157,8 @@ const SendMessage: React.FC = () => {
|
|
name="content"
|
|
name="content"
|
|
render={({ field: { name, onChange } }) => (
|
|
render={({ field: { name, onChange } }) => (
|
|
<JSONEditor
|
|
<JSONEditor
|
|
- readOnly={isSubmitting}
|
|
|
|
- defaultValue={contentDefaultValue}
|
|
|
|
|
|
+ readOnly={isSubmitting || messageIsSending}
|
|
|
|
+ defaultValue={contentExampleValue}
|
|
name={name}
|
|
name={name}
|
|
onChange={onChange}
|
|
onChange={onChange}
|
|
/>
|
|
/>
|
|
@@ -195,7 +174,7 @@ const SendMessage: React.FC = () => {
|
|
name="headers"
|
|
name="headers"
|
|
render={({ field: { name, onChange } }) => (
|
|
render={({ field: { name, onChange } }) => (
|
|
<JSONEditor
|
|
<JSONEditor
|
|
- readOnly={isSubmitting}
|
|
|
|
|
|
+ readOnly={isSubmitting || messageIsSending}
|
|
defaultValue="{}"
|
|
defaultValue="{}"
|
|
name={name}
|
|
name={name}
|
|
onChange={onChange}
|
|
onChange={onChange}
|
|
@@ -205,14 +184,22 @@ const SendMessage: React.FC = () => {
|
|
/>
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
- <Button
|
|
|
|
- buttonSize="M"
|
|
|
|
- buttonType="primary"
|
|
|
|
|
|
+ {schemaErrors && (
|
|
|
|
+ <div className="mb-4">
|
|
|
|
+ {schemaErrors.map((err) => (
|
|
|
|
+ <p className="help is-danger" key={err}>
|
|
|
|
+ {err}
|
|
|
|
+ </p>
|
|
|
|
+ ))}
|
|
|
|
+ </div>
|
|
|
|
+ )}
|
|
|
|
+ <button
|
|
type="submit"
|
|
type="submit"
|
|
- disabled={!isDirty || isSubmitting}
|
|
|
|
|
|
+ className="button is-primary"
|
|
|
|
+ disabled={!isDirty || isSubmitting || messageIsSending}
|
|
>
|
|
>
|
|
Send
|
|
Send
|
|
- </Button>
|
|
|
|
|
|
+ </button>
|
|
</form>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
);
|