Added key-value form for stream parameters (#2191)
* Added key-value form for stream parameters * Removed unused variable * fixing some test cases and fixing width of stream props * adding key value validation and tests * fixing placeholder padding and font size * remove unnecessary code Co-authored-by: rAzizbekyan <razizbekyan@provectus.com> Co-authored-by: Robert Azizbekyan <103438454+rAzizbekyan@users.noreply.github.com>
This commit is contained in:
parent
cbd4e4a52a
commit
0b76b12518
5 changed files with 172 additions and 236 deletions
|
@ -198,15 +198,23 @@ const Query: FC = () => {
|
|||
|
||||
const submitHandler = useCallback(
|
||||
(values: FormValues) => {
|
||||
const streamsProperties = values.streamsProperties.reduce(
|
||||
(acc, current) => ({
|
||||
...acc,
|
||||
[current.key as keyof string]: current.value,
|
||||
}),
|
||||
{} as { [key: string]: string }
|
||||
);
|
||||
setFetching(true);
|
||||
dispatch(
|
||||
executeKsql({
|
||||
clusterName,
|
||||
ksqlCommandV2: {
|
||||
...values,
|
||||
streamsProperties: values.streamsProperties
|
||||
? JSON.parse(values.streamsProperties)
|
||||
: undefined,
|
||||
streamsProperties:
|
||||
values.streamsProperties[0].key !== ''
|
||||
? JSON.parse(JSON.stringify(streamsProperties))
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -27,8 +27,47 @@ export const KSQLButtons = styled.div`
|
|||
gap: 16px;
|
||||
`;
|
||||
|
||||
export const StreamPropertiesContainer = styled.label`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
export const InputsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
export const StreamPropertiesInputWrapper = styled.div`
|
||||
& > input {
|
||||
height: 40px;
|
||||
border: 1px solid grey;
|
||||
border-radius: 4px;
|
||||
min-width: 300px;
|
||||
font-size: 16px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DeleteButtonWrapper = styled.div`
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-self: flex-start;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
export const LabelContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const Fieldset = styled.fieldset`
|
||||
width: 100%;
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
export const Editor = styled(BaseEditor)(
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import { FormError } from 'components/common/Input/Input.styled';
|
||||
import { ErrorMessage } from '@hookform/error-message';
|
||||
import { useForm, Controller, useFieldArray } from 'react-hook-form';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
|
||||
import CloseIcon from 'components/common/Icons/CloseIcon';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import yup from 'lib/yupExtended';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import { SchemaType } from 'generated-sources';
|
||||
|
||||
import * as S from './QueryForm.styled';
|
||||
|
||||
|
@ -17,16 +18,22 @@ export interface Props {
|
|||
submitHandler: (values: FormValues) => void;
|
||||
}
|
||||
|
||||
export type StreamsPropertiesType = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
export type FormValues = {
|
||||
ksql: string;
|
||||
streamsProperties: string;
|
||||
streamsProperties: StreamsPropertiesType[];
|
||||
};
|
||||
|
||||
const streamsPropertiesSchema = yup.object().shape({
|
||||
key: yup.string().trim(),
|
||||
value: yup.string().trim(),
|
||||
});
|
||||
const validationSchema = yup.object({
|
||||
ksql: yup.string().trim().required(),
|
||||
streamsProperties: yup.lazy((value) =>
|
||||
value === '' ? yup.string().trim() : yup.string().trim().isJsonObject()
|
||||
),
|
||||
streamsProperties: yup.array().of(streamsPropertiesSchema),
|
||||
});
|
||||
|
||||
const QueryForm: React.FC<Props> = ({
|
||||
|
@ -46,9 +53,16 @@ const QueryForm: React.FC<Props> = ({
|
|||
resolver: yupResolver(validationSchema),
|
||||
defaultValues: {
|
||||
ksql: '',
|
||||
streamsProperties: '',
|
||||
streamsProperties: [{ key: '', value: '' }],
|
||||
},
|
||||
});
|
||||
const { fields, append, remove } = useFieldArray<
|
||||
FormValues,
|
||||
'streamsProperties'
|
||||
>({
|
||||
control,
|
||||
name: 'streamsProperties',
|
||||
});
|
||||
|
||||
return (
|
||||
<S.QueryWrapper>
|
||||
|
@ -93,48 +107,69 @@ const QueryForm: React.FC<Props> = ({
|
|||
<ErrorMessage errors={errors} name="ksql" />
|
||||
</FormError>
|
||||
</S.Fieldset>
|
||||
<S.Fieldset aria-labelledby="streamsPropertiesLabel">
|
||||
<S.KSQLInputHeader>
|
||||
<label id="streamsPropertiesLabel">
|
||||
Stream properties (JSON format)
|
||||
</label>
|
||||
<Button
|
||||
onClick={() => setValue('streamsProperties', '')}
|
||||
buttonType="primary"
|
||||
buttonSize="S"
|
||||
isInverted
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</S.KSQLInputHeader>
|
||||
<Controller
|
||||
control={control}
|
||||
name="streamsProperties"
|
||||
render={({ field }) => (
|
||||
<S.Editor
|
||||
{...field}
|
||||
commands={[
|
||||
{
|
||||
// commands is array of key bindings.
|
||||
// name for the key binding.
|
||||
name: 'commandName',
|
||||
// key combination used for the command.
|
||||
bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
|
||||
// function to execute when keys are pressed.
|
||||
exec: () => {
|
||||
handleSubmit(submitHandler)();
|
||||
},
|
||||
},
|
||||
]}
|
||||
schemaType={SchemaType.JSON}
|
||||
readOnly={fetching}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormError>
|
||||
<ErrorMessage errors={errors} name="streamsProperties" />
|
||||
</FormError>
|
||||
</S.Fieldset>
|
||||
|
||||
<S.StreamPropertiesContainer>
|
||||
Stream properties:
|
||||
{fields.map((item, index) => (
|
||||
<S.InputsContainer key={item.id}>
|
||||
<S.StreamPropertiesInputWrapper>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`streamsProperties.${index}.key`}
|
||||
render={({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
placeholder="Key"
|
||||
aria-label="key"
|
||||
type="text"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormError>
|
||||
<ErrorMessage
|
||||
errors={errors}
|
||||
name={`streamsProperties.${index}.key`}
|
||||
/>
|
||||
</FormError>
|
||||
</S.StreamPropertiesInputWrapper>
|
||||
<S.StreamPropertiesInputWrapper>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`streamsProperties.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<input
|
||||
{...field}
|
||||
placeholder="Value"
|
||||
aria-label="value"
|
||||
type="text"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormError>
|
||||
<ErrorMessage
|
||||
errors={errors}
|
||||
name={`streamsProperties.${index}.value`}
|
||||
/>
|
||||
</FormError>
|
||||
</S.StreamPropertiesInputWrapper>
|
||||
|
||||
<S.DeleteButtonWrapper onClick={() => remove(index)}>
|
||||
<IconButtonWrapper aria-label="deleteProperty">
|
||||
<CloseIcon aria-hidden />
|
||||
</IconButtonWrapper>
|
||||
</S.DeleteButtonWrapper>
|
||||
</S.InputsContainer>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
onClick={() => append({ key: '', value: '' })}
|
||||
>
|
||||
<i className="fas fa-plus" />
|
||||
Add Stream Property
|
||||
</Button>
|
||||
</S.StreamPropertiesContainer>
|
||||
</S.KSQLInputsWrapper>
|
||||
<S.KSQLButtons>
|
||||
<Button
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { render } from 'lib/testHelpers';
|
||||
import React from 'react';
|
||||
import QueryForm, { Props } from 'components/KsqlDb/Query/QueryForm/QueryForm';
|
||||
import { screen, within } from '@testing-library/dom';
|
||||
import { screen, waitFor, within } from '@testing-library/dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
|
@ -26,20 +26,11 @@ describe('QueryForm', () => {
|
|||
// Represents SQL editor
|
||||
expect(within(KSQLBlock).getByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
const streamPropertiesBlock = screen.getByLabelText(
|
||||
'Stream properties (JSON format)'
|
||||
);
|
||||
const streamPropertiesBlock = screen.getByRole('textbox', { name: 'key' });
|
||||
expect(streamPropertiesBlock).toBeInTheDocument();
|
||||
expect(
|
||||
within(streamPropertiesBlock).getByText('Stream properties (JSON format)')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(streamPropertiesBlock).getByRole('button', { name: 'Clear' })
|
||||
).toBeInTheDocument();
|
||||
// Represents JSON editor
|
||||
expect(
|
||||
within(streamPropertiesBlock).getByRole('textbox')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Stream properties:')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Clear' })).toBeInTheDocument();
|
||||
expect(screen.queryAllByRole('textbox')[0]).toBeInTheDocument();
|
||||
|
||||
// Form controls
|
||||
expect(screen.getByRole('button', { name: 'Execute' })).toBeInTheDocument();
|
||||
|
@ -69,58 +60,10 @@ describe('QueryForm', () => {
|
|||
await act(() =>
|
||||
userEvent.click(screen.getByRole('button', { name: 'Execute' }))
|
||||
);
|
||||
expect(screen.getByText('ksql is a required field')).toBeInTheDocument();
|
||||
expect(submitFn).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('renders error with non-JSON streamProperties', async () => {
|
||||
renderComponent({
|
||||
fetching: false,
|
||||
hasResults: false,
|
||||
handleClearResults: jest.fn(),
|
||||
handleSSECancel: jest.fn(),
|
||||
submitHandler: jest.fn(),
|
||||
waitFor(() => {
|
||||
expect(screen.getByText('ksql is a required field')).toBeInTheDocument();
|
||||
expect(submitFn).not.toBeCalled();
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
// the use of `paste` is a hack that i found somewhere,
|
||||
// `type` won't work
|
||||
userEvent.paste(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'not-a-JSON-string'
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Execute' }));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText('streamsProperties is not JSON object')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without error with correct JSON', async () => {
|
||||
renderComponent({
|
||||
fetching: false,
|
||||
hasResults: false,
|
||||
handleClearResults: jest.fn(),
|
||||
handleSSECancel: jest.fn(),
|
||||
submitHandler: jest.fn(),
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'{"totallyJSON": "string"}'
|
||||
);
|
||||
userEvent.click(screen.getByRole('button', { name: 'Execute' }));
|
||||
});
|
||||
expect(
|
||||
screen.queryByText('streamsProperties is not JSON object')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('submits with correct inputs', async () => {
|
||||
|
@ -134,18 +77,9 @@ describe('QueryForm', () => {
|
|||
});
|
||||
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(screen.getByLabelText('KSQL')).getByRole('textbox'),
|
||||
'show tables;'
|
||||
);
|
||||
|
||||
userEvent.paste(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'{"totallyJSON": "string"}'
|
||||
);
|
||||
|
||||
userEvent.paste(screen.getAllByRole('textbox')[0], 'show tables;');
|
||||
userEvent.paste(screen.getByRole('textbox', { name: 'key' }), 'test');
|
||||
userEvent.paste(screen.getByRole('textbox', { name: 'value' }), 'test');
|
||||
userEvent.click(screen.getByRole('button', { name: 'Execute' }));
|
||||
});
|
||||
|
||||
|
@ -223,41 +157,7 @@ describe('QueryForm', () => {
|
|||
expect(submitFn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('submits form with ctrl+enter on streamProperties editor', async () => {
|
||||
const submitFn = jest.fn();
|
||||
renderComponent({
|
||||
fetching: false,
|
||||
hasResults: false,
|
||||
handleClearResults: jest.fn(),
|
||||
handleSSECancel: jest.fn(),
|
||||
submitHandler: submitFn,
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(screen.getByLabelText('KSQL')).getByRole('textbox'),
|
||||
'show tables;'
|
||||
);
|
||||
|
||||
userEvent.paste(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'{"some":"json"}'
|
||||
);
|
||||
|
||||
userEvent.type(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'{ctrl}{enter}'
|
||||
);
|
||||
});
|
||||
|
||||
expect(submitFn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('clears KSQL with Clear button', async () => {
|
||||
it('add new property', async () => {
|
||||
renderComponent({
|
||||
fetching: false,
|
||||
hasResults: false,
|
||||
|
@ -267,22 +167,15 @@ describe('QueryForm', () => {
|
|||
});
|
||||
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(screen.getByLabelText('KSQL')).getByRole('textbox'),
|
||||
'show tables;'
|
||||
);
|
||||
userEvent.click(
|
||||
within(screen.getByLabelText('KSQL')).getByRole('button', {
|
||||
name: 'Clear',
|
||||
})
|
||||
screen.getByRole('button', { name: 'Add Stream Property' })
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.queryByText('show tables;')).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole('textbox', { name: 'key' }).length).toEqual(2);
|
||||
});
|
||||
|
||||
it('clears streamProperties with Clear button', async () => {
|
||||
renderComponent({
|
||||
it('delete stream property', async () => {
|
||||
await renderComponent({
|
||||
fetching: false,
|
||||
hasResults: false,
|
||||
handleClearResults: jest.fn(),
|
||||
|
@ -291,20 +184,13 @@ describe('QueryForm', () => {
|
|||
});
|
||||
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'{"some":"json"}'
|
||||
);
|
||||
userEvent.click(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('button', {
|
||||
name: 'Clear',
|
||||
})
|
||||
screen.getByRole('button', { name: 'Add Stream Property' })
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('{"some":"json"}')).not.toBeInTheDocument();
|
||||
await act(() => {
|
||||
userEvent.click(screen.getAllByLabelText('deleteProperty')[0]);
|
||||
});
|
||||
expect(screen.getAllByRole('textbox', { name: 'key' }).length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,11 +3,11 @@ import React from 'react';
|
|||
import Query, {
|
||||
getFormattedErrorFromTableData,
|
||||
} from 'components/KsqlDb/Query/Query';
|
||||
import { screen, within } from '@testing-library/dom';
|
||||
import { screen } from '@testing-library/dom';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { clusterKsqlDbQueryPath } from 'lib/paths';
|
||||
import { act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const clusterName = 'testLocal';
|
||||
const renderComponent = () =>
|
||||
|
@ -25,9 +25,7 @@ describe('Query', () => {
|
|||
renderComponent();
|
||||
|
||||
expect(screen.getByLabelText('KSQL')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Stream properties:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
@ -41,12 +39,10 @@ describe('Query', () => {
|
|||
Object.defineProperty(window, 'EventSource', {
|
||||
value: EventSourceMock,
|
||||
});
|
||||
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
const textAreaElement = inputs[0] as HTMLTextAreaElement;
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(screen.getByLabelText('KSQL')).getByRole('textbox'),
|
||||
'show tables;'
|
||||
);
|
||||
userEvent.paste(textAreaElement, 'show tables;');
|
||||
userEvent.click(screen.getByRole('button', { name: 'Execute' }));
|
||||
});
|
||||
|
||||
|
@ -63,47 +59,19 @@ describe('Query', () => {
|
|||
Object.defineProperty(window, 'EventSource', {
|
||||
value: EventSourceMock,
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(screen.getByLabelText('KSQL')).getByRole('textbox'),
|
||||
'show tables;'
|
||||
);
|
||||
userEvent.paste(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'{"some":"json"}'
|
||||
);
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
const textAreaElement = inputs[0] as HTMLTextAreaElement;
|
||||
userEvent.paste(textAreaElement, 'show tables;');
|
||||
});
|
||||
await act(() => {
|
||||
userEvent.paste(screen.getByLabelText('key'), 'key');
|
||||
userEvent.paste(screen.getByLabelText('value'), 'value');
|
||||
});
|
||||
await act(() => {
|
||||
userEvent.click(screen.getByRole('button', { name: 'Execute' }));
|
||||
});
|
||||
expect(mock.calls().length).toBe(1);
|
||||
});
|
||||
|
||||
it('fetch on execute with streamParams', async () => {
|
||||
renderComponent();
|
||||
|
||||
const mock = fetchMock.postOnce(`/api/clusters/${clusterName}/ksql/v2`, {
|
||||
pipeId: 'testPipeID',
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'EventSource', {
|
||||
value: EventSourceMock,
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
userEvent.paste(
|
||||
within(screen.getByLabelText('KSQL')).getByRole('textbox'),
|
||||
'show tables;'
|
||||
);
|
||||
userEvent.paste(
|
||||
within(
|
||||
screen.getByLabelText('Stream properties (JSON format)')
|
||||
).getByRole('textbox'),
|
||||
'{"some":"json"}'
|
||||
);
|
||||
userEvent.click(screen.getByRole('button', { name: 'Execute' }));
|
||||
});
|
||||
expect(mock.calls().length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue