KC: Make viewing/editing config a single view (#2613)

* Get rid of KC edit config page

* fix e2e-checks

Co-authored-by: VladSenyuta <vlad.senyuta@gmail.com>
This commit is contained in:
Oleg Shur 2022-09-26 12:05:01 +03:00 committed by GitHub
parent b940c28b5c
commit bae5c39cf2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 103 additions and 236 deletions

View file

@ -23,9 +23,9 @@ public class ConnectorsView {
return this; return this;
} }
@Step("Open 'Edit Config' of connector") @Step()
public ConnectorUpdateView openEditConfig() { public ConnectorUpdateView openConfigTab() {
BrowserUtils.javaExecutorClick($x("//button[text()='Edit Config']")); BrowserUtils.javaExecutorClick($(By.xpath("//a[text() ='Config']")));
return new ConnectorUpdateView(); return new ConnectorUpdateView();
} }

View file

@ -81,7 +81,7 @@ public class ConnectorsTests extends BaseTest {
.waitUntilScreenReady() .waitUntilScreenReady()
.openConnector(CONNECTOR_FOR_UPDATE.getName()); .openConnector(CONNECTOR_FOR_UPDATE.getName());
pages.connectorsView.connectorIsVisibleOnOverview(); pages.connectorsView.connectorIsVisibleOnOverview();
pages.connectorsView.openEditConfig() pages.connectorsView.openConfigTab()
.updConnectorConfig(CONNECTOR_FOR_UPDATE.getConfig()); .updConnectorConfig(CONNECTOR_FOR_UPDATE.getConfig());
pages.openConnectorsList(CLUSTER_NAME); pages.openConnectorsList(CLUSTER_NAME);
Assertions.assertTrue(pages.connectorsList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()),"isConnectorVisible()"); Assertions.assertTrue(pages.connectorsList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()),"isConnectorVisible()");

View file

