diff --git a/package-lock.json b/package-lock.json index 5678a24a4c..f708a8e517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8693,12 +8693,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -11507,12 +11501,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -16767,46 +16755,6 @@ "redux": "^3.6.0 || ^4.0.0 || ^5.0.0" } }, - "node_modules/redux-form": { - "version": "8.3.10", - "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-8.3.10.tgz", - "integrity": "sha512-Eeog8dJYUxCSZI/oBoy7VkprvMjj1lpUnHa3LwjVNZvYDNeiRZMoZoaAT+6nlK2YQ4aiBopKUMiLe4ihUOHCGg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2", - "es6-error": "^4.1.1", - "hoist-non-react-statics": "^3.3.2", - "invariant": "^2.2.4", - "is-promise": "^2.1.0", - "lodash": "^4.17.15", - "prop-types": "^15.6.1", - "react-is": "^16.4.2" - }, - "engines": { - "node": ">=8.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/redux-form" - }, - "peerDependencies": { - "immutable": "^3.8.2 || ^4.0.0", - "react": "^16.4.2 || ^17.0.0 || ^18.0.0", - "react-redux": "^6.0.1 || ^7.0.0 || ^8.0.0", - "redux": "^3.7.2 || ^4.0.0" - }, - "peerDependenciesMeta": { - "immutable": { - "optional": true - } - } - }, - "node_modules/redux-form/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/redux-promise-middleware": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/redux-promise-middleware/-/redux-promise-middleware-6.2.0.tgz", @@ -20139,7 +20087,6 @@ "redux": "^4.2.1", "redux-actions": "2.6.5", "redux-first-history": "^5.2.0", - "redux-form": "^8.3.10", "redux-promise-middleware": "^6.1.3", "store": "^2.0.12", "ts-key-enum": "^2.0.0", diff --git a/packages/jaeger-ui/package.json b/packages/jaeger-ui/package.json index 5dc9c19d8c..be802c9221 100644 --- a/packages/jaeger-ui/package.json +++ b/packages/jaeger-ui/package.json @@ -84,7 +84,6 @@ "redux": "^4.2.1", "redux-actions": "2.6.5", "redux-first-history": "^5.2.0", - "redux-form": "^8.3.10", "redux-promise-middleware": "^6.1.3", "store": "^2.0.12", "ts-key-enum": "^2.0.0", diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.jsx b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.jsx index fc5353635b..1c77022ced 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.jsx +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.jsx @@ -25,26 +25,26 @@ import queryString from 'query-string'; import { IoHelp } from 'react-icons/io5'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; import store from 'store'; import * as markers from './SearchForm.markers'; import { trackFormInput } from './SearchForm.track'; import * as jaegerApiActions from '../../actions/jaeger-api'; import { formatDate, formatTime } from '../../utils/date'; -import reduxFormFieldAdapter from '../../utils/redux-form-field-adapter'; -import { DEFAULT_OPERATION, DEFAULT_LIMIT, DEFAULT_LOOKBACK } from '../../constants/search-form'; +import { + DEFAULT_OPERATION, + DEFAULT_LIMIT, + DEFAULT_LOOKBACK, + SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE, +} from '../../constants/search-form'; import { getConfigValue } from '../../utils/config/get-config'; import SearchableSelect from '../common/SearchableSelect'; import './SearchForm.css'; +import ValidatedFormField from '../../utils/ValidatedFormField'; const FormItem = Form.Item; const Option = Select.Option; -const AdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input }); -const AdaptedSelect = reduxFormFieldAdapter({ AntInputComponent: SearchableSelect }); -const ValidatedAdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input, isValidatedInput: true }); - export function getUnixTimeStampInMSFromForm({ startDate, startDateTime, endDate, endDateTime }) { const start = `${startDate} ${startDateTime}`; const end = `${endDate} ${endDateTime}`; @@ -259,23 +259,59 @@ export function submitForm(fields, searchTraces) { } export class SearchFormImpl extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + formData: { + service: this.props.initialValues?.service, + operation: this.props.initialValues?.operation, + tags: this.props.initialValues?.tags, + lookback: this.props.initialValues?.lookback, + startDate: this.props.initialValues?.startDate, + startDateTime: this.props.initialValues?.startDateTime, + endDate: this.props.initialValues?.endDate, + endDateTime: this.props.initialValues?.endDateTime, + minDuration: this.props.initialValues?.minDuration, + maxDuration: this.props.initialValues?.maxDuration, + resultsLimit: this.props.initialValues?.resultsLimit, + }, + }; + } + + handleChange = fieldData => { + this.setState(prevState => ({ + formData: { + ...prevState.formData, + ...fieldData, + }, + })); + if (fieldData.service) { + this.props.changeServiceHandler(fieldData.service); + this.setState(prevState => ({ + formData: { + ...prevState.formData, + operation: DEFAULT_OPERATION, + }, + })); + } + }; + + handleSubmit = e => { + e.preventDefault(); + this.props.submitFormHandler(this.state.formData); + }; + render() { - const { - handleSubmit, - invalid, - searchMaxLookback, - selectedLookback, - selectedService = '-', - services, - submitting: disabled, - } = this.props; + const { invalid, searchMaxLookback, services, submitting: disabled } = this.props; + const { formData } = this.state; + const { service: selectedService, lookback: selectedLookback } = formData; const selectedServicePayload = services.find(s => s.name === selectedService); const opsForSvc = (selectedServicePayload && selectedServicePayload.operations) || []; const noSelectedService = selectedService === '-' || !selectedService; const tz = selectedLookback === 'custom' ? new Date().toTimeString().replace(/^.*?GMT/, 'UTC') : null; return ( -
+ @@ -283,20 +319,19 @@ export class SearchFormImpl extends React.PureComponent { } > - this.handleChange({ service: value })} > {services.map(service => ( ))} - + } > - this.handleChange({ operation: value })} > {['all'].concat(opsForSvc).map(op => ( ))} - + } > - this.handleChange({ tags: e.target.value })} /> - this.handleChange({ lookback: value })} > {optionsWithinMaxLookback(searchMaxLookback)} - + {selectedLookback === 'custom' && [ @@ -436,17 +470,24 @@ export class SearchFormImpl extends React.PureComponent { > - this.handleChange({ startDate: e.target.value })} /> - + this.handleChange({ startDateTime: e.target.value })} + /> , @@ -472,17 +513,24 @@ export class SearchFormImpl extends React.PureComponent { > - this.handleChange({ endDate: e.target.value })} /> - + this.handleChange({ endDateTime: e.target.value })} + /> , @@ -491,36 +539,41 @@ export class SearchFormImpl extends React.PureComponent { - this.handleChange({ maxDuration: e.target.value })} /> - this.handleChange({ minDuration: e.target.value })} /> - this.handleChange({ resultsLimit: e.target.value })} /> @@ -538,7 +591,6 @@ export class SearchFormImpl extends React.PureComponent { } SearchFormImpl.propTypes = { - handleSubmit: PropTypes.func.isRequired, invalid: PropTypes.bool, submitting: PropTypes.bool, searchMaxLookback: PropTypes.shape({ @@ -551,20 +603,14 @@ SearchFormImpl.propTypes = { operations: PropTypes.arrayOf(PropTypes.string), }) ), - selectedService: PropTypes.string, - selectedLookback: PropTypes.string, }; SearchFormImpl.defaultProps = { invalid: false, services: [], submitting: false, - selectedService: null, - selectedLookback: null, }; -export const searchSideBarFormSelector = formValueSelector('searchSideBar'); - export function mapStateToProps(state) { const { service, @@ -673,23 +719,19 @@ export function mapStateToProps(state) { traceIDs: traceIDs || null, }, searchMaxLookback: _get(state, 'config.search.maxLookback'), - selectedService: searchSideBarFormSelector(state, 'service'), - selectedLookback: searchSideBarFormSelector(state, 'lookback'), }; } export function mapDispatchToProps(dispatch) { const { searchTraces } = bindActionCreators(jaegerApiActions, dispatch); return { - onSubmit: fields => submitForm(fields, searchTraces), + changeServiceHandler: service => + dispatch({ + type: SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE, + payload: service, + }), + submitFormHandler: fields => submitForm(fields, searchTraces), }; } -export default connect( - mapStateToProps, - mapDispatchToProps -)( - reduxForm({ - form: 'searchSideBar', - })(SearchFormImpl) -); +export default connect(mapStateToProps, mapDispatchToProps)(SearchFormImpl); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js index 495902a8d4..348b584cc4 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js @@ -20,6 +20,8 @@ import { shallow } from 'enzyme'; import dayjs from 'dayjs'; import queryString from 'query-string'; import store from 'store'; +import sinon from 'sinon'; +import * as jaegerApiActions from '../../actions/jaeger-api'; import { convertQueryParamsToFormDates, @@ -36,6 +38,7 @@ import { } from './SearchForm'; import * as markers from './SearchForm.markers'; import getConfig from '../../utils/config/get-config'; +import { SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE } from '../../constants/search-form'; function makeDateParams(dateOffset = 0) { const date = new Date(); @@ -65,6 +68,8 @@ const defaultProps = { { name: 'svcA', operations: ['A', 'B'] }, { name: 'svcB', operations: ['A', 'B'] }, ], + changeServiceHandler: () => {}, + submitFormHandler: () => {}, }; describe('conversion utils', () => { @@ -389,26 +394,26 @@ describe('', () => { it('enables operations only when a service is selected', () => { let ops = wrapper.find('[placeholder="Select An Operation"]'); - expect(ops.prop('props').disabled).toBe(true); - wrapper = shallow(); + expect(ops.prop('disabled')).toBe(true); + wrapper.instance().handleChange({ service: 'svcA' }); ops = wrapper.find('[placeholder="Select An Operation"]'); - expect(ops.prop('props').disabled).toBe(false); + expect(ops.prop('disabled')).toBe(false); }); it('keeps operation disabled when no service selected', () => { let ops = wrapper.find('[placeholder="Select An Operation"]'); - expect(ops.prop('props').disabled).toBe(true); - wrapper = shallow(); + expect(ops.prop('disabled')).toBe(true); + wrapper.instance().handleChange({ service: '' }); ops = wrapper.find('[placeholder="Select An Operation"]'); - expect(ops.prop('props').disabled).toBe(true); + expect(ops.prop('disabled')).toBe(true); }); it('enables operation when unknown service selected', () => { let ops = wrapper.find('[placeholder="Select An Operation"]'); - expect(ops.prop('props').disabled).toBe(true); - wrapper = shallow(); + expect(ops.prop('disabled')).toBe(true); + wrapper.instance().handleChange({ service: 'svcC' }); ops = wrapper.find('[placeholder="Select An Operation"]'); - expect(ops.prop('props').disabled).toBe(false); + expect(ops.prop('disabled')).toBe(false); }); it('shows custom date inputs when `props.selectedLookback` is "custom"', () => { @@ -419,20 +424,23 @@ describe('', () => { ]; } expect(getDateFieldLengths(wrapper)).toEqual([0, 0]); - wrapper = shallow(); + wrapper = shallow(); + wrapper.instance().handleChange({ lookback: 'custom' }); expect(getDateFieldLengths(wrapper)).toEqual([1, 1]); }); it('disables the submit button when a service is not selected', () => { let btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); expect(btn.prop('disabled')).toBeTruthy(); - wrapper = shallow(); + wrapper = shallow(); + wrapper.instance().handleChange({ service: 'svcA' }); btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); expect(btn.prop('disabled')).toBeFalsy(); }); it('disables the submit button when the form has invalid data', () => { - wrapper = shallow(); + wrapper = shallow(); + wrapper.instance().handleChange({ service: 'svcA' }); let btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); // If this test fails on the following expect statement, this may be a false negative caused by a separate // regression. @@ -451,9 +459,104 @@ describe('', () => { }, }; window.getJaegerUiConfig = jest.fn(() => config); - wrapper = shallow(); - const field = wrapper.find(`Field[name="resultsLimit"]`); - expect(field.prop('props').max).toEqual(maxLimit); + wrapper = shallow(); + wrapper.instance().handleChange({ service: 'svcA' }); + const field = wrapper.find(`Input[name="resultsLimit"]`); + expect(field.prop('max')).toEqual(maxLimit); + }); + + it('updates state when tags input changes', () => { + const tagsInput = wrapper.find('Input[name="tags"]'); + tagsInput.simulate('change', { target: { value: 'new=tag' } }); + expect(wrapper.state('formData').tags).toBe('new=tag'); + }); + + it('updates state when service input changes', () => { + const serviceInput = wrapper.find('SearchableSelect[name="service"]'); + serviceInput.simulate('change', 'svcA'); + expect(wrapper.state('formData').service).toBe('svcA'); + }); + + it('updates state when operation input changes', () => { + const operationInput = wrapper.find('SearchableSelect[name="operation"]'); + operationInput.simulate('change', 'A'); + expect(wrapper.state('formData').operation).toBe('A'); + }); + + it('updates state when lookback input changes', () => { + const lookbackInput = wrapper.find('SearchableSelect[name="lookback"]'); + lookbackInput.simulate('change', 'new-lookback'); + expect(wrapper.state('formData').lookback).toBe('new-lookback'); + }); + + it('updates state when date and time fields input changes', () => { + const lookbackInput = wrapper.find('SearchableSelect[name="lookback"]'); + lookbackInput.simulate('change', 'custom'); + expect(wrapper.state('formData').lookback).toBe('custom'); + + const startDateInput = wrapper.find('Input[name="startDate"]'); + startDateInput.simulate('change', { target: { value: 'new-date' } }); + expect(wrapper.state('formData').startDate).toBe('new-date'); + + const startDateTimeInput = wrapper.find('Input[name="startDateTime"]'); + startDateTimeInput.simulate('change', { target: { value: 'new-time' } }); + expect(wrapper.state('formData').startDateTime).toBe('new-time'); + + const endDateInput = wrapper.find('Input[name="endDate"]'); + endDateInput.simulate('change', { target: { value: 'new-date' } }); + expect(wrapper.state('formData').endDate).toBe('new-date'); + + const endDateTimeInput = wrapper.find('Input[name="endDateTime"]'); + endDateTimeInput.simulate('change', { target: { value: 'new-time' } }); + expect(wrapper.state('formData').endDateTime).toBe('new-time'); + }); + + it('updates state when minDuration input changes', () => { + const minDurationInput = wrapper.find('ValidatedFormField[name="minDuration"]'); + minDurationInput.simulate('change', { target: { value: 'new-minDuration' } }); + expect(wrapper.state('formData').minDuration).toBe('new-minDuration'); + }); + + it('updates state when maxDuration input changes', () => { + const maxDurationInput = wrapper.find('ValidatedFormField[name="maxDuration"]'); + maxDurationInput.simulate('change', { target: { value: 'new-maxDuration' } }); + expect(wrapper.state('formData').maxDuration).toBe('new-maxDuration'); + }); + + it('updates state when resultsLimit input changes', () => { + const resultsLimitInput = wrapper.find('Input[name="resultsLimit"]'); + resultsLimitInput.simulate('change', { target: { value: 'new-resultsLimit' } }); + expect(wrapper.state('formData').resultsLimit).toBe('new-resultsLimit'); + }); +}); + +describe('submitFormHandler', () => { + let wrapper; + let submitFormHandler; + let preventDefault; + + beforeEach(() => { + submitFormHandler = sinon.spy(); + preventDefault = sinon.spy(); + wrapper = shallow( + + ); + }); + + it('calls submitFormHandler with formData on form submit', () => { + const formData = wrapper.state('formData'); + wrapper.instance().handleSubmit({ preventDefault }); + expect(preventDefault.calledOnce).toBe(true); + expect(submitFormHandler.calledOnceWith(formData)).toBe(true); + }); + + it('prevents default form submission behavior', () => { + wrapper.instance().handleSubmit({ preventDefault }); + expect(preventDefault.calledOnce).toBe(true); }); }); @@ -586,7 +689,37 @@ describe('mapStateToProps()', () => { describe('mapDispatchToProps()', () => { it('creates the actions correctly', () => { expect(mapDispatchToProps(() => {})).toEqual({ - onSubmit: expect.any(Function), + changeServiceHandler: expect.any(Function), + submitFormHandler: expect.any(Function), + }); + }); + + it('should dispatch changeServiceHandler correctly', () => { + const dispatch = jest.fn(); + const { changeServiceHandler } = mapDispatchToProps(dispatch); + const service = 'test-service'; + + changeServiceHandler(service); + + expect(dispatch).toHaveBeenCalledWith({ + type: SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE, + payload: service, }); }); + + it('should dispatch submitFormHandler correctly', () => { + const dispatch = jest.fn(); + const searchTraces = jest.fn(); + const fields = { + lookback: '1ms', + operation: 'A', + resultsLimit: 20, + service: 'svcA', + }; + + jest.spyOn(jaegerApiActions, 'searchTraces').mockReturnValue(searchTraces); + const { submitFormHandler } = mapDispatchToProps(dispatch); + submitFormHandler(fields); + expect(dispatch).toHaveBeenCalledWith(expect.any(Function)); + }); }); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js index 1cac4a08fd..5c65866bd7 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js @@ -14,17 +14,6 @@ import { BrowserRouter, MemoryRouter } from 'react-router-dom'; -jest.mock('redux-form', () => { - function reduxForm() { - return component => component; - } - function formValueSelector() { - return () => null; - } - const Field = () =>
; - return { Field, formValueSelector, reduxForm }; -}); - jest.mock('store'); /* eslint-disable import/first */ diff --git a/packages/jaeger-ui/src/constants/search-form.tsx b/packages/jaeger-ui/src/constants/search-form.tsx index e716b295af..4cf53bc56c 100644 --- a/packages/jaeger-ui/src/constants/search-form.tsx +++ b/packages/jaeger-ui/src/constants/search-form.tsx @@ -16,4 +16,4 @@ export const DEFAULT_OPERATION = 'all'; export const DEFAULT_LOOKBACK = '1h'; export const DEFAULT_LIMIT = 20; -export const FORM_CHANGE_ACTION_TYPE = '@@redux-form/CHANGE'; +export const SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE = '@@redux/searchSideBar/CHANGE_SERVICE'; diff --git a/packages/jaeger-ui/src/middlewares/index.js b/packages/jaeger-ui/src/middlewares/index.js index 21f8c3eb9a..9c3b45f7f5 100644 --- a/packages/jaeger-ui/src/middlewares/index.js +++ b/packages/jaeger-ui/src/middlewares/index.js @@ -13,11 +13,11 @@ // limitations under the License. import promiseMiddleware from 'redux-promise-middleware'; -import { change } from 'redux-form'; import { replace } from 'redux-first-history'; import { searchTraces, fetchServiceOperations } from '../actions/jaeger-api'; import { getUrl as getSearchUrl } from '../components/SearchTracePage/url'; +import { SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE } from '../constants/search-form'; export { default as trackMiddleware } from './track'; @@ -25,14 +25,8 @@ export { default as trackMiddleware } from './track'; * Middleware to load "operations" for a particular service. */ export const loadOperationsForServiceMiddleware = store => next => action => { - if ( - action.type === '@@redux-form/CHANGE' && - action.meta.form === 'searchSideBar' && - action.meta.field === 'service' && - action.payload !== '-' - ) { + if (action.type === SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE && action.payload !== '-') { store.dispatch(fetchServiceOperations(action.payload)); - store.dispatch(change('searchSideBar', 'operation', 'all')); } return next(action); }; diff --git a/packages/jaeger-ui/src/middlewares/index.test.js b/packages/jaeger-ui/src/middlewares/index.test.js index 933102bea5..1f53e8f564 100644 --- a/packages/jaeger-ui/src/middlewares/index.test.js +++ b/packages/jaeger-ui/src/middlewares/index.test.js @@ -23,10 +23,9 @@ jest.mock( }) ); -import { change } from 'redux-form'; - import * as jaegerMiddlewares from './index'; import { fetchServiceOperations } from '../actions/jaeger-api'; +import { SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE } from '../constants/search-form'; it('jaegerMiddlewares should contain the promise middleware', () => { expect(typeof jaegerMiddlewares.promise).toBe('function'); @@ -36,8 +35,11 @@ it('loadOperationsForServiceMiddleware fetches operations for services', () => { const { loadOperationsForServiceMiddleware } = jaegerMiddlewares; const dispatch = jest.fn(); const next = jest.fn(); - const action = change('searchSideBar', 'service', 'yo'); + const action = { + type: SEARCH_SIDEBAR_CHANGE_SERVICE_ACTION_TYPE, + payload: 'yo', + }; loadOperationsForServiceMiddleware({ dispatch })(next)(action); expect(dispatch).toHaveBeenCalledWith(fetchServiceOperations('yo')); - expect(dispatch).toHaveBeenCalledWith(change('searchSideBar', 'operation', 'all')); + expect(next).toHaveBeenCalledWith(action); }); diff --git a/packages/jaeger-ui/src/reducers/index.js b/packages/jaeger-ui/src/reducers/index.js index a109e8a387..b29369f7f2 100644 --- a/packages/jaeger-ui/src/reducers/index.js +++ b/packages/jaeger-ui/src/reducers/index.js @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { reducer as formReducer } from 'redux-form'; - import config from './config'; import dependencies from './dependencies'; import ddg from './ddg'; @@ -32,5 +30,4 @@ export default { services, metrics, trace, - form: formReducer, }; diff --git a/packages/jaeger-ui/src/utils/redux-form-field-adapter.css b/packages/jaeger-ui/src/utils/ValidatedFormField.css similarity index 92% rename from packages/jaeger-ui/src/utils/redux-form-field-adapter.css rename to packages/jaeger-ui/src/utils/ValidatedFormField.css index 2448f0f9db..d1999944cb 100644 --- a/packages/jaeger-ui/src/utils/redux-form-field-adapter.css +++ b/packages/jaeger-ui/src/utils/ValidatedFormField.css @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.AdaptedReduxFormField--isValidatedInput.is-invalid { +.value-is-invalid { background: #fff9f8; border: 1px solid #c00; } diff --git a/packages/jaeger-ui/src/utils/ValidatedFormField.test.js b/packages/jaeger-ui/src/utils/ValidatedFormField.test.js new file mode 100644 index 0000000000..0fec48a1bc --- /dev/null +++ b/packages/jaeger-ui/src/utils/ValidatedFormField.test.js @@ -0,0 +1,49 @@ +// Copyright (c) 2025 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import ValidatedFormField from './ValidatedFormField'; + +describe('ValidatedFormField', () => { + const mockValidate = jest.fn(); + const mockOnChange = jest.fn(); + const defaultProps = { + name: 'formField', + placeholder: 'formFieldPlaceholder', + disabled: false, + validate: mockValidate, + onChange: mockOnChange, + value: 'initial value', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders without crashing', () => { + const { getByPlaceholderText } = render(); + expect(getByPlaceholderText('formFieldPlaceholder')).not.toBeNull(); + }); + + it('calls validate function with the correct value', () => { + render(); + expect(mockValidate).toHaveBeenCalledWith('initial value'); + }); + + it('displays Popover when validation fails and blur is true', () => { + mockValidate.mockReturnValue({ content: 'error content', title: 'error title' }); + const { getByPlaceholderText, getByText } = render(); + const input = getByPlaceholderText('formFieldPlaceholder'); + fireEvent.blur(input); + expect(getByText('error content')).not.toBeNull(); + }); + + it('hides Popover when validation passes or blur is false', () => { + mockValidate.mockReturnValue(null); + const { getByPlaceholderText, queryByText } = render(); + const input = getByPlaceholderText('formFieldPlaceholder'); + fireEvent.focus(input); + expect(queryByText('error content')).toBeNull(); + }); +}); diff --git a/packages/jaeger-ui/src/utils/ValidatedFormField.tsx b/packages/jaeger-ui/src/utils/ValidatedFormField.tsx new file mode 100644 index 0000000000..379ac68261 --- /dev/null +++ b/packages/jaeger-ui/src/utils/ValidatedFormField.tsx @@ -0,0 +1,35 @@ +// Copyright (c) 2025 The Jaeger Authors. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from 'react'; +import { Input, Popover } from 'antd'; +import cx from 'classnames'; + +import './ValidatedFormField.css'; + +export default function ValidatedFormField(props: any) { + const [blur, setOnBlur] = useState(false); + const { onChange: handleChange, validate, ...rest } = props; + + const validationResult = validate(rest.value); + const isInvalid = blur && Boolean(validationResult); + + return ( + + { + setOnBlur(true); + }} + onFocus={() => { + setOnBlur(false); + }} + type="text" + {...rest} + /> + + ); +} diff --git a/packages/jaeger-ui/src/utils/__snapshots__/redux-form-field-adapter.test.js.snap b/packages/jaeger-ui/src/utils/__snapshots__/redux-form-field-adapter.test.js.snap deleted file mode 100644 index 2d77ded995..0000000000 --- a/packages/jaeger-ui/src/utils/__snapshots__/redux-form-field-adapter.test.js.snap +++ /dev/null @@ -1,90 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reduxFormFieldAdapter not validated input should render as expected 1`] = ` - -`; - -exports[`reduxFormFieldAdapter validate input should render Popover as invisible if there is an error but the field is active 1`] = ` - - - -`; - -exports[`reduxFormFieldAdapter validate input should render Popover as visible if there is an error and the field is not active 1`] = ` - - - -`; - -exports[`reduxFormFieldAdapter validate input should render as expected when there is not an error 1`] = ` - - - -`; diff --git a/packages/jaeger-ui/src/utils/configure-store.test.js b/packages/jaeger-ui/src/utils/configure-store.test.js index de4fcf1ef9..21227ccb96 100644 --- a/packages/jaeger-ui/src/utils/configure-store.test.js +++ b/packages/jaeger-ui/src/utils/configure-store.test.js @@ -26,5 +26,4 @@ it('configureStore() should return the redux store', () => { expect({}.hasOwnProperty.call(store.getState(), 'router')).toBeTruthy(); expect({}.hasOwnProperty.call(store.getState(), 'trace')).toBeTruthy(); - expect({}.hasOwnProperty.call(store.getState(), 'form')).toBeTruthy(); }); diff --git a/packages/jaeger-ui/src/utils/redux-form-field-adapter.test.js b/packages/jaeger-ui/src/utils/redux-form-field-adapter.test.js deleted file mode 100644 index bd728323f9..0000000000 --- a/packages/jaeger-ui/src/utils/redux-form-field-adapter.test.js +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Input, Popover } from 'antd'; -import { shallow } from 'enzyme'; -import React from 'react'; - -import reduxFormFieldAdapter from './redux-form-field-adapter'; - -describe('reduxFormFieldAdapter', () => { - const error = { - content: 'error content', - title: 'error title', - }; - let input; - let meta; - - beforeEach(() => { - input = { - onChange: function onChange() {}, - onBlur: function onBlur() {}, - value: 'inputValue', - }; - meta = { - error: null, - active: false, - }; - }); - - describe('not validated input', () => { - const AdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input }); - it('should render as expected', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('validate input', () => { - const AdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input, isValidatedInput: true }); - it('should render as expected when there is not an error', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should render Popover as invisible if there is an error but the field is active', () => { - meta.error = error; - meta.active = true; - const wrapper = shallow(); - expect(wrapper.find(Popover).prop('visible')).toBeFalsy(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should render Popover as visible if there is an error and the field is not active', () => { - meta.error = error; - const wrapper = shallow(); - expect(wrapper.find(Popover).prop('open')).toBeTruthy(); - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('onChangeAdapter', () => { - it('should use input.onChange when adapter is not provided', () => { - const AdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input }); - const wrapper = shallow(); - expect(wrapper.find(Input).prop('onChange')).toBe(input.onChange); - }); - - it('should call input.onChange with result of adapter when adapter is provided', () => { - const onChangeMockInput = 'onChangeMockInput'; - const onChangeAdapterMockReturnValue = 'onChangeAdapterMockReturnValue'; - const onChangeAdapter = jest.fn().mockReturnValue(onChangeAdapterMockReturnValue); - const onChangeSpy = jest.fn(); - input.onChange = onChangeSpy; - const AdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input, onChangeAdapter }); - const wrapper = shallow(); - - const renderedOnChangeFn = wrapper.find(Input).prop('onChange'); - expect(renderedOnChangeFn).not.toBe(input.onChange); - renderedOnChangeFn(onChangeMockInput); - expect(onChangeAdapter).toHaveBeenCalledWith(onChangeMockInput); - expect(onChangeSpy).toHaveBeenCalledWith(onChangeAdapterMockReturnValue); - }); - }); -}); diff --git a/packages/jaeger-ui/src/utils/redux-form-field-adapter.tsx b/packages/jaeger-ui/src/utils/redux-form-field-adapter.tsx deleted file mode 100644 index 21d4bbf388..0000000000 --- a/packages/jaeger-ui/src/utils/redux-form-field-adapter.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Popover } from 'antd'; -import cx from 'classnames'; -import * as React from 'react'; - -import './redux-form-field-adapter.css'; - -const noop = () => {}; - -export default function reduxFormFieldAdapter({ - AntInputComponent, - onChangeAdapter, - isValidatedInput = false, -}: { - AntInputComponent: React.ComponentType; - onChangeAdapter?: (evt: React.ChangeEvent) => any; - isValidatedInput?: boolean; -}) { - return function _reduxFormFieldAdapter(props: any) { - const { - input: { onBlur, onChange, onFocus, value }, - children, - ...rest - } = props; - const isInvalid = !rest.meta.active && Boolean(rest.meta.error); - const content = ( - onChange(onChangeAdapter(arg)) : onChange} - value={value} - {...rest} - > - {children} - - ); - return isValidatedInput ? ( - - {content} - - ) : ( - content - ); - }; -}