diff --git a/src/legacy/ui/public/vis/editors/default/components/bottom_bar.tsx b/src/legacy/ui/public/vis/editors/default/components/bottom_bar.tsx index 6bf5546d8fdad1..58d91218d540a3 100644 --- a/src/legacy/ui/public/vis/editors/default/components/bottom_bar.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/bottom_bar.tsx @@ -18,7 +18,14 @@ */ import React, { useCallback } from 'react'; -import { EuiBottomBar, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { + EuiBottomBar, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiToolTip, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Vis } from 'ui/vis'; @@ -27,6 +34,8 @@ import { discardChanges, EditorAction } from '../state'; interface DefaultEditorBottomBarProps { applyChanges(): void; isDirty: boolean; + isInvalid: boolean; + isTouched: boolean; dispatch: React.Dispatch; vis: Vis; } @@ -34,6 +43,8 @@ interface DefaultEditorBottomBarProps { function DefaultEditorBottomBar({ applyChanges, isDirty, + isInvalid, + isTouched, dispatch, vis, }: DefaultEditorBottomBarProps) { @@ -71,25 +82,50 @@ function DefaultEditorBottomBar({ - - - + {isInvalid && isTouched ? ( + + + + + + ) : ( + + + + )} )} diff --git a/src/legacy/ui/public/vis/editors/default/default_editor.tsx b/src/legacy/ui/public/vis/editors/default/default_editor.tsx index a8ce94b4db9781..f480390ebae88e 100644 --- a/src/legacy/ui/public/vis/editors/default/default_editor.tsx +++ b/src/legacy/ui/public/vis/editors/default/default_editor.tsx @@ -23,6 +23,7 @@ import { getVisualizeLoader } from 'ui/visualize'; import { EmbeddedVisualizeHandler } from 'ui/visualize/loader/embedded_visualize_handler'; import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/types'; +import './vis_type_agg_filter'; import { DefaultEditorSideBar } from './components/sidebar'; import { DefaultEditorBottomBar } from './components/bottom_bar'; import { useEditorReducer, useEditorContext, useEditorFormState } from './state'; @@ -80,14 +81,19 @@ function DefaultEditor({ }, [vis]); const applyChanges = useCallback(() => { + setTouched(true); + + if (formState.invalid) { + return; + } + vis.setCurrentState(state); vis.updateState(); vis.emit('dirtyStateChange', { isDirty: false, }); setDirty(false); - setTouched(true); - }, [vis, state]); + }, [vis, state, formState.invalid, setDirty, setTouched]); return (
@@ -128,6 +134,8 @@ function DefaultEditor({ applyChanges={applyChanges} dispatch={dispatch} isDirty={isDirty} + isTouched={formState.touched} + isInvalid={formState.invalid} vis={vis} />
diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/fancy_forms.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/fancy_forms.js deleted file mode 100644 index 5760a1dc77e7ae..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/fancy_forms.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -describe('fancy forms', function () { - let $el; - let $scope; - let $compile; - let $rootScope; - let ngForm; - - function generateEl() { - return $('
').html( - $('') - ); - } - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($injector) { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - - $scope = $rootScope.$new(); - $el = generateEl(); - - $compile($el)($scope); - $scope.$apply(); - - ngForm = $el.controller('form'); - })); - - describe('ngFormController', function () { - it('counts errors', function () { - expect(ngForm.errorCount()).to.be(1); - }); - - it('clears errors', function () { - $scope.val = 'something'; - $scope.$apply(); - expect(ngForm.errorCount()).to.be(0); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/nested_fancy_forms.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/nested_fancy_forms.js deleted file mode 100644 index 095007004ae7f9..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/nested_fancy_forms.js +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import testSubjSelector from '@kbn/test-subj-selector'; -import sinon from 'sinon'; -import $ from 'jquery'; - -const template = ` - - - - -
-`; - -describe('fancy forms', function () { - let setup; - const trash = []; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(($injector) => { - const $rootScope = $injector.get('$rootScope'); - const $compile = $injector.get('$compile'); - - setup = function (options = {}) { - const { - name = 'person1', - tasks = [], - onSubmit = () => {}, - } = options; - - const $el = $(template).appendTo('body'); - trash.push(() => $el.remove()); - const $scope = $rootScope.$new(); - - $scope.name = name; - $scope.tasks = tasks; - $scope.onSubmit = onSubmit; - - $compile($el)($scope); - $scope.$apply(); - - return { - $el, - $scope, - }; - }; - })); - - afterEach(() => trash.splice(0).forEach(fn => fn())); - - describe('nested forms', function () { - it('treats new fields as "soft" errors', function () { - const { $scope } = setup({ name: '' }); - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(0); - }); - - it('upgrades fields to regular errors on attempted submit', function () { - const { $scope, $el } = setup({ name: '' }); - - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(0); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('prevents submit when there are errors', function () { - const onSubmit = sinon.stub(); - const { $scope, $el } = setup({ name: '', onSubmit }); - - expect($scope.person.errorCount()).to.be(1); - sinon.assert.notCalled(onSubmit); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(1); - sinon.assert.notCalled(onSubmit); - - $scope.$apply(() => { - $scope.name = 'foo'; - }); - - expect($scope.person.errorCount()).to.be(0); - sinon.assert.notCalled(onSubmit); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(0); - sinon.assert.calledOnce(onSubmit); - }); - - it('new fields are no longer soft after blur', function () { - const { $scope, $el } = setup({ name: '' }); - expect($scope.person.softErrorCount()).to.be(0); - $el.find(testSubjSelector('name')).blur(); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('counts errors/softErrors in sub forms', function () { - const { $scope, $el } = setup(); - - expect($scope.person.errorCount()).to.be(0); - - $scope.$apply(() => { - $scope.tasks = [ - { - name: 'foo', - description: '' - }, - { - name: 'foo', - description: '' - } - ]; - }); - - expect($scope.person.errorCount()).to.be(2); - expect($scope.person.softErrorCount()).to.be(0); - - $el.find(testSubjSelector('taskDesc')).first().blur(); - - expect($scope.person.errorCount()).to.be(2); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('only counts down', function () { - const { $scope, $el } = setup({ - tasks: [ - { - name: 'foo', - description: '' - }, - { - name: 'bar', - description: '' - }, - { - name: 'baz', - description: '' - } - ] - }); - - // top level form sees 3 errors - expect($scope.person.errorCount()).to.be(3); - expect($scope.person.softErrorCount()).to.be(0); - - $el.find('ng-form').toArray().forEach((el, i) => { - const $task = $(el); - const $taskScope = $task.scope(); - const form = $task.controller('form'); - - // sub forms only see one error - expect(form.errorCount()).to.be(1); - expect(form.softErrorCount()).to.be(0); - - // blurs only count locally - $task.find(testSubjSelector('taskDesc')).blur(); - expect(form.softErrorCount()).to.be(1); - - // but parent form see them - expect($scope.person.softErrorCount()).to.be(1); - - $taskScope.$apply(() => { - $taskScope.task.description = 'valid'; - }); - - expect(form.errorCount()).to.be(0); - expect(form.softErrorCount()).to.be(0); - expect($scope.person.errorCount()).to.be(2 - i); - expect($scope.person.softErrorCount()).to.be(0); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js deleted file mode 100644 index 9ffebb792d3fd5..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 { uiModules } from '../../../../modules'; - -import { decorateFormController } from './kbn_form_controller'; -import { decorateModelController } from './kbn_model_controller'; - -uiModules - .get('kibana') - .config(function ($provide) { - $provide.decorator('formDirective', decorateFormController); - $provide.decorator('ngFormDirective', decorateFormController); - $provide.decorator('ngModelDirective', decorateModelController); - }); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js deleted file mode 100644 index 927e6d69e3c8a3..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 './fancy_forms'; diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js deleted file mode 100644 index 2a288cafcf6f1e..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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. - */ - -export function decorateFormController($delegate, $injector) { - const [directive] = $delegate; - const FormController = directive.controller; - - class KbnFormController extends FormController { - // prevent inheriting FormController's static $inject property - // which is angular's cache of the DI arguments for a function - static $inject = ['$scope', '$element']; - - constructor($scope, $element, ...superArgs) { - super(...superArgs); - - const onSubmit = (event) => { - this._markInvalidTouched(event); - }; - - $element.on('submit', onSubmit); - $scope.$on('$destroy', () => { - $element.off('submit', onSubmit); - }); - } - - errorCount() { - return this._getInvalidModels().length; - } - - // same as error count, but filters out untouched and pristine models - softErrorCount() { - return this._getInvalidModels() - .filter(model => model.$touched || model.$dirty) - .length; - } - - $setTouched() { - this._getInvalidModels() - .forEach(model => model.$setTouched()); - } - - _markInvalidTouched(event) { - if (this.errorCount()) { - event.preventDefault(); - event.stopImmediatePropagation(); - this.$setTouched(); - } - } - - _getInvalidModels() { - return this.$$controls.reduce((acc, control) => { - // recurse into sub-form - if (typeof control._getInvalidModels === 'function') { - return [...acc, ...control._getInvalidModels()]; - } - - if (control.$invalid) { - return [...acc, control]; - } - - return acc; - }, []); - } - } - - // replace controller with our wrapper - directive.controller = [ - ...$injector.annotate(KbnFormController), - ...$injector.annotate(FormController), - (...args) => ( - new KbnFormController(...args) - ) - ]; - - return $delegate; -} - diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js deleted file mode 100644 index ce4e8a8230be66..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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. - */ - -export function decorateModelController($delegate, $injector) { - const [directive] = $delegate; - const ModelController = directive.controller; - - class KbnModelController extends ModelController { - // prevent inheriting ModelController's static $inject property - // which is angular's cache of the DI arguments for a function - static $inject = ['$scope', '$element']; - - constructor($scope, $element, ...superArgs) { - super(...superArgs); - - const onInvalid = () => { - this.$setTouched(); - }; - - // the browser emits an "invalid" event when browser supplied - // validation fails, which implies that the user has indirectly - // interacted with the control and it should be treated as "touched" - $element.on('invalid', onInvalid); - $scope.$on('$destroy', () => { - $element.off('invalid', onInvalid); - }); - } - } - - // replace controller with our wrapper - directive.controller = [ - ...$injector.annotate(KbnModelController), - ...$injector.annotate(ModelController), - (...args) => ( - new KbnModelController(...args) - ) - ]; - - return $delegate; -} diff --git a/src/legacy/ui/public/vis/editors/default/state/editor_form_state.ts b/src/legacy/ui/public/vis/editors/default/state/editor_form_state.ts index b3af5ac6759af9..ebd4fbe05e4bfb 100644 --- a/src/legacy/ui/public/vis/editors/default/state/editor_form_state.ts +++ b/src/legacy/ui/public/vis/editors/default/state/editor_form_state.ts @@ -26,26 +26,30 @@ function useEditorFormState() { const [formState, setFormState] = useState({ validity: {}, touched: false, + invalid: false, }); const setValidity: SetValidity = useCallback((modelName, value) => { - setFormState(model => ({ - ...model, - validity: { + setFormState(model => { + const validity = { ...model.validity, [modelName]: value, - }, - })); - }, []); + }; - const setTouched = useCallback( - (touched: boolean) => - setFormState(model => ({ + return { ...model, - touched, - })), - [] - ); + validity, + invalid: Object.values(validity).some(valid => !valid), + }; + }); + }, []); + + const setTouched = useCallback((touched: boolean) => { + setFormState(model => ({ + ...model, + touched, + })); + }, []); return { formState,