From cfc63ea63592eb4296398d655d9b54cceb4d6a9d Mon Sep 17 00:00:00 2001 From: luk-kaminski Date: Tue, 20 Sep 2022 06:42:10 +0200 Subject: [PATCH 1/3] Exists term is now properly validated, no matter what type of field is used (#13489) --- .../graylog/plugins/views/QueryValidationResourceIT.java | 6 ++++++ .../plugins/views/search/validation/ParsedQuery.java | 1 - .../validation/validators/FieldValueTypeValidator.java | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/full-backend-tests/src/test/java/org/graylog/plugins/views/QueryValidationResourceIT.java b/full-backend-tests/src/test/java/org/graylog/plugins/views/QueryValidationResourceIT.java index 866e21311ed4..b8b5fccb4c09 100644 --- a/full-backend-tests/src/test/java/org/graylog/plugins/views/QueryValidationResourceIT.java +++ b/full-backend-tests/src/test/java/org/graylog/plugins/views/QueryValidationResourceIT.java @@ -146,6 +146,12 @@ void testInvalidValueType() { validatableResponse.assertThat().body("explanations.error_type[0]", equalTo("INVALID_VALUE_TYPE")); } + @ContainerMatrixTest + void testSuccessfullyValidatesExistsTerms() { + verifyQueryIsValidatedSuccessfully("_exists_:timestamp"); + verifyQueryIsValidatedSuccessfully("_exists_:level"); + } + @ContainerMatrixTest void testQuotedDefaultField() { // if the validation correctly recognizes the quoted text, it should not warn about lowercase or diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/ParsedQuery.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/ParsedQuery.java index a32fce0f3a28..8892111eaa67 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/ParsedQuery.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/ParsedQuery.java @@ -20,7 +20,6 @@ import com.google.common.collect.ImmutableList; import javax.validation.constraints.NotNull; -import java.util.List; import java.util.Set; import java.util.stream.Collectors; diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/validators/FieldValueTypeValidator.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/validators/FieldValueTypeValidator.java index f8435d4a577a..dc0922f95ebc 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/validators/FieldValueTypeValidator.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/validation/validators/FieldValueTypeValidator.java @@ -93,6 +93,7 @@ private List validateQueryValues(PositionTrackingQuery decora final Map fields = availableFields.stream().collect(Collectors.toMap(MappedFieldTypeDTO::name, Function.identity())); return parsedQuery.terms().stream() + .filter(term -> !term.isExistsField()) .map(term -> { final MappedFieldTypeDTO fieldType = fields.get(term.getRealFieldName()); final Optional typeName = Optional.ofNullable(fieldType) From 54055c2cb8d9ba5ff630357001899b1d159cd93e Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Tue, 20 Sep 2022 13:48:07 +0200 Subject: [PATCH 2/3] Implement shared component for modal submit and cancel button. (#13490) * Install DM Sans font * Remove Barlow condensed font. * Create shared component for modal submit and cancel buttons. * Create shared component for modal submit and cancel button. * Implement `ModalSubmit` component for `ExportModal`. * Implement `BootstrapModalConfirm` instead of `BootstrapModalWrapper` for `DecoratorsConfigUpdate`. * Implement `ModalSubmit` for `ContentPackParameterList`. * Implement `MocalSubmit` forfurther modals. * Fixing import * Do not use `BootstrapModalConfirm` in `DecoratorsConfigUpdate`, since the modal component is onlt being used for simple confirm dialogs. * Fixing linter hints * Fixing tests --- .../bootstrap/BootstrapModalConfirm.jsx | 16 ++-- .../bootstrap/BootstrapModalForm.jsx | 12 ++- .../src/components/common/FormSubmit.tsx | 77 +++++++++++++++++++ .../src/components/common/ModalSubmit.tsx | 48 ++++++++++++ .../src/components/common/index.tsx | 2 + .../decorators/DecoratorsConfigUpdate.tsx | 7 +- .../ContentPackParameterList.jsx | 16 ++-- .../content-packs/ContentPacksList.jsx | 11 +-- .../extractors/ExtractorSortModal.jsx | 7 +- .../permissions/EntityShareModal.test.tsx | 7 +- .../permissions/EntityShareModal.tsx | 1 - .../components/export/ExportModal.test.tsx | 5 +- .../views/components/export/ExportModal.tsx | 32 ++++---- .../highlighting/HighlightForm.test.tsx | 10 +-- .../sidebar/highlighting/HighlightForm.tsx | 11 +-- 15 files changed, 196 insertions(+), 66 deletions(-) create mode 100644 graylog2-web-interface/src/components/common/FormSubmit.tsx create mode 100644 graylog2-web-interface/src/components/common/ModalSubmit.tsx diff --git a/graylog2-web-interface/src/components/bootstrap/BootstrapModalConfirm.jsx b/graylog2-web-interface/src/components/bootstrap/BootstrapModalConfirm.jsx index 95b986633597..979715cf7131 100644 --- a/graylog2-web-interface/src/components/bootstrap/BootstrapModalConfirm.jsx +++ b/graylog2-web-interface/src/components/bootstrap/BootstrapModalConfirm.jsx @@ -17,9 +17,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ModalSubmit from 'components/common/ModalSubmit'; + import Modal from './Modal'; import BootstrapModalWrapper from './BootstrapModalWrapper'; -import Button from './Button'; /** * Component that displays a confirmation dialog box that the user can @@ -34,8 +35,6 @@ class BootstrapModalConfirm extends React.Component { PropTypes.string, PropTypes.element, ]).isRequired, - /** Text to use in the cancel button. */ - cancelButtonText: PropTypes.string, /** Text to use in the confirmation button. */ confirmButtonText: PropTypes.string, /** Indicates whether the cancel button should be disabled or not. */ @@ -65,7 +64,6 @@ class BootstrapModalConfirm extends React.Component { static defaultProps = { showModal: false, - cancelButtonText: 'Cancel', confirmButtonText: 'Confirm', cancelButtonDisabled: false, confirmButtonDisabled: false, @@ -88,6 +86,7 @@ class BootstrapModalConfirm extends React.Component { onConfirm(this.close); }; + // eslint-disable-next-line react/no-unused-class-component-methods open = () => { this.modal.open(); }; @@ -105,7 +104,6 @@ class BootstrapModalConfirm extends React.Component { children, cancelButtonDisabled, confirmButtonDisabled, - cancelButtonText, confirmButtonText, } = this.props; @@ -125,8 +123,12 @@ class BootstrapModalConfirm extends React.Component { - - + ); diff --git a/graylog2-web-interface/src/components/bootstrap/BootstrapModalForm.jsx b/graylog2-web-interface/src/components/bootstrap/BootstrapModalForm.jsx index 8e42c351ae51..956b430035d5 100644 --- a/graylog2-web-interface/src/components/bootstrap/BootstrapModalForm.jsx +++ b/graylog2-web-interface/src/components/bootstrap/BootstrapModalForm.jsx @@ -18,9 +18,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import $ from 'jquery'; +import ModalSubmit from 'components/common/ModalSubmit'; + import Modal from './Modal'; import BootstrapModalWrapper from './BootstrapModalWrapper'; -import Button from './Button'; /** * Encapsulates a form element inside a bootstrap modal, hiding some custom logic that this kind of component @@ -43,8 +44,6 @@ class BootstrapModalForm extends React.Component { onCancel: PropTypes.func, /* Object with additional props to pass to the form */ formProps: PropTypes.object, - /* Text to use in the cancel button. "Cancel" is the default */ - cancelButtonText: PropTypes.string, /* Text to use in the submit button. "Submit" is the default */ submitButtonText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), submitButtonDisabled: PropTypes.bool, @@ -54,7 +53,6 @@ class BootstrapModalForm extends React.Component { static defaultProps = { backdrop: undefined, formProps: {}, - cancelButtonText: 'Cancel', submitButtonText: 'Submit', submitButtonDisabled: false, onModalOpen: () => {}, @@ -104,7 +102,6 @@ class BootstrapModalForm extends React.Component { formProps, bsSize, onModalClose, - cancelButtonText, show, submitButtonText, onModalOpen, @@ -136,8 +133,9 @@ class BootstrapModalForm extends React.Component { {body} - - + diff --git a/graylog2-web-interface/src/components/common/FormSubmit.tsx b/graylog2-web-interface/src/components/common/FormSubmit.tsx new file mode 100644 index 000000000000..4db532222806 --- /dev/null +++ b/graylog2-web-interface/src/components/common/FormSubmit.tsx @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; + +import { Button, ButtonToolbar } from 'components/bootstrap'; +import type { IconName } from 'components/common/Icon'; +import Icon from 'components/common/Icon'; +import Spinner from 'components/common/Spinner'; + +type Props = { + className?: string, + disableCancel?: boolean, + disabledSubmit?: boolean, + isSubmitting?: boolean, + leftCol?: React.ReactNode, + onCancel: () => void, + onSubmit?: () => void, + submitButtonText: string, + submitIcon?: IconName, + submitButtonType?: 'submit' | 'button', + submitLoadingText?: string, +} + +const FormSubmit = ({ + className, + disableCancel, + disabledSubmit, + isSubmitting, + leftCol, + onCancel, + onSubmit, + submitButtonText, + submitButtonType, + submitIcon, + submitLoadingText, +}: Props) => ( + + {leftCol} + + + +); + +FormSubmit.defaultProps = { + className: undefined, + disableCancel: false, + disabledSubmit: false, + isSubmitting: false, + leftCol: undefined, + onSubmit: undefined, + submitButtonType: 'submit', + submitIcon: undefined, + submitLoadingText: undefined, +}; + +export default FormSubmit; diff --git a/graylog2-web-interface/src/components/common/ModalSubmit.tsx b/graylog2-web-interface/src/components/common/ModalSubmit.tsx new file mode 100644 index 000000000000..4f3cd2078d4c --- /dev/null +++ b/graylog2-web-interface/src/components/common/ModalSubmit.tsx @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; + +import FormSubmit from 'components/common/FormSubmit'; + +type Props = React.ComponentProps + +/* eslint-disable react/prop-types */ +const ModalSubmit = ({ + className, + disabledSubmit, + disableCancel, + isSubmitting, + leftCol, + onCancel, + onSubmit, + submitLoadingText, + submitIcon, + submitButtonText, +}: Props) => ( + +); + +export default ModalSubmit; diff --git a/graylog2-web-interface/src/components/common/index.tsx b/graylog2-web-interface/src/components/common/index.tsx index 9b2e8c4ce73c..3c1ad0575560 100644 --- a/graylog2-web-interface/src/components/common/index.tsx +++ b/graylog2-web-interface/src/components/common/index.tsx @@ -52,6 +52,7 @@ export { default as ExternalLinkButton } from './ExternalLinkButton'; export { default as FlatContentRow } from './FlatContentRow'; export { default as FormikFormGroup } from './FormikFormGroup'; export { default as FormikInput } from './FormikInput'; +export { default as FormSubmit } from './FormSubmit'; export { default as HasOwnership } from './HasOwnership'; export { default as HoverForHelp } from './HoverForHelp'; export { default as ISODurationInput } from './ISODurationInput'; @@ -70,6 +71,7 @@ export { default as LoadingIndicator } from './LoadingIndicator'; export { default as LocaleSelect } from './LocaleSelect'; export { default as Markdown } from './Markdown'; export { default as MessageDetailsDefinitionList } from './MessageDetailsDefinitionList'; +export { default as ModalSubmit } from './ModalSubmit'; export { default as MultiSelect } from './MultiSelect'; export { default as OverlayElement } from './OverlayElement'; export { default as OverlayTrigger } from './OverlayTrigger'; diff --git a/graylog2-web-interface/src/components/configurations/decorators/DecoratorsConfigUpdate.tsx b/graylog2-web-interface/src/components/configurations/decorators/DecoratorsConfigUpdate.tsx index 2e21de12856d..d1311350b66a 100644 --- a/graylog2-web-interface/src/components/configurations/decorators/DecoratorsConfigUpdate.tsx +++ b/graylog2-web-interface/src/components/configurations/decorators/DecoratorsConfigUpdate.tsx @@ -18,8 +18,8 @@ import React, { useCallback, useState } from 'react'; import { cloneDeep } from 'lodash'; import BootstrapModalWrapper from 'components/bootstrap/BootstrapModalWrapper'; -import { Button, Modal } from 'components/bootstrap'; -import { IfPermitted } from 'components/common'; +import { Modal } from 'components/bootstrap'; +import { IfPermitted, ModalSubmit } from 'components/common'; import type { Stream } from 'stores/streams/StreamsStore'; import DecoratorList from 'views/components/messagelist/decorators/DecoratorList'; import AddDecoratorButton from 'views/components/messagelist/decorators/AddDecoratorButton'; @@ -98,8 +98,7 @@ const DecoratorsConfigUpdate = ({ streams, decorators, types, show = false, onCa - - + ); diff --git a/graylog2-web-interface/src/components/content-packs/ContentPackParameterList.jsx b/graylog2-web-interface/src/components/content-packs/ContentPackParameterList.jsx index f6bfd0d42995..e463055f0f71 100644 --- a/graylog2-web-interface/src/components/content-packs/ContentPackParameterList.jsx +++ b/graylog2-web-interface/src/components/content-packs/ContentPackParameterList.jsx @@ -19,7 +19,7 @@ import React from 'react'; import { findIndex } from 'lodash'; import { Badge, Button, Modal, ButtonToolbar } from 'components/bootstrap'; -import { DataTable, SearchForm, Icon } from 'components/common'; +import { DataTable, SearchForm, Icon, ModalSubmit } from 'components/common'; import BootstrapModalWrapper from 'components/bootstrap/BootstrapModalWrapper'; import ContentPackEditParameter from 'components/content-packs/ContentPackEditParameter'; import ObjectUtils from 'util/ObjectUtils'; @@ -145,7 +145,8 @@ class ContentPackParameterList extends React.Component { }; const size = parameter ? 'xsmall' : 'small'; - const name = parameter ? 'Edit' : 'Create parameter'; + const titleName = parameter ? 'Edit parameter' : 'Create parameter'; + const triggerButtonName = parameter ? 'Edit' : 'Create parameter'; const modal = ( { modalRef = node; }} bsSize="large"> @@ -162,12 +163,9 @@ class ContentPackParameterList extends React.Component { parameterToEdit={parameter} /> -
- - - - -
+
); @@ -178,7 +176,7 @@ class ContentPackParameterList extends React.Component { bsSize={size} title="Edit Modal" onClick={openModal}> - {name} + {triggerButtonName} {modal} diff --git a/graylog2-web-interface/src/components/content-packs/ContentPacksList.jsx b/graylog2-web-interface/src/components/content-packs/ContentPacksList.jsx index 120ea640d662..6ecc863f227f 100644 --- a/graylog2-web-interface/src/components/content-packs/ContentPacksList.jsx +++ b/graylog2-web-interface/src/components/content-packs/ContentPacksList.jsx @@ -21,7 +21,6 @@ import { LinkContainer, Link } from 'components/common/router'; import Routes from 'routing/Routes'; import { Button, - ButtonToolbar, Col, DropdownButton, MenuItem, @@ -30,6 +29,7 @@ import { } from 'components/bootstrap'; import { Pagination, PageSizeSelect, + ModalSubmit, } from 'components/common'; import TypeAheadDataFilter from 'components/common/TypeAheadDataFilter'; import BootstrapModalWrapper from 'components/bootstrap/BootstrapModalWrapper'; @@ -94,7 +94,7 @@ class ContentPacksList extends React.Component { const modal = ( { modalRef = node; }} bsSize="large"> - Install + Install Content Pack { installRef = node; }} @@ -102,12 +102,7 @@ class ContentPacksList extends React.Component { onInstall={onInstallProp} /> -
- - - - -
+
); diff --git a/graylog2-web-interface/src/components/extractors/ExtractorSortModal.jsx b/graylog2-web-interface/src/components/extractors/ExtractorSortModal.jsx index 9299286703d4..40e4aa192d62 100644 --- a/graylog2-web-interface/src/components/extractors/ExtractorSortModal.jsx +++ b/graylog2-web-interface/src/components/extractors/ExtractorSortModal.jsx @@ -17,9 +17,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { Row, Col, Modal, Button, BootstrapModalWrapper } from 'components/bootstrap'; +import { Row, Col, Modal, BootstrapModalWrapper } from 'components/bootstrap'; import SortableList from 'components/common/SortableList'; import { ExtractorsActions } from 'stores/extractors/ExtractorsStore'; +import { ModalSubmit } from 'components/common/index'; class ExtractorSortModal extends React.Component { static propTypes = { @@ -35,6 +36,7 @@ class ExtractorSortModal extends React.Component { }; } + // eslint-disable-next-line react/no-unused-class-component-methods open = () => { this.modal.open(); }; @@ -92,8 +94,7 @@ class ExtractorSortModal extends React.Component { - - + ); diff --git a/graylog2-web-interface/src/components/permissions/EntityShareModal.test.tsx b/graylog2-web-interface/src/components/permissions/EntityShareModal.test.tsx index e45e9e4dc73b..f99dd3c38fb2 100644 --- a/graylog2-web-interface/src/components/permissions/EntityShareModal.test.tsx +++ b/graylog2-web-interface/src/components/permissions/EntityShareModal.test.tsx @@ -93,9 +93,12 @@ describe('EntityShareModal', () => { it('closes modal on cancel', async () => { const onClose = jest.fn(); - const { getByText } = render(); + const { getByRole } = render(); - const cancelButton = getByText('Discard changes'); + const cancelButton = getByRole('button', { + name: /cancel/i, + hidden: true, + }); fireEvent.click(cancelButton); diff --git a/graylog2-web-interface/src/components/permissions/EntityShareModal.tsx b/graylog2-web-interface/src/components/permissions/EntityShareModal.tsx index 15625a8c4e2d..6d769fd04bd6 100644 --- a/graylog2-web-interface/src/components/permissions/EntityShareModal.tsx +++ b/graylog2-web-interface/src/components/permissions/EntityShareModal.tsx @@ -79,7 +79,6 @@ const EntityShareModal = ({ description, entityId, entityType, entityTitle, enti return ( { // Prepare expected payload const triggerFormSubmit = () => { - const submitButton = screen.getByTestId('download-button'); + const submitButton = screen.getByRole('button', { + name: /start download/i, + hidden: true, + }); fireEvent.click(submitButton); }; diff --git a/graylog2-web-interface/src/views/components/export/ExportModal.tsx b/graylog2-web-interface/src/views/components/export/ExportModal.tsx index 634d524731d5..fdb530e4f727 100644 --- a/graylog2-web-interface/src/views/components/export/ExportModal.tsx +++ b/graylog2-web-interface/src/views/components/export/ExportModal.tsx @@ -22,12 +22,12 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; import { Field, Formik, Form } from 'formik'; +import ModalSubmit from 'components/common/ModalSubmit'; import connect from 'stores/connect'; import type SearchExecutionState from 'views/logic/search/SearchExecutionState'; import { SearchExecutionStateStore } from 'views/stores/SearchExecutionStateStore'; import type View from 'views/logic/views/View'; import type Widget from 'views/logic/widgets/Widget'; -import { Icon, Spinner } from 'components/common'; import { Modal, Button } from 'components/bootstrap'; import BootstrapModalWrapper from 'components/bootstrap/BootstrapModalWrapper'; import ExportWidgetSelection from 'views/components/export/ExportWidgetSelection'; @@ -107,7 +107,7 @@ const ExportModal = ({ closeModal, view, directExportWidgetId, executionState }: return ( onSubmit={_startDownload} initialValues={initialValues}> - {({ submitForm, values: { selectedWidget, selectedFields }, setFieldValue }) => { + {({ values: { selectedWidget, selectedFields }, setFieldValue }) => { const showWidgetSelection = shouldShowWidgetSelection(singleWidgetDownload, selectedWidget, exportableWidgets); const allowWidgetSelection = shouldAllowWidgetSelection(singleWidgetDownload, showWidgetSelection, exportableWidgets); const enableDownload = shouldEnableDownload(showWidgetSelection, selectedWidget, selectedFields, loading); @@ -115,8 +115,8 @@ const ExportModal = ({ closeModal, view, directExportWidgetId, executionState }: const setSelectedFields = (newFields) => setFieldValue('selectedFields', newFields); return ( -
- + + {title} @@ -147,16 +147,22 @@ const ExportModal = ({ closeModal, view, directExportWidgetId, executionState }: - {allowWidgetSelection && } - - + + Select different message table + + ) + } + onCancel={closeModal} + disabledSubmit={!enableDownload} + isSubmitting={loading} + submitLoadingText="Downloading..." + submitIcon="cloud-download-alt" + submitButtonText="Start Download" /> - - + +
); }} diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.test.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.test.tsx index 02025831ee8d..7efa644c4f3a 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.test.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.test.tsx @@ -62,7 +62,7 @@ describe('HighlightForm', () => { ); const triggerSaveButtonClick = async () => { - const elem = await screen.findByText('Save'); + const elem = await screen.findByText('Update rule'); fireEvent.click(elem); }; @@ -79,9 +79,7 @@ describe('HighlightForm', () => { it('should render for new', async () => { const { findByText } = render( {}} />); - const form = await findByText('New Highlighting Rule'); - - expect(form).toBeInTheDocument(); + await findByText('Create Highlighting Rule'); }); it('should fire onClose on cancel', async () => { @@ -109,7 +107,7 @@ describe('HighlightForm', () => { userEvent.click(screen.getByLabelText('Static Color')); - userEvent.click(screen.getByText('Save')); + await triggerSaveButtonClick(); await waitFor(() => expect(HighlightingRulesActions.update) .toHaveBeenCalledWith(rule, expect.objectContaining({ @@ -126,7 +124,7 @@ describe('HighlightForm', () => { userEvent.clear(highestValue); userEvent.type(highestValue, '100'); - userEvent.click(screen.getByText('Save')); + await triggerSaveButtonClick(); await waitFor(() => expect(HighlightingRulesActions.update) .toHaveBeenCalledWith(rule, expect.objectContaining({ diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx index 27de1c94e41b..42b208760cc4 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx @@ -22,7 +22,7 @@ import { Formik, Form, Field } from 'formik'; import isNil from 'lodash/isNil'; import { defaultCompare } from 'logic/DefaultCompare'; -import { Input, BootstrapModalWrapper, Button, Modal } from 'components/bootstrap'; +import { Input, BootstrapModalWrapper, Modal } from 'components/bootstrap'; import FieldTypesContext from 'views/components/contexts/FieldTypesContext'; import type FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping'; import Select from 'components/common/Select'; @@ -41,6 +41,7 @@ import { GradientColor, StaticColor, } from 'views/logic/views/formatting/highlighting/HighlightingColor'; +import { ModalSubmit } from 'components/common'; type Props = { onClose: () => void, @@ -123,7 +124,8 @@ const HighlightForm = ({ onClose, rule }: Props) => { HighlightingRulesActions.add(HighlightingRule.create(field, value, condition, newColor)).then(onClose); }; - const headerTxt = rule ? 'Edit' : 'New'; + const headerPrefix = rule ? 'Edit' : 'Create'; + const submitButtonPrefix = rule ? 'Update' : 'Create'; return ( { onClose={onClose}>
- {headerTxt} Highlighting Rule + {headerPrefix} Highlighting Rule @@ -186,8 +188,7 @@ const HighlightForm = ({ onClose, rule }: Props) => { - - +
From 57ff69566e6198858c3008141f944a41711e23df Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Tue, 20 Sep 2022 14:54:47 +0200 Subject: [PATCH 3/3] Fixing active state in main navigation. (#13498) * Display active state for nav items, when nav item path matches. * Fix active state for navigation dropdowns. * Fixing linter hints * Simplify title generation in `SystemMenu`. * Fixing PropTypes error. * Replace enzyme with react testing library in `NavigationLink`. * Fixing test --- .../src/components/bootstrap/NavDropdown.jsx | 19 +++- .../src/components/bootstrap/NavItem.tsx | 26 +++++ .../src/components/bootstrap/Navbar.jsx | 7 +- .../src/components/bootstrap/imports.js | 1 - .../src/components/bootstrap/index.js | 1 + .../src/components/common/router.tsx | 37 ++++--- .../components/navigation/Navigation.test.tsx | 2 +- .../src/components/navigation/Navigation.tsx | 12 +-- .../navigation/NavigationLink.test.jsx | 24 ++--- .../src/components/navigation/SystemMenu.jsx | 96 +++++++------------ 10 files changed, 120 insertions(+), 105 deletions(-) create mode 100644 graylog2-web-interface/src/components/bootstrap/NavItem.tsx diff --git a/graylog2-web-interface/src/components/bootstrap/NavDropdown.jsx b/graylog2-web-interface/src/components/bootstrap/NavDropdown.jsx index 68902b250ee8..05b2f53f455a 100644 --- a/graylog2-web-interface/src/components/bootstrap/NavDropdown.jsx +++ b/graylog2-web-interface/src/components/bootstrap/NavDropdown.jsx @@ -15,9 +15,11 @@ * . */ +import * as React from 'react'; // eslint-disable-next-line no-restricted-imports import { NavDropdown as BootstrapNavDropdown } from 'react-bootstrap'; import styled from 'styled-components'; +import PropTypes from 'prop-types'; import menuItemStyles from './styles/menuItem'; @@ -41,10 +43,25 @@ class ModifiedBootstrapNavDropdown extends BootstrapNavDropdown { } } -const NavDropdown = styled(BootstrapNavDropdown)` +const StyledNavDropdown = styled(BootstrapNavDropdown)` ${menuItemStyles} `; +const NavDropdown = ({ inactiveTitle, title, ...props }) => { + const isActive = inactiveTitle ? inactiveTitle !== title : undefined; + + return ; +}; + +NavDropdown.propTypes = { + inactiveTitle: PropTypes.string, + title: PropTypes.node.isRequired, +}; + +NavDropdown.defaultProps = { + inactiveTitle: undefined, +}; + const ModifiedNavDropdown = styled(ModifiedBootstrapNavDropdown)` ${menuItemStyles} `; diff --git a/graylog2-web-interface/src/components/bootstrap/NavItem.tsx b/graylog2-web-interface/src/components/bootstrap/NavItem.tsx new file mode 100644 index 000000000000..86849f332d2e --- /dev/null +++ b/graylog2-web-interface/src/components/bootstrap/NavItem.tsx @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { NavItem as BootstrapNavItem } from 'react-bootstrap'; + +const NavItem = (props: React.ComponentProps) => ; + +NavItem.displayName = 'NavItem'; + +/** @component */ +export default NavItem; diff --git a/graylog2-web-interface/src/components/bootstrap/Navbar.jsx b/graylog2-web-interface/src/components/bootstrap/Navbar.jsx index 9a94b41c2cc2..03666d997962 100644 --- a/graylog2-web-interface/src/components/bootstrap/Navbar.jsx +++ b/graylog2-web-interface/src/components/bootstrap/Navbar.jsx @@ -56,11 +56,14 @@ const Navbar = styled(BootstrapNavbar)(({ theme }) => css` color: ${theme.colors.variant.darkest.default}; background-color: ${theme.colors.gray[90]}; - &:hover, - &:focus { + &:hover { color: ${theme.colors.variant.darkest.default}; background-color: ${theme.colors.gray[80]}; } + + &:focus { + background-color: ${theme.colors.gray[90]}; + } } > .disabled > a, diff --git a/graylog2-web-interface/src/components/bootstrap/imports.js b/graylog2-web-interface/src/components/bootstrap/imports.js index a0ca1ef373c7..7f3729b5aab5 100644 --- a/graylog2-web-interface/src/components/bootstrap/imports.js +++ b/graylog2-web-interface/src/components/bootstrap/imports.js @@ -27,7 +27,6 @@ export { Dropdown, Form, Grid, - NavItem, Pager, PanelGroup, Radio, // NOTE: do we want custom or keep OS styles diff --git a/graylog2-web-interface/src/components/bootstrap/index.js b/graylog2-web-interface/src/components/bootstrap/index.js index 1c14161c7c5b..bb956a97e772 100644 --- a/graylog2-web-interface/src/components/bootstrap/index.js +++ b/graylog2-web-interface/src/components/bootstrap/index.js @@ -36,6 +36,7 @@ export { default as MenuItem } from './MenuItem'; export { default as Modal } from './Modal'; export { default as Nav } from './Nav'; export { default as NavDropdown } from './NavDropdown'; +export { default as NavItem } from './NavItem'; export { default as Navbar } from './Navbar'; export { default as Panel } from './Panel'; export { default as Popover } from './Popover'; diff --git a/graylog2-web-interface/src/components/common/router.tsx b/graylog2-web-interface/src/components/common/router.tsx index 7010738bd776..8a24b7e590b0 100644 --- a/graylog2-web-interface/src/components/common/router.tsx +++ b/graylog2-web-interface/src/components/common/router.tsx @@ -23,18 +23,17 @@ import history from 'util/History'; export type HistoryElement = Location; -const _targetPathname = (to) => { - const target = typeof to?.pathname === 'string' ? to.pathname : to; +// list of children which are being used for navigation and should receive the `active` class. +const NAV_CHILDREN = ['Button', 'NavItem']; - return String(target).split(/[?#]/)[0]; -}; +const _targetPathname = (to: string) => String(to).split(/[?#]/)[0]; -const _setActiveClassName = (pathname, to, currentClassName, displayName) => { +const _setActiveClassName = (pathname: string, to: string, currentClassName: string, displayName: string, relativeActive: boolean) => { const targetPathname = _targetPathname(to); - const isActive = targetPathname === pathname; - const isButton = displayName === 'Button'; + const isActive = relativeActive ? pathname.startsWith(targetPathname) : targetPathname === pathname; + const isNavComponent = NAV_CHILDREN.includes(displayName); - return isButton && isActive + return isNavComponent && isActive ? `active ${currentClassName ?? ''}` : currentClassName; }; @@ -48,18 +47,24 @@ type Props = { children: React.ReactElement, onClick?: () => unknown, disabled?: boolean, - to: string, + to: string | { pathname: string }, + // if set the child component will receive the active class + // when the part of the URL path matches the `to` prop. + relativeActive?: boolean, }; -const isLeftClickEvent = (e) => (e.button === 0); +const isLeftClickEvent = (e: React.MouseEvent) => (e.button === 0); -const isModifiedEvent = (e) => !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); +const isModifiedEvent = (e: React.MouseEvent) => !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); -const LinkContainer = ({ children, onClick, to, ...rest }: Props) => { +const LinkContainer = ({ children, onClick, to: toProp, relativeActive, ...rest }: Props) => { const { pathname } = useLocation(); const { props: { onClick: childrenOnClick, className }, type: { displayName } } = React.Children.only(children); - const childrenClassName = useMemo(() => _setActiveClassName(pathname, to, className, displayName), [pathname, to, className, displayName]); - const _onClick = useCallback((e) => { + const to = (typeof toProp === 'object' && 'pathname' in toProp) ? toProp.pathname : toProp; + const childrenClassName = useMemo(() => _setActiveClassName(pathname, to, className, displayName, relativeActive), + [pathname, to, className, displayName, relativeActive], + ); + const _onClick = useCallback((e: React.MouseEvent) => { if (!isLeftClickEvent(e) || isModifiedEvent(e)) { return; } @@ -81,6 +86,10 @@ const LinkContainer = ({ children, onClick, to, ...rest }: Props) => { return React.cloneElement(React.Children.only(children), { ...rest, className: childrenClassName, onClick: _onClick, href: to }); }; +LinkContainer.defaultProps = { + relativeActive: false, +}; + export { Link, LinkContainer, diff --git a/graylog2-web-interface/src/components/navigation/Navigation.test.tsx b/graylog2-web-interface/src/components/navigation/Navigation.test.tsx index 295a03ed4346..667ae698bc81 100644 --- a/graylog2-web-interface/src/components/navigation/Navigation.test.tsx +++ b/graylog2-web-interface/src/components/navigation/Navigation.test.tsx @@ -240,7 +240,7 @@ describe('Navigation', () => { describe('uses correct permissions:', () => { const verifyPermissions = ({ count, links }) => { const wrapper = mount(); - const navigationLinks = wrapper.find('NavItem'); + const navigationLinks = wrapper.find('NavItem[active=false]'); expect(navigationLinks).toHaveLength(count); diff --git a/graylog2-web-interface/src/components/navigation/Navigation.tsx b/graylog2-web-interface/src/components/navigation/Navigation.tsx index 5de8eed79e50..81427eda3620 100644 --- a/graylog2-web-interface/src/components/navigation/Navigation.tsx +++ b/graylog2-web-interface/src/components/navigation/Navigation.tsx @@ -85,7 +85,7 @@ const formatPluginRoute = (pluginRoute: PluginNavigation, currentUserPermissions } return ( - + {pluginRoute.children.map((child) => formatSinglePluginRoute(child, currentUserPermissions, false))} ); @@ -134,7 +134,7 @@ const Navigation = React.memo(({ pathname }: Props) => { - + @@ -145,19 +145,19 @@ const Navigation = React.memo(({ pathname }: Props) => {