Skip to content

Commit

Permalink
DBZ-3696 Addition of Topic Groups to UI (debezium#396)
Browse files Browse the repository at this point in the history
* DBZ-3696 Addition of Topic Groups

* Add Topic Group Options section

* using common name field and updates

* Displays alert when topic creation feature disabled

* Update card css, response flags

* Utilize backend service
  • Loading branch information
mdrillin authored Oct 20, 2021
1 parent e4f1aaa commit 99c0bed
Show file tree
Hide file tree
Showing 20 changed files with 1,227 additions and 14 deletions.
10 changes: 10 additions & 0 deletions ui/packages/services/src/globals/globals.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,14 @@ export class GlobalsService extends BaseService {
return this.httpGet<string[]>(endpoint);
}

/**
* Get the enabled state for topic creation for the supplied clusterId
*/
public getTopicCreationEnabled(clusterId: number): Promise<boolean> {
this.logger?.info("[GlobalsService] Getting the enabled state for topic creation.");

const endpoint: string = this.endpoint("/:clusterId/topic-creation-enabled", { clusterId });
return this.httpGet<boolean>(endpoint);
}

}
110 changes: 110 additions & 0 deletions ui/packages/ui/assets/mockResponse/topicCreation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"defaults": [
{
"category": "TOPIC_CREATION",
"defaultValue": -1,
"description": "Topic Replication Factor default value",
"displayName": "Topic Replication Factor default",
"isMandatory": false,
"name": "topic.creation.default.replication.factor",
"type": "INT"
},
{
"category": "TOPIC_CREATION",
"defaultValue": -1,
"description": "Topic Partitions default value",
"displayName": "Topic Partitions default",
"isMandatory": false,
"name": "topic.creation.default.partitions",
"type": "INT"
},
{
"category": "TOPIC_CREATION",
"defaultValue": "delete",
"allowedValues": ["compact", "delete"],
"description": "Topic Cleanup Policy default value",
"displayName": "Topic Cleanup Policy default",
"isMandatory": false,
"name": "topic.creation.default.cleanup.policy",
"type": "STRING"
},
{
"category": "TOPIC_CREATION",
"defaultValue": "producer",
"allowedValues": ["uncompressed", "zstd", "lz4", "snappy", "gzip", "producer"],
"description": "Topic Compression Type default value",
"displayName": "Topic Compression Type default",
"isMandatory": false,
"name": "topic.creation.default.compression.type",
"type": "STRING"
}
],
"groups": {
"properties": [
{
"category": "TOPIC_CREATION",
"description": "Topic Creation Groups",
"displayName": "Topic Creation Groups",
"isMandatory": false,
"name": "topic.creation.groups",
"type": "STRING"
},
{
"category": "TOPIC_CREATION",
"description": "Topic Group includes",
"displayName": "Topic includes",
"isMandatory": false,
"name": "topic.creation.(.+).include",
"type": "STRING"
},
{
"category": "TOPIC_CREATION",
"description": "Topic Group excludes",
"displayName": "Topic excludes",
"isMandatory": false,
"name": "topic.creation.(.+).exclude",
"type": "STRING"
}
],
"options": [
{
"category": "TOPIC_CREATION",
"defaultValue": -1,
"description": "Topic Replication Factor value",
"displayName": "Topic Replication Factor",
"isMandatory": false,
"name": "topic.creation.(.+).replication.factor",
"type": "INT"
},
{
"category": "TOPIC_CREATION",
"defaultValue": -1,
"description": "Topic Partitions value",
"displayName": "Topic Partitions",
"isMandatory": false,
"name": "topic.creation.(.+).partitions",
"type": "INT"
},
{
"category": "TOPIC_CREATION",
"defaultValue": "delete",
"allowedValues": ["compact", "delete"],
"description": "Topic Cleanup Policy value",
"displayName": "Topic Cleanup Policy",
"isMandatory": false,
"name": "topic.creation.(.+).cleanup.policy",
"type": "STRING"
},
{
"category": "TOPIC_CREATION",
"defaultValue": "producer",
"allowedValues": ["uncompressed", "zstd", "lz4", "snappy", "gzip", "producer"],
"description": "Topic Compression Type value",
"displayName": "Topic Compression Type",
"isMandatory": false,
"name": "topic.creation.(.+).compression.type",
"type": "STRING"
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export const NameInputField = (props: INameInputFieldProps) => {
isRequired={isRequired}
defaultValue={value}
onChange={saveInputText}
aria-label="transform name input"
aria-label="name input"
validated={isInvalid ? "error" : 'default'}
name={name}
placeholder={placeholder}
/>
</FormGroup>
);
};
};
2 changes: 2 additions & 0 deletions ui/packages/ui/src/app/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export * from './ToastAlertComponent';
export * from './ConnectorTypeComponent';
export * from './ApplicationErrorPage';
export * from './BasicSelectInput';
export * from './NameInputField';
export * from './transformHelpers';
export * from './topicCreationHelpers';
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Form, Grid, GridItem } from '@patternfly/react-core';
import React from 'react';
import { FormComponent } from 'components';
import { Formik, useFormikContext } from 'formik';
import * as Yup from 'yup';
import _ from 'lodash';
import { formatPropertyDefinitions } from 'shared';