@ -2,7 +2,6 @@ import React from 'react';
import { Navigate, Routes, Route } from 'react-router-dom'; import { Navigate, Routes, Route } from 'react-router-dom';
import { import {
RouteParams, RouteParams,
clusterConnectConnectorEditRelativePath,
clusterConnectConnectorRelativePath, clusterConnectConnectorRelativePath,
clusterConnectConnectorsRelativePath, clusterConnectConnectorsRelativePath,
clusterConnectorNewRelativePath, clusterConnectorNewRelativePath,
@ -13,7 +12,6 @@ import useAppParams from 'lib/hooks/useAppParams';
import ListPage from './List/ListPage'; import ListPage from './List/ListPage';
import New from './New/New'; import New from './New/New';
import Edit from './Edit/Edit';
import DetailsPage from './Details/DetailsPage'; import DetailsPage from './Details/DetailsPage';
const Connect: React.FC = () => { const Connect: React.FC = () => {
@ -23,10 +21,6 @@ const Connect: React.FC = () => {
<Routes> <Routes>
<Route index element={<ListPage />} /> <Route index element={<ListPage />} />
<Route path={clusterConnectorNewRelativePath} element={<New />} /> <Route path={clusterConnectorNewRelativePath} element={<New />} />
<Route
path={clusterConnectConnectorEditRelativePath}
element={<Edit />}
/>
<Route <Route
path={getNonExactPath(clusterConnectConnectorRelativePath)} path={getNonExactPath(clusterConnectConnectorRelativePath)}
element={<DetailsPage />} element={<DetailsPage />}

View file

@ -10,7 +10,6 @@ import {
useUpdateConnectorState, useUpdateConnectorState,
} from 'lib/hooks/api/kafkaConnect'; } from 'lib/hooks/api/kafkaConnect';
import { import {
clusterConnectConnectorEditPath,
clusterConnectorsPath, clusterConnectorsPath,
RouterParamsClusterConnectConnector, RouterParamsClusterConnectConnector,
} from 'lib/paths'; } from 'lib/paths';
@ -115,20 +114,6 @@ const Actions: React.FC = () => {
> >
Restart Failed Tasks Restart Failed Tasks
</Button> </Button>
<Button
buttonSize="M"
buttonType="primary"
type="button"
disabled={isMutating}
to={clusterConnectConnectorEditPath(
routerProps.clusterName,
routerProps.connectName,
routerProps.connectorName
)}
>
Edit Config
</Button>
<Button <Button
buttonSize="M" buttonSize="M"
buttonType="secondary" buttonType="secondary"

View file

@ -31,7 +31,6 @@ const expectActionButtonsExists = () => {
expect(screen.getByText('Restart Connector')).toBeInTheDocument(); expect(screen.getByText('Restart Connector')).toBeInTheDocument();
expect(screen.getByText('Restart All Tasks')).toBeInTheDocument(); expect(screen.getByText('Restart All Tasks')).toBeInTheDocument();
expect(screen.getByText('Restart Failed Tasks')).toBeInTheDocument(); expect(screen.getByText('Restart Failed Tasks')).toBeInTheDocument();
expect(screen.getByText('Edit Config')).toBeInTheDocument();
expect(screen.getByText('Delete')).toBeInTheDocument(); expect(screen.getByText('Delete')).toBeInTheDocument();
}; };
@ -63,7 +62,7 @@ describe('Actions', () => {
data: set({ ...connector }, 'status.state', ConnectorState.PAUSED), data: set({ ...connector }, 'status.state', ConnectorState.PAUSED),
})); }));
renderComponent(); renderComponent();
expect(screen.getAllByRole('button').length).toEqual(6); expect(screen.getAllByRole('button').length).toEqual(5);
expect(screen.getByText('Resume')).toBeInTheDocument(); expect(screen.getByText('Resume')).toBeInTheDocument();
expect(screen.queryByText('Pause')).not.toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument();
expectActionButtonsExists(); expectActionButtonsExists();
@ -74,7 +73,7 @@ describe('Actions', () => {
data: set({ ...connector }, 'status.state', ConnectorState.FAILED), data: set({ ...connector }, 'status.state', ConnectorState.FAILED),
})); }));
renderComponent(); renderComponent();
expect(screen.getAllByRole('button').length).toEqual(5); expect(screen.getAllByRole('button').length).toEqual(4);
expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Resume')).not.toBeInTheDocument();
expect(screen.queryByText('Pause')).not.toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument();
expectActionButtonsExists(); expectActionButtonsExists();
@ -85,7 +84,7 @@ describe('Actions', () => {
data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED), data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED),
})); }));
renderComponent(); renderComponent();
expect(screen.getAllByRole('button').length).toEqual(5); expect(screen.getAllByRole('button').length).toEqual(4);
expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Resume')).not.toBeInTheDocument();
expect(screen.queryByText('Pause')).not.toBeInTheDocument(); expect(screen.queryByText('Pause')).not.toBeInTheDocument();
expectActionButtonsExists(); expectActionButtonsExists();
@ -96,7 +95,7 @@ describe('Actions', () => {
data: set({ ...connector }, 'status.state', ConnectorState.RUNNING), data: set({ ...connector }, 'status.state', ConnectorState.RUNNING),
})); }));
renderComponent(); renderComponent();
expect(screen.getAllByRole('button').length).toEqual(6); expect(screen.getAllByRole('button').length).toEqual(5);
expect(screen.queryByText('Resume')).not.toBeInTheDocument(); expect(screen.queryByText('Resume')).not.toBeInTheDocument();
expect(screen.getByText('Pause')).toBeInTheDocument(); expect(screen.getByText('Pause')).toBeInTheDocument();
expectActionButtonsExists(); expectActionButtonsExists();

View file

