From c5dad21f5c0db6798456211525a0cb2a295c6a8b Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Fri, 1 Nov 2024 14:52:47 +0100 Subject: [PATCH 1/8] Remove redux store usage from `EditWidgetFrame`. --- .../views/components/widgets/EditWidgetFrame.tsx | 15 +++++++-------- .../src/views/components/widgets/Widget.tsx | 13 ++++++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx index 733a9d4e228f..7779bc227b82 100644 --- a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx +++ b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx @@ -23,8 +23,7 @@ import WidgetContext from 'views/components/contexts/WidgetContext'; import QueryEditModeContext from 'views/components/contexts/QueryEditModeContext'; import SaveOrCancelButtons from 'views/components/widgets/SaveOrCancelButtons'; import WidgetEditApplyAllChangesProvider from 'views/components/contexts/WidgetEditApplyAllChangesProvider'; -import useViewType from 'views/hooks/useViewType'; -import View from 'views/logic/views/View'; +import type Widget from 'views/logic/widgets/Widget'; import WidgetQueryControls from '../WidgetQueryControls'; import WidgetOverrideElements from '../WidgetOverrideElements'; @@ -50,25 +49,25 @@ const Visualization = styled.div` type Props = { children: React.ReactNode, + displaySubmitActions?: boolean, onCancel: () => void, onSubmit: () => void, - displaySubmitActions?: boolean, + showQueryControls?: boolean, + onChange: (widgetId: string, newWidget: Widget) => Promise, }; -const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = true }: Props) => { +const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = true, showQueryControls = true, onChange }: Props) => { const widget = useContext(WidgetContext); - const viewType = useViewType(); - const isDashboard = viewType === View.Type.Dashboard; if (!widget) { return ; } return ( - + - {(isDashboard && !widget.returnsAllRecords) && ( + {(showQueryControls && !widget.returnsAllRecords) && ( diff --git a/graylog2-web-interface/src/views/components/widgets/Widget.tsx b/graylog2-web-interface/src/views/components/widgets/Widget.tsx index cba9ce0c06c4..40b28929a97c 100644 --- a/graylog2-web-interface/src/views/components/widgets/Widget.tsx +++ b/graylog2-web-interface/src/views/components/widgets/Widget.tsx @@ -152,13 +152,14 @@ type EditWrapperProps = { editing: boolean, fields: FieldTypeMappingsList, id: string, - onToggleEdit: () => void, onCancelEdit: () => void, + onToggleEdit: () => void, onWidgetConfigChange: (newWidgetConfig: WidgetConfig) => void, + showQueryControls?: boolean, type: string, }; -const EditWrapper = ({ +export const EditWrapper = ({ children, config, editing, @@ -168,12 +169,17 @@ const EditWrapper = ({ onCancelEdit, onWidgetConfigChange, type, + showQueryControls, }: EditWrapperProps) => { const EditComponent = useMemo(() => _editComponentForType(type), [type]); const hasOwnSubmitButton = _hasOwnEditSubmitButton(type); return editing ? ( - + Date: Fri, 1 Nov 2024 15:14:05 +0100 Subject: [PATCH 2/8] Simplify `WidgetEditApplyAllChangesProvider`. --- .../WidgetEditApplyAllChangesProvider.tsx | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx b/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx index 69c6e63abda0..bdade00ff8e1 100644 --- a/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx +++ b/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx @@ -22,30 +22,37 @@ import UserNotification from 'util/UserNotification'; import DisableSubmissionStateContext from 'views/components/contexts/DisableSubmissionStateContext'; import useAppDispatch from 'stores/useAppDispatch'; import { updateWidget } from 'views/logic/slices/widgetActions'; +import type WidgetConfig from 'views/logic/widgets/WidgetConfig'; import WidgetEditApplyAllChangesContext from './WidgetEditApplyAllChangesContext'; +const useBindApplyChanges = () => { + const applyChangesRef = useRef(null); + + const bindApplyChanges = useCallback((updateWidgetConfig) => { + applyChangesRef.current = updateWidgetConfig; + }, []); + + return { applyChangesRef, bindApplyChanges }; +}; + type Props = { widget: Widget, children: React.ReactNode, } -const WidgetEditApplyAllChangesProvider = ({ children, widget }: Props) => { +const useApplyAllWidgetChanges = ( + widget: Widget, + applySearchControlsChanges: React.RefObject<(widget: Widget) => Widget>, + applyElementConfigurationChanges: React.RefObject<(widgetConfig: WidgetConfig) => WidgetConfig>, +) => { const { setDisabled } = useContext(DisableSubmissionStateContext); - const setDisableWidgetEditSubmit = useCallback((disabled: boolean) => setDisabled('widget-edit-apply-all-changes', disabled), [setDisabled]); - const applySearchControlsChanges = useRef(null); - const applyElementConfigurationChanges = useRef(null); const dispatch = useAppDispatch(); + const setDisableWidgetEditSubmit = useCallback( + (disabled: boolean) => setDisabled('widget-edit-apply-all-changes', disabled), + [setDisabled]); - const bindApplySearchControlsChanges = useCallback((updateWidgetConfig) => { - applySearchControlsChanges.current = updateWidgetConfig; - }, []); - - const bindApplyElementConfigurationChanges = useCallback((_updateWidget) => { - applyElementConfigurationChanges.current = _updateWidget; - }, []); - - const applyAllWidgetChanges = useCallback(() => { + return useCallback(() => { let newWidget = widget; let hasChanges = false; @@ -79,7 +86,13 @@ const WidgetEditApplyAllChangesProvider = ({ children, widget }: Props) => { } return Promise.resolve(); - }, [widget, setDisableWidgetEditSubmit, dispatch]); + }, [widget, applySearchControlsChanges, applyElementConfigurationChanges, setDisableWidgetEditSubmit, dispatch]); +}; + +const WidgetEditApplyAllChangesProvider = ({ children, widget }: Props) => { + const { applyChangesRef: applySearchControlsChanges, bindApplyChanges: bindApplySearchControlsChanges } = useBindApplyChanges(); + const { applyChangesRef: applyElementConfigurationChanges, bindApplyChanges: bindApplyElementConfigurationChanges } = useBindApplyChanges(); + const applyAllWidgetChanges = useApplyAllWidgetChanges(widget, applySearchControlsChanges, applyElementConfigurationChanges); const contextValue = useMemo(() => ({ applyAllWidgetChanges, From 8a642cb28b98036ed6d4bddce889c3086dbf16cb Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Mon, 25 Nov 2024 11:49:01 +0100 Subject: [PATCH 3/8] Improve reusability of field select component. --- .../aggregationwizard/FieldSelect.tsx | 164 +----------------- .../aggregationwizard/FieldSelectBase.tsx | 163 +++++++++++++++++ .../components/widgets/EditWidgetFrame.tsx | 2 +- .../widgets/FieldsConfiguration.tsx | 34 ++-- .../src/views/components/widgets/Widget.tsx | 8 +- 5 files changed, 198 insertions(+), 173 deletions(-) create mode 100644 graylog2-web-interface/src/views/components/aggregationwizard/FieldSelectBase.tsx diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx index d1ec754ff256..17b6c7f7e476 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx @@ -1,167 +1,21 @@ -/* - * 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 type { SyntheticEvent } from 'react'; -import { useCallback, useContext, useMemo } from 'react'; -import * as Immutable from 'immutable'; -import styled, { css } from 'styled-components'; +import { useContext } from 'react'; +import Immutable from 'immutable'; -import { defaultCompare } from 'logic/DefaultCompare'; -import FieldTypesContext from 'views/components/contexts/FieldTypesContext'; -import Select from 'components/common/Select'; -import type FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping'; -import FieldTypeIcon from 'views/components/sidebar/fields/FieldTypeIcon'; -import type FieldType from 'views/logic/fieldtypes/FieldType'; +import FieldSelectBase from 'views/components/aggregationwizard/FieldSelectBase'; import useActiveQueryId from 'views/hooks/useActiveQueryId'; -import type { SelectRef } from 'components/common/Select/Select'; -import { Button } from 'components/bootstrap'; - -const FieldName = styled.span` - display: inline-flex; - gap: 2px; - align-items: center; -`; - -const ButtonRow = styled.div` - margin-top: 5px; - display: inline-flex; - gap: 5px; - margin-bottom: 10px; -`; - -type Props = { - ariaLabel?: string, - autoFocus?: boolean, - allowCreate?: boolean, - className?: string, - clearable?: boolean, - excludedFields?: Array, - id: string, - isFieldQualified?: (field: FieldTypeMapping) => boolean, - menuPortalTarget?: HTMLElement, - name: string, - onChange: (fieldName: string) => void, - onMenuClose?: () => void, - openMenuOnFocus?: boolean, - persistSelection?: boolean, - placeholder?: string, - selectRef?: SelectRef, - size?: 'normal' | 'small', - value: string | undefined, - onSelectAllRest?: (fieldNames: Array) => void, - showSelectAllRest?: boolean, - onDeSelectAll?: (e: SyntheticEvent) => void, - showDeSelectAll?: boolean, -} - -const sortByLabel = ({ label: label1 }: { label: string }, { label: label2 }: { label: string }) => defaultCompare(label1, label2); - -const UnqualifiedOption = styled.span(({ theme }) => css` - color: ${theme.colors.gray[70]}; -`); - -type OptionRendererProps = { - label: string, - qualified: boolean, - type?: FieldType, -}; - -const OptionRenderer = ({ label, qualified, type }: OptionRendererProps) => { - const children = ( - - {type && <> }{label} - - ); +import FieldTypesContext from 'views/components/contexts/FieldTypesContext'; - return qualified ? {children} : {children}; -}; +type Props = Omit, 'options'> -const FieldSelect = ({ - ariaLabel, - autoFocus, - allowCreate = false, - className, - clearable = false, - excludedFields = [], - id, - isFieldQualified = () => true, - menuPortalTarget, - name, - onChange, - onMenuClose, - openMenuOnFocus, - persistSelection, - placeholder, - selectRef, - size = 'small', - value, - onSelectAllRest, - showSelectAllRest = false, - onDeSelectAll, - showDeSelectAll = false, -}: Props) => { +const FieldSelect = (props: Props) => { const activeQuery = useActiveQueryId(); const fieldTypes = useContext(FieldTypesContext); - const fieldOptions = useMemo(() => fieldTypes.queryFields - .get(activeQuery, Immutable.List()) - .filter((field) => !excludedFields.includes(field.name)) - .map((field) => ({ - label: field.name, - value: field.name, - type: field.type, - qualified: isFieldQualified(field), - })) - .toArray() - .sort(sortByLabel), [activeQuery, excludedFields, fieldTypes.queryFields, isFieldQualified]); - - const _onSelectAllRest = useCallback(() => onSelectAllRest(fieldOptions.map(({ value: fieldValue }) => fieldValue)), [fieldOptions, onSelectAllRest]); - - const _showSelectAllRest = !!fieldOptions?.length && showSelectAllRest && typeof _onSelectAllRest === 'function'; - - const _showDeSelectAll = showDeSelectAll && typeof onDeSelectAll === 'function'; + const fieldOptions = fieldTypes.queryFields.get(activeQuery, Immutable.List()).toArray(); return ( - <> - + {(_showSelectAllRest || _showDeSelectAll) && ( + + {_showSelectAllRest && } + {_showDeSelectAll && } + + )} + + + ); +}; + +export default FieldSelect; diff --git a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx index 7779bc227b82..635b42c50d5a 100644 --- a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx +++ b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx @@ -64,7 +64,7 @@ const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = } return ( - + {(showQueryControls && !widget.returnsAllRecords) && ( diff --git a/graylog2-web-interface/src/views/components/widgets/FieldsConfiguration.tsx b/graylog2-web-interface/src/views/components/widgets/FieldsConfiguration.tsx index 36e06f56401d..29f80d19f86e 100644 --- a/graylog2-web-interface/src/views/components/widgets/FieldsConfiguration.tsx +++ b/graylog2-web-interface/src/views/components/widgets/FieldsConfiguration.tsx @@ -50,6 +50,7 @@ type Props = { showDeSelectAll?: boolean, showListCollapseButton?: boolean showUnit?: boolean, + fieldSelect?: React.ComponentType> } const FieldsConfiguration = ({ @@ -65,6 +66,7 @@ const FieldsConfiguration = ({ showDeSelectAll = false, showListCollapseButton = false, showUnit = false, + fieldSelect: FieldSelectComponent = FieldSelect, }: Props) => { const [showSelectedList, setShowSelectedList] = useState(true); const onAddField = useCallback((newField: string) => ( @@ -106,22 +108,22 @@ const FieldsConfiguration = ({ onChange={onChange} showUnit={showUnit} /> )} - + ); }; diff --git a/graylog2-web-interface/src/views/components/widgets/Widget.tsx b/graylog2-web-interface/src/views/components/widgets/Widget.tsx index 40b28929a97c..0fd2d2ae6927 100644 --- a/graylog2-web-interface/src/views/components/widgets/Widget.tsx +++ b/graylog2-web-interface/src/views/components/widgets/Widget.tsx @@ -45,6 +45,7 @@ import useViewType from 'views/hooks/useViewType'; import View from 'views/logic/views/View'; import IfDashboard from 'views/components/dashboard/IfDashboard'; import FullSizeContainer from 'views/components/aggregationbuilder/FullSizeContainer'; +import type WidgetType from 'views/logic/widgets/Widget'; import WidgetFrame from './WidgetFrame'; import WidgetHeader from './WidgetHeader'; @@ -173,11 +174,16 @@ export const EditWrapper = ({ }: EditWrapperProps) => { const EditComponent = useMemo(() => _editComponentForType(type), [type]); const hasOwnSubmitButton = _hasOwnEditSubmitButton(type); + const _onWidgetConfigChange = useCallback((_widgetId: string, widget: WidgetType) => { + onWidgetConfigChange(widget.config); + + return Promise.resolve(); + }, [onWidgetConfigChange]); return editing ? ( Date: Mon, 25 Nov 2024 11:49:32 +0100 Subject: [PATCH 4/8] Decouple `EditWidgetFrame` from redux state. --- .../WidgetEditApplyAllChangesProvider.tsx | 23 +++++++++---------- .../widgets/EditWidgetFrame.test.tsx | 2 +- .../components/widgets/EditWidgetFrame.tsx | 11 +++++---- .../src/views/components/widgets/Widget.tsx | 11 ++++----- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx b/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx index bdade00ff8e1..09548c0f58ae 100644 --- a/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx +++ b/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx @@ -20,8 +20,6 @@ import { useContext, useRef, useCallback, useMemo } from 'react'; import type Widget from 'views/logic/widgets/Widget'; import UserNotification from 'util/UserNotification'; import DisableSubmissionStateContext from 'views/components/contexts/DisableSubmissionStateContext'; -import useAppDispatch from 'stores/useAppDispatch'; -import { updateWidget } from 'views/logic/slices/widgetActions'; import type WidgetConfig from 'views/logic/widgets/WidgetConfig'; import WidgetEditApplyAllChangesContext from './WidgetEditApplyAllChangesContext'; @@ -36,18 +34,13 @@ const useBindApplyChanges = () => { return { applyChangesRef, bindApplyChanges }; }; -type Props = { - widget: Widget, - children: React.ReactNode, -} - const useApplyAllWidgetChanges = ( widget: Widget, applySearchControlsChanges: React.RefObject<(widget: Widget) => Widget>, applyElementConfigurationChanges: React.RefObject<(widgetConfig: WidgetConfig) => WidgetConfig>, + onChange: (newWidget: Widget) => Promise, ) => { const { setDisabled } = useContext(DisableSubmissionStateContext); - const dispatch = useAppDispatch(); const setDisableWidgetEditSubmit = useCallback( (disabled: boolean) => setDisabled('widget-edit-apply-all-changes', disabled), [setDisabled]); @@ -77,7 +70,7 @@ const useApplyAllWidgetChanges = ( if (hasChanges) { setDisableWidgetEditSubmit(true); - return dispatch(updateWidget(widget.id, newWidget)) + return onChange(newWidget) .catch((error) => { UserNotification.error(`Applying widget changes failed with status: ${error}`); @@ -86,13 +79,19 @@ const useApplyAllWidgetChanges = ( } return Promise.resolve(); - }, [widget, applySearchControlsChanges, applyElementConfigurationChanges, setDisableWidgetEditSubmit, dispatch]); + }, [widget, applySearchControlsChanges, applyElementConfigurationChanges, setDisableWidgetEditSubmit, onChange]); }; -const WidgetEditApplyAllChangesProvider = ({ children, widget }: Props) => { +type Props = { + widget: Widget, + children: React.ReactNode, + onChange: (newWidget: Widget) => Promise, +} + +const WidgetEditApplyAllChangesProvider = ({ children, widget, onChange }: Props) => { const { applyChangesRef: applySearchControlsChanges, bindApplyChanges: bindApplySearchControlsChanges } = useBindApplyChanges(); const { applyChangesRef: applyElementConfigurationChanges, bindApplyChanges: bindApplyElementConfigurationChanges } = useBindApplyChanges(); - const applyAllWidgetChanges = useApplyAllWidgetChanges(widget, applySearchControlsChanges, applyElementConfigurationChanges); + const applyAllWidgetChanges = useApplyAllWidgetChanges(widget, applySearchControlsChanges, applyElementConfigurationChanges, onChange); const contextValue = useMemo(() => ({ applyAllWidgetChanges, diff --git a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx index 2b88aef4f6d8..f46a7777f609 100644 --- a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx +++ b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx @@ -70,7 +70,7 @@ describe('EditWidgetFrame', () => { const renderSUT = (props?: Partial>) => render(( - {}} onCancel={() => {}} {...props}> + {}} onCancel={() => {}} onChange={() => Promise.resolve()} {...props}> Hello World! These are some buttons! diff --git a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx index 635b42c50d5a..9ce11878c2b0 100644 --- a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx +++ b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx @@ -53,10 +53,11 @@ type Props = { onCancel: () => void, onSubmit: () => void, showQueryControls?: boolean, - onChange: (widgetId: string, newWidget: Widget) => Promise, + onChange: (newWidget: Widget) => Promise, + containerComponent?: React.ComponentType }; -const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = true, showQueryControls = true, onChange }: Props) => { +const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = true, showQueryControls = true, onChange, containerComponent: ContainerComponent = WidgetOverrideElements }: Props) => { const widget = useContext(WidgetContext); if (!widget) { @@ -64,7 +65,7 @@ const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = } return ( - + {(showQueryControls && !widget.returnsAllRecords) && ( @@ -75,9 +76,9 @@ const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = )} - + {children} - + {displaySubmitActions && (
diff --git a/graylog2-web-interface/src/views/components/widgets/Widget.tsx b/graylog2-web-interface/src/views/components/widgets/Widget.tsx index 0fd2d2ae6927..4789326f071b 100644 --- a/graylog2-web-interface/src/views/components/widgets/Widget.tsx +++ b/graylog2-web-interface/src/views/components/widgets/Widget.tsx @@ -174,16 +174,15 @@ export const EditWrapper = ({ }: EditWrapperProps) => { const EditComponent = useMemo(() => _editComponentForType(type), [type]); const hasOwnSubmitButton = _hasOwnEditSubmitButton(type); - const _onWidgetConfigChange = useCallback((_widgetId: string, widget: WidgetType) => { - onWidgetConfigChange(widget.config); - - return Promise.resolve(); - }, [onWidgetConfigChange]); + const dispatch = useAppDispatch(); + const onChangeWidget = useCallback((newWidget: WidgetType) => ( + dispatch(updateWidget(newWidget.id, newWidget)).then(() => {}) + ), [dispatch]); return editing ? ( Date: Tue, 26 Nov 2024 14:13:11 +0100 Subject: [PATCH 5/8] Simplify widget edit submit logic by removing `onSubmit` prop from widget edit components. Instead we only call `applyAllWidgetChanges` from `WidgetEditApplyAllChangesContext. The `WidgetEditApplyAllChangesProvider` now calls a `onSubmit` prop which takes care of updateing the widget. --- .../AggregationWizard.test.tsx | 9 ------ .../aggregationwizard/AggregationWizard.tsx | 3 +- .../ElementsConfiguration.tsx | 5 ++-- .../aggregationwizard/FieldSelect.tsx | 16 +++++++++++ ...regationWizard.corevisualizations.test.tsx | 1 - .../AggregationWizard.groupBy.test.tsx | 1 - .../AggregationWizard.metric.test.tsx | 1 - .../__tests__/AggregationWizard.sort.test.tsx | 1 - .../WidgetEditApplyAllChangesProvider.tsx | 28 ++++++++----------- .../components/widgets/EditMessageList.tsx | 4 +-- .../widgets/EditWidgetFrame.test.tsx | 4 +-- .../components/widgets/EditWidgetFrame.tsx | 9 +++--- .../widgets/SaveOrCancelButtons.tsx | 4 +-- .../src/views/components/widgets/Widget.tsx | 16 +++++++---- .../widgets/events/EventsWidgetEdit.tsx | 4 +-- graylog2-web-interface/src/views/types.ts | 1 - 16 files changed, 52 insertions(+), 55 deletions(-) diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.test.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.test.tsx index 414ffea9846e..f36fc2c524e9 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.test.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.test.tsx @@ -42,7 +42,6 @@ describe('AggregationWizard', () => { {}} - onSubmit={() => {}} onCancel={() => {}} config={widgetConfig} editing @@ -98,14 +97,6 @@ describe('AggregationWizard', () => { await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument()); }); - it('should call onSubmit', async () => { - const onSubmit = jest.fn(); - renderSUT({ onSubmit }); - userEvent.click(await screen.findByRole('button', { name: /update widget/i })); - - await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - }); - it('should call onCancel', async () => { const onCancel = jest.fn(); renderSUT({ onCancel }); diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.tsx index e19f72b4526e..74f0692cb3ad 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/AggregationWizard.tsx @@ -100,7 +100,7 @@ const validateForm = (formValues: WidgetConfigFormValues) => { return elementValidationResults.reduce((prev, cur) => ({ ...prev, ...cur }), {}); }; -const AggregationWizard = ({ onChange, config, children, onSubmit, onCancel }: EditWidgetComponentProps & { children: React.ReactElement }) => { +const AggregationWizard = ({ onChange, config, children, onCancel }: EditWidgetComponentProps & { children: React.ReactElement }) => { const initialFormValues = _initialFormValues(config); return ( @@ -114,7 +114,6 @@ const AggregationWizard = ({ onChange, config, children, onSubmit, onCancel }: E diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/ElementsConfiguration.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/ElementsConfiguration.tsx index 6b9054f00a84..868508906462 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/ElementsConfiguration.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/ElementsConfiguration.tsx @@ -52,11 +52,10 @@ type Props = { values: WidgetConfigFormValues, setValues: (formValues: WidgetConfigFormValues) => void, ) => void, - onSubmit: () => void, onCancel: () => void, } -const ElementsConfiguration = ({ aggregationElementsByKey, config, onConfigChange, onCreate, onSubmit, onCancel }: Props) => { +const ElementsConfiguration = ({ aggregationElementsByKey, config, onConfigChange, onCreate, onCancel }: Props) => { const { values, setValues } = useFormikContext(); return ( @@ -64,7 +63,7 @@ const ElementsConfiguration = ({ aggregationElementsByKey, config, onConfigChang - + )}>
diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx index 17b6c7f7e476..17feb94ad391 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/FieldSelect.tsx @@ -1,3 +1,19 @@ +/* + * 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 { useContext } from 'react'; import Immutable from 'immutable'; diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.corevisualizations.test.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.corevisualizations.test.tsx index 41a83a343aa9..72190d5c0729 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.corevisualizations.test.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.corevisualizations.test.tsx @@ -61,7 +61,6 @@ const SimpleAggregationWizard = (props: Partial {}} onChange={() => {}} - onSubmit={() => {}} {...props}> The visualization diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.groupBy.test.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.groupBy.test.tsx index ccaf983443bc..1a01bb9ed1f6 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.groupBy.test.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.groupBy.test.tsx @@ -94,7 +94,6 @@ describe('AggregationWizard', () => { editing id="widget-id" type="AGGREGATION" - onSubmit={() => {}} onCancel={() => {}} fields={Immutable.List([])} onChange={() => {}} diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.metric.test.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.metric.test.tsx index ede18c0b0761..1b5ef6d9b43c 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.metric.test.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.metric.test.tsx @@ -93,7 +93,6 @@ describe('AggregationWizard', () => { const renderSUT = (props = {}) => render( {}} - onSubmit={() => {}} onCancel={() => {}} config={widgetConfig} editing diff --git a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.sort.test.tsx b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.sort.test.tsx index e02f0c36f7a8..ff55e34e9d8c 100644 --- a/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.sort.test.tsx +++ b/graylog2-web-interface/src/views/components/aggregationwizard/__tests__/AggregationWizard.sort.test.tsx @@ -94,7 +94,6 @@ const renderSUT = (props = {}) => render(( {}} - onSubmit={() => {}} onCancel={() => {}} config={widgetConfig} editing diff --git a/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx b/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx index 09548c0f58ae..883a91525393 100644 --- a/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx +++ b/graylog2-web-interface/src/views/components/contexts/WidgetEditApplyAllChangesProvider.tsx @@ -38,7 +38,7 @@ const useApplyAllWidgetChanges = ( widget: Widget, applySearchControlsChanges: React.RefObject<(widget: Widget) => Widget>, applyElementConfigurationChanges: React.RefObject<(widgetConfig: WidgetConfig) => WidgetConfig>, - onChange: (newWidget: Widget) => Promise, + onSubmit: (newWidget: Widget, hasChanges: boolean) => Promise, ) => { const { setDisabled } = useContext(DisableSubmissionStateContext); const setDisableWidgetEditSubmit = useCallback( @@ -67,31 +67,27 @@ const useApplyAllWidgetChanges = ( } } - if (hasChanges) { - setDisableWidgetEditSubmit(true); + setDisableWidgetEditSubmit(true); - return onChange(newWidget) - .catch((error) => { - UserNotification.error(`Applying widget changes failed with status: ${error}`); + return onSubmit(newWidget, hasChanges) + .catch((error) => { + UserNotification.error(`Applying widget changes failed with status: ${error}`); - return error; - }).finally(() => setDisableWidgetEditSubmit(false)); - } - - return Promise.resolve(); - }, [widget, applySearchControlsChanges, applyElementConfigurationChanges, setDisableWidgetEditSubmit, onChange]); + return error; + }).finally(() => setDisableWidgetEditSubmit(false)); + }, [widget, applySearchControlsChanges, applyElementConfigurationChanges, setDisableWidgetEditSubmit, onSubmit]); }; type Props = { - widget: Widget, children: React.ReactNode, - onChange: (newWidget: Widget) => Promise, + onSubmit: (newWidget: Widget, hasChanges: boolean) => Promise, + widget: Widget, } -const WidgetEditApplyAllChangesProvider = ({ children, widget, onChange }: Props) => { +const WidgetEditApplyAllChangesProvider = ({ children, widget, onSubmit }: Props) => { const { applyChangesRef: applySearchControlsChanges, bindApplyChanges: bindApplySearchControlsChanges } = useBindApplyChanges(); const { applyChangesRef: applyElementConfigurationChanges, bindApplyChanges: bindApplyElementConfigurationChanges } = useBindApplyChanges(); - const applyAllWidgetChanges = useApplyAllWidgetChanges(widget, applySearchControlsChanges, applyElementConfigurationChanges, onChange); + const applyAllWidgetChanges = useApplyAllWidgetChanges(widget, applySearchControlsChanges, applyElementConfigurationChanges, onSubmit); const contextValue = useMemo(() => ({ applyAllWidgetChanges, diff --git a/graylog2-web-interface/src/views/components/widgets/EditMessageList.tsx b/graylog2-web-interface/src/views/components/widgets/EditMessageList.tsx index 132af20b94e7..ca0d0749d50b 100644 --- a/graylog2-web-interface/src/views/components/widgets/EditMessageList.tsx +++ b/graylog2-web-interface/src/views/components/widgets/EditMessageList.tsx @@ -69,7 +69,7 @@ const _onSortDirectionChange = (direction: SortConfig['direction'], config, onCh return onChange(newConfig); }; -const EditMessageList = ({ children, config, fields, onChange, onCancel, onSubmit }: EditWidgetComponentProps) => { +const EditMessageList = ({ children, config, fields, onChange, onCancel }: EditWidgetComponentProps) => { const { sort } = config; const [sortDirection] = (sort || []).map((s) => s.direction); const onDecoratorsChange = (newDecorators) => onChange(config.toBuilder().decorators(newDecorators).build()); @@ -79,7 +79,7 @@ const EditMessageList = ({ children, config, fields, onChange, onCancel, onSubmi return ( - } + } alignActionsAtBottom> _onFieldSelectionChanged(newFields, config, onChange)} diff --git a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx index f46a7777f609..33d4f7a3b239 100644 --- a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx +++ b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.test.tsx @@ -70,7 +70,7 @@ describe('EditWidgetFrame', () => { const renderSUT = (props?: Partial>) => render(( - {}} onCancel={() => {}} onChange={() => Promise.resolve()} {...props}> + {}} onSubmit={() => Promise.resolve()} {...props}> Hello World! These are some buttons! @@ -114,7 +114,7 @@ describe('EditWidgetFrame', () => { }); it('calls onSubmit', async () => { - const onSubmit = jest.fn(); + const onSubmit = jest.fn(() => Promise.resolve()); renderSUT({ onSubmit }); fireEvent.click(await screen.findByRole('button', { name: /update widget/i })); diff --git a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx index 9ce11878c2b0..03365fd9903a 100644 --- a/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx +++ b/graylog2-web-interface/src/views/components/widgets/EditWidgetFrame.tsx @@ -51,13 +51,12 @@ type Props = { children: React.ReactNode, displaySubmitActions?: boolean, onCancel: () => void, - onSubmit: () => void, showQueryControls?: boolean, - onChange: (newWidget: Widget) => Promise, + onSubmit: (newWidget: Widget, hasChanges: boolean) => Promise, containerComponent?: React.ComponentType }; -const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = true, showQueryControls = true, onChange, containerComponent: ContainerComponent = WidgetOverrideElements }: Props) => { +const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = true, showQueryControls = true, containerComponent: ContainerComponent = WidgetOverrideElements }: Props) => { const widget = useContext(WidgetContext); if (!widget) { @@ -65,7 +64,7 @@ const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = } return ( - + {(showQueryControls && !widget.returnsAllRecords) && ( @@ -82,7 +81,7 @@ const EditWidgetFrame = ({ children, onCancel, onSubmit, displaySubmitActions = {displaySubmitActions && (
- +
)}
diff --git a/graylog2-web-interface/src/views/components/widgets/SaveOrCancelButtons.tsx b/graylog2-web-interface/src/views/components/widgets/SaveOrCancelButtons.tsx index 4cbce60d48cb..35a7449b3b33 100644 --- a/graylog2-web-interface/src/views/components/widgets/SaveOrCancelButtons.tsx +++ b/graylog2-web-interface/src/views/components/widgets/SaveOrCancelButtons.tsx @@ -25,10 +25,9 @@ export const UPDATE_WIDGET_BTN_TEXT = 'Update widget'; type Props = { onCancel: () => void, - onSubmit: () => void, }; -const SaveOrCancelButtons = ({ onSubmit, onCancel }: Props) => { +const SaveOrCancelButtons = ({ onCancel }: Props) => { const { applyAllWidgetChanges } = useContext(WidgetEditApplyAllChangesContext); const [isSubmitting, setIsSubmitting] = useState(false); const { disabled: disabledSubmit } = useContext(DisableSubmissionStateContext); @@ -38,7 +37,6 @@ const SaveOrCancelButtons = ({ onSubmit, onCancel }: Props) => { return applyAllWidgetChanges().then(() => { setIsSubmitting(false); - onSubmit(); }).catch(() => { setIsSubmitting(false); }); diff --git a/graylog2-web-interface/src/views/components/widgets/Widget.tsx b/graylog2-web-interface/src/views/components/widgets/Widget.tsx index 4789326f071b..f4b5c0527872 100644 --- a/graylog2-web-interface/src/views/components/widgets/Widget.tsx +++ b/graylog2-web-interface/src/views/components/widgets/Widget.tsx @@ -175,14 +175,19 @@ export const EditWrapper = ({ const EditComponent = useMemo(() => _editComponentForType(type), [type]); const hasOwnSubmitButton = _hasOwnEditSubmitButton(type); const dispatch = useAppDispatch(); - const onChangeWidget = useCallback((newWidget: WidgetType) => ( - dispatch(updateWidget(newWidget.id, newWidget)).then(() => {}) - ), [dispatch]); + const onSubmitEdit = useCallback((newWidget: WidgetType, hasChanges: boolean) => { + if (hasChanges) { + return dispatch(updateWidget(newWidget.id, newWidget)).then(() => onToggleEdit()); + } + + onToggleEdit(); + + return Promise.resolve(); + }, [dispatch, onToggleEdit]); return editing ? ( - {children} diff --git a/graylog2-web-interface/src/views/components/widgets/events/EventsWidgetEdit.tsx b/graylog2-web-interface/src/views/components/widgets/events/EventsWidgetEdit.tsx index f0dc5b0c2c32..0969e0fed52d 100644 --- a/graylog2-web-interface/src/views/components/widgets/events/EventsWidgetEdit.tsx +++ b/graylog2-web-interface/src/views/components/widgets/events/EventsWidgetEdit.tsx @@ -107,7 +107,7 @@ const SubmitOnChange = () => { return <>; }; -const EventsWidgetEdit = ({ children, onCancel, config, onChange, onSubmit }: EditWidgetComponentProps) => { +const EventsWidgetEdit = ({ children, onCancel, config, onChange }: EditWidgetComponentProps) => { const filterComponents = usePluginEntities('views.components.widgets.events.filterComponents'); const eventAttributes = useEventAttributes(); @@ -172,7 +172,7 @@ const EventsWidgetEdit = ({ children, onCancel, config, onChange, onSubmit }: Ed - } + } alignActionsAtBottom> diff --git a/graylog2-web-interface/src/views/types.ts b/graylog2-web-interface/src/views/types.ts index 04ad375cab93..16597b526615 100644 --- a/graylog2-web-interface/src/views/types.ts +++ b/graylog2-web-interface/src/views/types.ts @@ -83,7 +83,6 @@ export interface EditWidgetComponentProps, onChange: (newConfig: Config) => void, - onSubmit: () => void, onCancel: () => void, } From 51bee8e0391e8bb1a19f27b313fbf67085251af6 Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Tue, 26 Nov 2024 16:09:24 +0100 Subject: [PATCH 6/8] Add note for `UPGRADING` doc. --- UPGRADING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/UPGRADING.md b/UPGRADING.md index f353023e4f4a..58461c0f296d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,7 +3,11 @@ Upgrading to Graylog 6.2.x ## Breaking Changes -- tbd +### Plugins + +Adjustment of `enterpriseWidgets` web interface plugin. The `editComponent` attribute now no longer has a `onSubmit` prop. +Before this change the prop had to be called to close the widget edit mode. Now it is enough to call `applyAllWidgetChanges` from the `WidgetEditApplyAllChangesContext`. +Alternatively the `SaveOrCancelButtons` component can be used in the edit component for custom widgets. It renders a cancel and submit button and calls `applyAllWidgetChanges` on submit. ## Configuration File Changes From 1fbde015f0ac3eee5d109ea836cc41e8829a8b1c Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Wed, 27 Nov 2024 10:29:46 +0100 Subject: [PATCH 7/8] Allows readonly widget title. --- .../views/components/common/EditableTitle.tsx | 4 +- .../views/components/widgets/WidgetHeader.tsx | 61 ++++++++++++------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/graylog2-web-interface/src/views/components/common/EditableTitle.tsx b/graylog2-web-interface/src/views/components/common/EditableTitle.tsx index 0b5e81248a4d..f13707b93ba3 100644 --- a/graylog2-web-interface/src/views/components/common/EditableTitle.tsx +++ b/graylog2-web-interface/src/views/components/common/EditableTitle.tsx @@ -19,7 +19,7 @@ import styled, { css } from 'styled-components'; import styles from './EditableTitle.css'; -const StyledStaticSpan = styled.span(({ theme }) => css` +export const Title = styled.span(({ theme }) => css` border: 1px solid ${theme.colors.global.contentBackground}; font-size: ${theme.fonts.size.large}; text-overflow: ellipsis; @@ -119,6 +119,6 @@ export default class EditableTitle extends React.Component { onChange={this._onChange} /> - ) : {value}; + ) : {value}; } } diff --git a/graylog2-web-interface/src/views/components/widgets/WidgetHeader.tsx b/graylog2-web-interface/src/views/components/widgets/WidgetHeader.tsx index d930348b6a8f..8d0da76e5d4a 100644 --- a/graylog2-web-interface/src/views/components/widgets/WidgetHeader.tsx +++ b/graylog2-web-interface/src/views/components/widgets/WidgetHeader.tsx @@ -18,7 +18,7 @@ import React from 'react'; import styled, { css } from 'styled-components'; import { Spinner, Icon } from 'components/common'; -import EditableTitle from 'views/components/common/EditableTitle'; +import EditableTitle, { Title } from 'views/components/common/EditableTitle'; import { Input } from 'components/bootstrap'; const LoadingSpinner = styled(Spinner)` @@ -75,6 +75,41 @@ const TitleInput = styled(Input)(({ theme }) => css` width: 100%; `); +type WidgetTitleProps = { + onChange?: (newTitle: string) => void, + editing: boolean, + title: string, + titleIcon?: React.ReactNode, +} + +const WidgetTitle = ({ onChange, editing, title, titleIcon }: WidgetTitleProps) => { + if (typeof onChange !== 'function') { + return <>{title}{titleIcon}; + } + + if (editing) { + return ( + + onChange(e.target.value)} + value={title} + required /> + + ); + } + + return ( + <> + + {titleIcon} + + ); +}; + type Props = { children?: React.ReactNode onRename?: (newTitle: string) => unknown @@ -90,30 +125,14 @@ const WidgetHeader = ({ editing, hideDragHandle = false, loading = false, - children = null, - titleIcon = null, - onRename = null, + children, + titleIcon, + onRename, }: Props) => ( {hideDragHandle || } - {editing ? ( - - onRename && onRename(e.target.value)} - value={title} - required /> - - ) : ( - <> - - {titleIcon} - - )} + {loading && } From a1b114d4fb263d6ea570210acf35cb588fd07b0c Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Sat, 30 Nov 2024 12:04:20 +0100 Subject: [PATCH 8/8] Unify names --- .../src/components/streams/StreamsOverview/StreamActions.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx b/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx index cbcc385c237f..34d2dcaa44a1 100644 --- a/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx +++ b/graylog2-web-interface/src/components/streams/StreamsOverview/StreamActions.tsx @@ -168,7 +168,8 @@ const StreamActions = ({ sendTelemetry(TELEMETRY_EVENT_TYPE.STREAMS.STREAM_ITEM_DATA_ROUTING_CLICKED, { app_pathname: 'stream', }); - }}>Data Routing + }}> + Data routing