export interface ITopicDefaultsProps {
topicDefaultProperties: any[];
topicDefaultValues?: any;
updateTopicDefaults: (value: any) => void;
setIsTopicCreationDirty: (data: boolean) => void;
}

const FormSubmit: React.FunctionComponent<any> = React.forwardRef((props, ref) => {
const { dirty, submitForm, validateForm } = useFormikContext();

React.useImperativeHandle(ref, () => ({
validate() {
validateForm();
submitForm();
}
}));
React.useEffect(() => {
if (dirty) {
props.setIsTopicCreationDirty(true);
}
}, [dirty]);
return null;
});

export const TopicDefaults: React.FunctionComponent<any> = React.forwardRef((props, ref) => {
const getInitialValues = (properties: any) => {
const combinedValues: any = {};

for (const prop of properties) {
if (!combinedValues[prop.name]) {
if (_.isEmpty(props.topicDefaultValues) || props.topicDefaultValues[prop.name] === undefined) {
prop.defaultValue === undefined
? (combinedValues[prop.name] = prop.type === 'INT' || prop.type === 'LONG' ? 0 : '')
: (combinedValues[prop.name] = prop.defaultValue);
} else {
combinedValues[prop.name] = props.topicDefaultValues[prop.name];
}
}
}
return combinedValues;
};

const propList = formatPropertyDefinitions(props.topicDefaultProperties);

const initialValues = getInitialValues(propList);

const basicValidationSchema = {};

const topicDefaultList = [...propList];

topicDefaultList.map((key: any) => {
if (key.type === 'STRING') {
basicValidationSchema[key.name] = Yup.string();
} else if (key.type === 'PASSWORD') {
basicValidationSchema[key.name] = Yup.string();
} else if (key.type === 'INT') {
basicValidationSchema[key.name] = Yup.string();
}
if (key.isMandatory) {
basicValidationSchema[key.name] = basicValidationSchema[key.name].required(`${key.name} required`);
}
});
const validationSchema = Yup.object().shape({ ...basicValidationSchema });

const handleSubmit = (value: any) => {
const newValues = {};
for (const prop of props.topicDefaultProperties) {
const newValue = (prop.type === 'INT' || prop.type === 'LONG' ||
prop.type === 'NON-NEG-INT' || prop.type === 'NON-NEG-LONG' ||
prop.type === 'POS-INT') ? Number(value[prop.name]) : value[prop.name];
if (!prop.defaultValue || newValue !== prop.defaultValue) {
newValues[prop.name] = newValue;
}
}
props.updateTopicDefaults(newValues);
props.setIsTopicCreationDirty(false);
};
return (
<>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={values => {
handleSubmit(values);
}}
enableReinitialize={true}
>
{({ errors, touched, setFieldValue }) => (
<Form className="pf-c-form">
<Grid hasGutter={true}>
{propList.map((prop, index) => {
return (
<GridItem key={index} lg={prop.gridWidthLg} sm={prop.gridWidthSm}>
<FormComponent
propertyDefinition={prop}
// tslint:disable-next-line: no-empty
propertyChange={() => {}}
setFieldValue={setFieldValue}
helperTextInvalid={errors[prop.name]}
invalidMsg={[]}
validated={errors[prop.name] && touched[prop.name] ? 'error' : 'default'}
// tslint:disable-next-line: no-empty
clearValidationError={() => {}}
/>
</GridItem>
);
})}
</Grid>
<FormSubmit ref={ref} setIsTopicCreationDirty={props.setIsTopicCreationDirty} />
</Form>
)}
</Formik>
</>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.topic-group-block {
padding: 5px 0px 0px 5px;
border: 1px solid var(--pf-global--BorderColor--100);
border-radius: 5px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
Button,
Form,
Grid,
GridItem,
Split,
SplitItem,
Title
} from '@patternfly/react-core';
import { TrashIcon } from '@patternfly/react-icons';
import React from 'react';
import { NameInputField, TopicGroupConfig } from 'components';
import _ from 'lodash';
import { getFormattedTopicCreationProperties } from 'shared';
import { useTranslation } from 'react-i18next';
import './TopicGroupCard.css';

export interface ITopicGroupCardProps {
topicGroupNo: number;
topicGroupName: string;
topicGroupNameList: string[];
topicGroupConfig: any;
deleteTopicGroup: (order: number) => void;
updateTopicGroup: (key: number, field: string, value: any) => void;
topicGroupsData: any;
topicGroupOptionsData: any;
setIsTopicCreationDirty: (data: boolean) => void;
}

export const TopicGroupCard: React.FunctionComponent<any> = React.forwardRef((props, ref) => {
const { t } = useTranslation();
const [nameIsValid, setNameIsValid] = React.useState<boolean>(true);

const deleteCard = () => {
props.deleteTopicGroup(props.topicGroupNo);
};

const updateName = (value: string, field?: string) => {
if (field) {
value === '' || props.topicGroupNameList.includes(value) ? setNameIsValid(false) : setNameIsValid(true);
props.updateTopicGroup(props.topicGroupNo, 'name', value);
}
};

return (
<Grid>
<GridItem span={12}>
<div className={'topic-group-block pf-u-mt-lg pf-u-p-sm pf-u-pb-lg'} id="topic-group-parent">
<Split>
<SplitItem isFilled={true}>
<Title headingLevel="h2">
Topic Group # {props.topicGroupNo} &nbsp;
</Title>
<Form>
<Grid hasGutter={true}>
<GridItem span={4}>
<NameInputField
label="Name"
description=""
fieldId="topic_group_name"
isRequired={true}
name="topic_group_name"
placeholder="Name"
inputType="text"
value={props.topicGroupName}
setFieldValue={updateName}
isInvalid={!nameIsValid}
invalidText={props.topicGroupName ? t('uniqueName') : t('nameRequired')}
/>
</GridItem>
</Grid>
</Form>
<TopicGroupConfig
ref={ref}
topicGroupNo={props.topicGroupNo}
topicGroupConfigProperties={getFormattedTopicCreationProperties(props.topicGroupsData)}
topicGroupConfigValues={props.topicGroupConfig}
topicGroupOptionProperties={getFormattedTopicCreationProperties(props.topicGroupOptionsData)}
updateTopicGroup={props.updateTopicGroup}
setIsTopicCreationDirty={props.setIsTopicCreationDirty}
/>
</SplitItem>
<SplitItem>
<Button variant="link" icon={<TrashIcon />} onClick={deleteCard} id='tooltip-selector' />
</SplitItem>
</Split>
</div>
</GridItem>
</Grid>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.topic-group-option-block {
margin-left: 20px;
padding: 5px 0px 0px 5px;
border: 1px dashed var(--pf-global--BorderColor--100);
border-radius: 5px;
}
Loading

0 comments on commit 99c0bed

Please sign in to comment.