@ -1,23 +1,95 @@
import React from 'react'; import React from 'react';
import useAppParams from 'lib/hooks/useAppParams'; import useAppParams from 'lib/hooks/useAppParams';
import Editor from 'components/common/Editor/Editor'; import { Controller, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import { yupResolver } from '@hookform/resolvers/yup';
import { RouterParamsClusterConnectConnector } from 'lib/paths'; import { RouterParamsClusterConnectConnector } from 'lib/paths';
import { useConnectorConfig } from 'lib/hooks/api/kafkaConnect'; import yup from 'lib/yupExtended';
import Editor from 'components/common/Editor/Editor';
import { Button } from 'components/common/Button/Button';
import {
useConnectorConfig,
useUpdateConnectorConfig,
} from 'lib/hooks/api/kafkaConnect';
import {
ConnectEditWarningMessageStyled,
ConnectEditWrapperStyled,
} from './Config.styled';
const validationSchema = yup.object().shape({
config: yup.string().required().isJsonObject(),
});
interface FormValues {
config: string;
}
const Config: React.FC = () => { const Config: React.FC = () => {
const routerProps = useAppParams<RouterParamsClusterConnectConnector>(); const routerParams = useAppParams<RouterParamsClusterConnectConnector>();
const { data: config } = useConnectorConfig(routerProps); const { data: config } = useConnectorConfig(routerParams);
const mutation = useUpdateConnectorConfig(routerParams);
if (!config) return null; const {
handleSubmit,
control,
reset,
formState: { isDirty, isSubmitting, isValid, errors },
setValue,
} = useForm<FormValues>({
mode: 'onTouched',
resolver: yupResolver(validationSchema),
defaultValues: {
config: JSON.stringify(config, null, '\t'),
},
});
React.useEffect(() => {
if (config) {
setValue('config', JSON.stringify(config, null, '\t'));
}
}, [config, setValue]);
const onSubmit = async (values: FormValues) => {
const requestBody = JSON.parse(values.config.trim());
await mutation.mutateAsync(requestBody);
reset(values);
};
const hasCredentials = JSON.stringify(config, null, '\t').includes(
'"******"'
);
return ( return (
<Editor <ConnectEditWrapperStyled>
readOnly {hasCredentials && (
value={JSON.stringify(config, null, '\t')} <ConnectEditWarningMessageStyled>
highlightActiveLine={false} Please replace ****** with the real credential values to avoid
isFixedHeight accidentally breaking your connector config!
style={{ margin: '16px' }} </ConnectEditWarningMessageStyled>
/> )}
<form onSubmit={handleSubmit(onSubmit)} aria-label="Edit connect form">
<div>
<Controller
control={control}
name="config"
render={({ field }) => (
<Editor {...field} readOnly={isSubmitting} />
)}
/>
</div>
<div>
<ErrorMessage errors={errors} name="config" />
</div>
<Button
buttonSize="M"
buttonType="primary"
type="submit"
disabled={!isValid || isSubmitting || !isDirty}
>
Submit
</Button>
</form>
</ConnectEditWrapperStyled>
); );
}; };

View file

@ -1,46 +0,0 @@
import React from 'react';
import { render, WithRoute } from 'lib/testHelpers';
import { clusterConnectConnectorConfigPath } from 'lib/paths';
import Config from 'components/Connect/Details/Config/Config';
import { screen } from '@testing-library/dom';
import { useConnectorConfig } from 'lib/hooks/api/kafkaConnect';
import { connector } from 'lib/fixtures/kafkaConnect';
jest.mock('components/common/Editor/Editor', () => () => (
<div>mock-Editor</div>
));
jest.mock('lib/hooks/api/kafkaConnect', () => ({
useConnectorConfig: jest.fn(),
}));
describe('Config', () => {
const renderComponent = () =>
render(
<WithRoute path={clusterConnectConnectorConfigPath()}>
<Config />
</WithRoute>,
{
initialEntries: [
clusterConnectConnectorConfigPath(
'my-cluster',
'my-connect',
'my-connector'
),
],
}
);
it('is empty when no config', () => {
(useConnectorConfig as jest.Mock).mockImplementation(() => ({}));
renderComponent();
expect(screen.queryByText('mock-Editor')).not.toBeInTheDocument();
});
it('renders editor', () => {
(useConnectorConfig as jest.Mock).mockImplementation(() => ({
data: connector.config,
}));
renderComponent();
expect(screen.getByText('mock-Editor')).toBeInTheDocument();
});
});

View file

@ -1,10 +1,7 @@
import React from 'react'; import React from 'react';
import { render, WithRoute } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { import { clusterConnectConnectorConfigPath } from 'lib/paths';
clusterConnectConnectorConfigPath, import Config from 'components/Connect/Details/Config/Config';
clusterConnectConnectorEditPath,
} from 'lib/paths';
import Edit from 'components/Connect/Edit/Edit';
import { connector } from 'lib/fixtures/kafkaConnect'; import { connector } from 'lib/fixtures/kafkaConnect';
import { waitFor } from '@testing-library/dom'; import { waitFor } from '@testing-library/dom';
import { act, fireEvent, screen } from '@testing-library/react'; import { act, fireEvent, screen } from '@testing-library/react';
@ -31,16 +28,16 @@ const [clusterName, connectName, connectorName] = [
'my-connector', 'my-connector',
]; ];
describe('Edit', () => { describe('Config', () => {
const pathname = clusterConnectConnectorEditPath(); const pathname = clusterConnectConnectorConfigPath();
const renderComponent = () => const renderComponent = () =>
render( render(
<WithRoute path={pathname}> <WithRoute path={pathname}>
<Edit /> <Config />
</WithRoute>, </WithRoute>,
{ {
initialEntries: [ initialEntries: [
clusterConnectConnectorEditPath( clusterConnectConnectorConfigPath(
clusterName, clusterName,
connectName, connectName,
connectorName connectorName
@ -66,11 +63,6 @@ describe('Edit', () => {
renderComponent(); renderComponent();
fireEvent.submit(screen.getByRole('form')); fireEvent.submit(screen.getByRole('form'));
await waitFor(() => expect(updateConfig).toHaveBeenCalledTimes(1)); await waitFor(() => expect(updateConfig).toHaveBeenCalledTimes(1));
await waitFor(() => expect(mockHistoryPush).toHaveBeenCalledTimes(1));
expect(mockHistoryPush).toHaveBeenCalledWith(
clusterConnectConnectorConfigPath(clusterName, connectName, connectorName)
);
}); });
it('does not redirect to connector config view on unsuccessful submit', async () => { it('does not redirect to connector config view on unsuccessful submit', async () => {

View file

@ -1,109 +0,0 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import useAppParams from 'lib/hooks/useAppParams';
import { Controller, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import { yupResolver } from '@hookform/resolvers/yup';
import {
clusterConnectConnectorConfigPath,
RouterParamsClusterConnectConnector,
} from 'lib/paths';
import yup from 'lib/yupExtended';
import Editor from 'components/common/Editor/Editor';
import { Button } from 'components/common/Button/Button';
import {
useConnectorConfig,
useUpdateConnectorConfig,
} from 'lib/hooks/api/kafkaConnect';
import {
ConnectEditWarningMessageStyled,
ConnectEditWrapperStyled,
} from './Edit.styled';
const validationSchema = yup.object().shape({
config: yup.string().required().isJsonObject(),
});
interface FormValues {
config: string;
}
const Edit: React.FC = () => {
const routerParams = useAppParams<RouterParamsClusterConnectConnector>();
const navigate = useNavigate();
const { data: config } = useConnectorConfig(routerParams);
const mutation = useUpdateConnectorConfig(routerParams);
const {
handleSubmit,
control,
formState: { isDirty, isSubmitting, isValid, errors },
setValue,
} = useForm<FormValues>({
mode: 'onTouched',
resolver: yupResolver(validationSchema),
defaultValues: {
config: JSON.stringify(config, null, '\t'),
},
});
React.useEffect(() => {
if (config) {
setValue('config', JSON.stringify(config, null, '\t'));
}
}, [config, setValue]);
const onSubmit = async (values: FormValues) => {
const requestBody = JSON.parse(values.config.trim());
const connector = await mutation.mutateAsync(requestBody);
if (connector) {
navigate(
clusterConnectConnectorConfigPath(
routerParams.clusterName,
routerParams.connectName,
routerParams.connectorName
)
);
}
};
const hasCredentials = JSON.stringify(config, null, '\t').includes(
'"******"'
);
return (
<ConnectEditWrapperStyled>
{hasCredentials && (
<ConnectEditWarningMessageStyled>
Please replace ****** with the real credential values to avoid
accidentally breaking your connector config!
</ConnectEditWarningMessageStyled>
)}
<form onSubmit={handleSubmit(onSubmit)} aria-label="Edit connect form">
<div>
<Controller
control={control}
name="config"
render={({ field }) => (
<Editor {...field} readOnly={isSubmitting} />
)}
/>
</div>
<div>
<ErrorMessage errors={errors} name="config" />
</div>
<Button
buttonSize="M"
buttonType="primary"
type="submit"
disabled={!isValid || isSubmitting || !isDirty}
>
Submit
</Button>
</form>
</ConnectEditWrapperStyled>
);
};
export default Edit;

View file

@ -2,12 +2,10 @@ import React from 'react';
import { render, WithRoute } from 'lib/testHelpers'; import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import Connect from 'components/Connect/Connect'; import Connect from 'components/Connect/Connect';
import { store } from 'redux/store';
import { import {
clusterConnectorsPath, clusterConnectorsPath,
clusterConnectorNewPath, clusterConnectorNewPath,
clusterConnectConnectorPath, clusterConnectConnectorPath,
clusterConnectConnectorEditPath,
getNonExactPath, getNonExactPath,
clusterConnectsPath, clusterConnectsPath,
} from 'lib/paths'; } from 'lib/paths';
@ -16,7 +14,6 @@ const ConnectCompText = {
new: 'New Page', new: 'New Page',
list: 'List Page', list: 'List Page',
details: 'Details Page', details: 'Details Page',
edit: 'Edit Page',
}; };
jest.mock('components/Connect/New/New', () => () => ( jest.mock('components/Connect/New/New', () => () => (
@ -28,9 +25,6 @@ jest.mock('components/Connect/List/ListPage', () => () => (
jest.mock('components/Connect/Details/DetailsPage', () => () => ( jest.mock('components/Connect/Details/DetailsPage', () => () => (
<div>{ConnectCompText.details}</div> <div>{ConnectCompText.details}</div>
)); ));
jest.mock('components/Connect/Edit/Edit', () => () => (
<div>{ConnectCompText.edit}</div>
));
describe('Connect', () => { describe('Connect', () => {
const renderComponent = (pathname: string, routePath: string) => const renderComponent = (pathname: string, routePath: string) =>
@ -38,7 +32,7 @@ describe('Connect', () => {
<WithRoute path={getNonExactPath(routePath)}> <WithRoute path={getNonExactPath(routePath)}>
<Connect /> <Connect />
</WithRoute>, </WithRoute>,
{ initialEntries: [pathname], store } { initialEntries: [pathname] }
); );
it('renders ListPage', () => { it('renders ListPage', () => {
@ -64,16 +58,4 @@ describe('Connect', () => {
); );
expect(screen.getByText(ConnectCompText.details)).toBeInTheDocument(); expect(screen.getByText(ConnectCompText.details)).toBeInTheDocument();
}); });
it('renders EditContainer', () => {
renderComponent(
clusterConnectConnectorEditPath(
'my-cluster',
'my-connect',
'my-connector'
),
clusterConnectsPath()
);
expect(screen.getByText(ConnectCompText.edit)).toBeInTheDocument();
});
}); });

View file

@ -8,6 +8,7 @@ import { kafkaConnectApiClient as api } from 'lib/api';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ClusterName } from 'redux/interfaces'; import { ClusterName } from 'redux/interfaces';
import { showSuccessAlert } from 'lib/errorHandling';
interface UseConnectorProps { interface UseConnectorProps {
clusterName: ClusterName; clusterName: ClusterName;
@ -43,10 +44,6 @@ const connectorTasksKey = (props: UseConnectorProps) => [
...connectorKey(props), ...connectorKey(props),
'tasks', 'tasks',
]; ];
const connectorConfigKey = (props: UseConnectorProps) => [
...connectorKey(props),
'config',
];
export function useConnects(clusterName: ClusterName) { export function useConnects(clusterName: ClusterName) {
return useQuery(connectsKey(clusterName), () => return useQuery(connectsKey(clusterName), () =>
@ -104,8 +101,10 @@ export function useUpdateConnectorConfig(props: UseConnectorProps) {
api.setConnectorConfig({ ...props, requestBody }), api.setConnectorConfig({ ...props, requestBody }),
{ {
onSuccess: () => { onSuccess: () => {
showSuccessAlert({
message: `Config successfully updated.`,
});
client.invalidateQueries(connectorKey(props)); client.invalidateQueries(connectorKey(props));
client.invalidateQueries(connectorConfigKey(props));
}, },
} }
); );

View file

@ -201,7 +201,6 @@ export const clusterConnectorsRelativePath = 'connectors';
export const clusterConnectorNewRelativePath = 'create-new'; export const clusterConnectorNewRelativePath = 'create-new';
export const clusterConnectConnectorsRelativePath = `${RouteParams.connectName}/connectors`; export const clusterConnectConnectorsRelativePath = `${RouteParams.connectName}/connectors`;
export const clusterConnectConnectorRelativePath = `${clusterConnectConnectorsRelativePath}/${RouteParams.connectorName}`; export const clusterConnectConnectorRelativePath = `${clusterConnectConnectorsRelativePath}/${RouteParams.connectorName}`;
export const clusterConnectConnectorEditRelativePath = `${clusterConnectConnectorRelativePath}/edit`;
export const clusterConnectConnectorTasksRelativePath = 'tasks'; export const clusterConnectConnectorTasksRelativePath = 'tasks';
export const clusterConnectConnectorConfigRelativePath = 'config'; export const clusterConnectConnectorConfigRelativePath = 'config';