From fe74704e12089255248f0b6c766576555e0e3f0b Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Thu, 9 Jan 2025 09:18:40 +0100 Subject: [PATCH 1/4] Improve reusability os search sidebar component. --- .../src/views/components/Search.tsx | 6 +++-- .../components/sidebar/ContentColumn.tsx | 25 +++++++++---------- .../src/views/components/sidebar/Sidebar.tsx | 16 ++++++------ .../components/sidebar/sidebarActions.tsx | 4 +-- .../components/sidebar/sidebarSections.tsx | 1 - 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/graylog2-web-interface/src/views/components/Search.tsx b/graylog2-web-interface/src/views/components/Search.tsx index 1a06ff00d2bb..707b42e1f28f 100644 --- a/graylog2-web-interface/src/views/components/Search.tsx +++ b/graylog2-web-interface/src/views/components/Search.tsx @@ -51,6 +51,7 @@ import { selectCurrentQueryResults } from 'views/logic/slices/viewSelectors'; import useAppSelector from 'stores/useAppSelector'; import useParameters from 'views/hooks/useParameters'; import useSearchConfiguration from 'hooks/useSearchConfiguration'; +import useViewTitle from 'views/hooks/useViewTitle'; import ExternalValueActionsProvider from './ExternalValueActionsProvider'; @@ -84,10 +85,11 @@ const SearchArea = styled(PageContentLayout)(() => { `; }); -const ConnectedSidebar = (props: Omit, 'results'>) => { +const ConnectedSidebar = (props: Omit, 'results' | 'title'>) => { const results = useAppSelector(selectCurrentQueryResults); + const title = useViewTitle(); - return ; + return ; }; const ViewAdditionalContextProvider = ({ children }: { children: React.ReactNode }) => { diff --git a/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx b/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx index 03674fb55028..d2c9e202c396 100644 --- a/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx @@ -19,9 +19,9 @@ import styled, { css } from 'styled-components'; import type { SearchPreferencesLayout } from 'views/components/contexts/SearchPagePreferencesContext'; import { IconButton } from 'components/common'; -import useViewTitle from 'views/hooks/useViewTitle'; type Props = { + title: string, children: React.ReactNode, closeSidebar: () => void, searchPageLayout: SearchPreferencesLayout | undefined | null, @@ -75,7 +75,7 @@ const Header = styled.div` grid-row: 1; `; -const SearchTitle = styled.div` +const TitleSection = styled.div` height: 35px; display: grid; grid-template-columns: 1fr auto; @@ -137,28 +137,27 @@ const toggleSidebarPinning = (searchPageLayout) => { togglePinning(); }; -const ContentColumn = ({ children, sectionTitle, closeSidebar, searchPageLayout, forceSideBarPinned }: Props) => { +const ContentColumn = ({ children, title, sectionTitle, closeSidebar, searchPageLayout, forceSideBarPinned }: Props) => { const sidebarIsPinned = searchPageLayout?.config.sidebar.isPinned || forceSideBarPinned; - const title = useViewTitle(); return (
- + {title} {!forceSideBarPinned && ( - - - toggleSidebarPinning(searchPageLayout)} - title={`Display sidebar ${sidebarIsPinned ? 'as overlay' : 'inline'}`} - name="keep" /> - - + + + toggleSidebarPinning(searchPageLayout)} + title={`Display sidebar ${sidebarIsPinned ? 'as overlay' : 'inline'}`} + name="keep" /> + + )} - + {sectionTitle}
diff --git a/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx b/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx index 955db6b8a45d..5d1c5cad8d4d 100644 --- a/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx @@ -22,7 +22,6 @@ import styled, { css } from 'styled-components'; import type QueryResult from 'views/logic/QueryResult'; import type { SearchPreferencesLayout } from 'views/components/contexts/SearchPagePreferencesContext'; import SearchPagePreferencesContext from 'views/components/contexts/SearchPagePreferencesContext'; -import useActiveQueryId from 'views/hooks/useActiveQueryId'; import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; import { getPathnameWithoutId } from 'util/URLUtils'; @@ -36,12 +35,13 @@ import type { SidebarAction } from './sidebarActions'; import sidebarActions from './sidebarActions'; type Props = { - children: React.ReactElement, + actions?: Array, + children?: React.ReactElement, + forceSideBarPinned?: boolean, results?: QueryResult searchPageLayout?: SearchPreferencesLayout, sections?: Array, - actions?: Array, - forceSideBarPinned?: boolean, + title: string, }; const Container = styled.div` @@ -77,10 +77,12 @@ const _selectSidebarSection = (sectionKey, activeSectionKey, setActiveSectionKey setActiveSectionKey(sectionKey); }; -const Sidebar = ({ searchPageLayout, results, children, sections = sidebarSections, actions = sidebarActions, forceSideBarPinned = false }: Props) => { +const Sidebar = ({ + searchPageLayout = undefined, results = undefined, children = undefined, title, + sections = sidebarSections, actions = sidebarActions, forceSideBarPinned = false, +}: Props) => { const sendTelemetry = useSendTelemetry(); const location = useLocation(); - const queryId = useActiveQueryId(); const sidebarIsPinned = searchPageLayout?.config.sidebar.isPinned || forceSideBarPinned; const initialSectionKey = sections[0].key; const [activeSectionKey, setActiveSectionKey] = useState(searchPageLayout?.config.sidebar.isPinned ? initialSectionKey : null); @@ -109,11 +111,11 @@ const Sidebar = ({ searchPageLayout, results, children, sections = sidebarSectio actions={actions} /> {activeSection && !!SectionContent && ( diff --git a/graylog2-web-interface/src/views/components/sidebar/sidebarActions.tsx b/graylog2-web-interface/src/views/components/sidebar/sidebarActions.tsx index 9c0ff2f73aa0..ca8896de04dc 100644 --- a/graylog2-web-interface/src/views/components/sidebar/sidebarActions.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/sidebarActions.tsx @@ -26,7 +26,7 @@ export type SidebarAction = { Component: React.ComponentType }; -const sidebarSections: Array = [ +const sidebarActions: Array = [ { key: 'undoAction', Component: UndoNavItem, @@ -37,4 +37,4 @@ const sidebarSections: Array = [ }, ]; -export default sidebarSections; +export default sidebarActions; diff --git a/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx b/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx index af9eaf978cd5..14bbc0d62f1d 100644 --- a/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx @@ -25,7 +25,6 @@ import HighlightingRules from './highlighting/HighlightingRules'; export type SidebarSectionProps = { sidebarChildren: React.ReactElement, sidebarIsPinned: boolean, - queryId: string, results: any, toggleSidebar: () => void }; From cd5fb55259f3224503e5309debfa3d899864328c Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Thu, 9 Jan 2025 14:23:37 +0100 Subject: [PATCH 2/4] Improve reusability of highlighting rules components. --- .../components/sidebar/SidebarNavigation.tsx | 16 +++-- .../sidebar/highlighting/HighlightForm.tsx | 26 ++++---- .../sidebar/highlighting/HighlightingRule.tsx | 58 +++++++++-------- .../highlighting/HighlightingRules.tsx | 64 +++++++++++++------ .../highlighting/ViewsHighlightingRules.tsx | 62 ++++++++++++++++++ .../components/sidebar/sidebarSections.tsx | 4 +- 6 files changed, 164 insertions(+), 66 deletions(-) create mode 100644 graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx diff --git a/graylog2-web-interface/src/views/components/sidebar/SidebarNavigation.tsx b/graylog2-web-interface/src/views/components/sidebar/SidebarNavigation.tsx index 2c8cb93fea93..70be51860d68 100644 --- a/graylog2-web-interface/src/views/components/sidebar/SidebarNavigation.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/SidebarNavigation.tsx @@ -120,12 +120,16 @@ const SidebarNavigation = ({ sections, activeSection, selectSidebarSection, side ); })} -
-
- {actions.map(({ key, Component }) => ( - - ))} -
+ {actions?.length > 0 && ( + <> +
+
+ {actions.map(({ key, Component }) => ( + + ))} +
+ + )}
); }; 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 31836bc94b32..5f8a9ea50523 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx @@ -25,7 +25,13 @@ 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'; -import HighlightingRule, { +import type { + Value, + Condition, + Color, +} from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import type HighlightingRule from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import { ConditionLabelMap, StringConditionLabelMap, } from 'views/logic/views/formatting/highlighting/HighlightingRule'; @@ -40,8 +46,6 @@ import { StaticColor, } from 'views/logic/views/formatting/highlighting/HighlightingColor'; import { ModalSubmit } from 'components/common'; -import useAppDispatch from 'stores/useAppDispatch'; -import { addHighlightingRule, updateHighlightingRule } from 'views/logic/slices/highlightActions'; import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; import { getPathnameWithoutId } from 'util/URLUtils'; @@ -50,9 +54,10 @@ import useLocation from 'routing/useLocation'; type Props = { onClose: () => void, rule?: HighlightingRule | null | undefined, + onSubmit: (field: string, value: Value, condition: Condition, color: Color) => Promise }; -const _isRequired = (field) => (value: string) => { +const _isRequired = (field: string) => (value: string) => { if (['', null, undefined].includes(value)) { return `${field} is required`; } @@ -107,7 +112,7 @@ const colorFromObject = (color: StaticColorObject | GradientColorObject) => { return undefined; }; -const HighlightForm = ({ onClose, rule }: Props) => { +const HighlightForm = ({ onClose, rule = undefined, onSubmit: onSubmitProp }: Props) => { const fieldTypes = useContext(FieldTypesContext); const sendTelemetry = useSendTelemetry(); const location = useLocation(); @@ -117,9 +122,8 @@ const HighlightForm = ({ onClose, rule }: Props) => { const fieldOptions = useMemo(() => fields.map(({ name }) => ({ value: name, label: name })) .sort((optA, optB) => defaultCompare(optA.label, optB.label)) .toArray(), [fields]); - const dispatch = useAppDispatch(); - const onSubmit = useCallback(({ field, value, color, condition }) => { + const onSubmit = useCallback(({ field, value, condition, color }: { field, value, condition, color }) => { const newColor = colorFromObject(color); sendTelemetry(TELEMETRY_EVENT_TYPE[`SEARCH_SIDEBAR_HIGHLIGHT_${rule ? 'UPDATED' : 'CREATED'}`], { @@ -127,12 +131,8 @@ const HighlightForm = ({ onClose, rule }: Props) => { app_action_value: 'search-sidebar-highlight', }); - if (rule) { - return dispatch(updateHighlightingRule(rule, { field, value, condition, color: newColor })).then(onClose); - } - - return dispatch(addHighlightingRule(HighlightingRule.create(field, value, condition, newColor))).then(onClose); - }, [dispatch, location.pathname, onClose, rule, sendTelemetry]); + return onSubmitProp(field, value, condition, newColor).then(onClose); + }, [location.pathname, onClose, onSubmitProp, rule, sendTelemetry]); const headerPrefix = rule ? 'Edit' : 'Create'; const submitButtonPrefix = rule ? 'Update' : 'Create'; diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx index 0c3cbf2c9db5..9e511785c4cd 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx @@ -19,14 +19,14 @@ import { forwardRef, useCallback, useState } from 'react'; import styled, { css } from 'styled-components'; import { DEFAULT_CUSTOM_HIGHLIGHT_RANGE } from 'views/Constants'; -import type Rule from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import type { Condition, Value, Color } from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import type HighlightingRuleClass from 'views/logic/views/formatting/highlighting/HighlightingRule'; import { ConditionLabelMap } from 'views/logic/views/formatting/highlighting/HighlightingRule'; import { ColorPickerPopover, Icon, IconButton } from 'components/common'; import HighlightForm from 'views/components/sidebar/highlighting/HighlightForm'; import type HighlightingColor from 'views/logic/views/formatting/highlighting/HighlightingColor'; import { StaticColor } from 'views/logic/views/formatting/highlighting/HighlightingColor'; import type { AppDispatch } from 'stores/useAppDispatch'; -import useAppDispatch from 'stores/useAppDispatch'; import { updateHighlightingRule, removeHighlightingRule } from 'views/logic/slices/highlightActions'; import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; @@ -69,17 +69,6 @@ const DragHandle = styled.div` width: 25px; `; -const updateColor = (rule: Rule, newColor: HighlightingColor, hidePopover: () => void) => async (dispatch: AppDispatch) => dispatch(updateHighlightingRule(rule, { color: newColor })).then(hidePopover); - -const onDelete = (rule: Rule) => async (dispatch: AppDispatch) => { - // eslint-disable-next-line no-alert - if (window.confirm('Do you really want to remove this highlighting?')) { - return dispatch(removeHighlightingRule(rule)); - } - - return Promise.resolve(); -}; - type RuleColorPreviewProps = { color: HighlightingColor, onChange: (newColor: HighlightingColor, hidePopover: () => void) => void, @@ -107,21 +96,24 @@ const RuleColorPreview = ({ color, onChange }: RuleColorPreviewProps) => { }; type Props = { - rule: Rule, + rule: HighlightingRuleClass, className?: string, draggableProps?: DraggableProps; dragHandleProps?: DragHandleProps; + onUpdate: (existingRule: HighlightingRuleClass, field: string, value: Value, condition: Condition, color: Color) => Promise, + onDelete: (rule: HighlightingRuleClass) => Promise, }; const HighlightingRule = forwardRef(({ rule, - className, - draggableProps, - dragHandleProps, + className = undefined, + draggableProps = undefined, + dragHandleProps = undefined, + onUpdate, + onDelete, }, ref) => { const { field, value, color, condition } = rule; const [showForm, setShowForm] = useState(false); - const dispatch = useAppDispatch(); const sendTelemetry = useSendTelemetry(); const location = useLocation(); @@ -131,16 +123,24 @@ const HighlightingRule = forwardRef(({ app_action_value: 'search-sidebar-highlight-color-update', }); - return dispatch(updateColor(rule, newColor, hidePopover)); - }, [dispatch, location.pathname, rule, sendTelemetry]); + return onUpdate(rule, rule.field, rule.value, rule.condition, newColor).then(hidePopover); + // return dispatch(updateColor(rule, newColor, hidePopover)); + }, [location.pathname, onUpdate, rule, sendTelemetry]); + const _onDelete = useCallback(() => { - sendTelemetry(TELEMETRY_EVENT_TYPE.SEARCH_SIDEBAR_HIGHLIGHT_DELETED, { - app_pathname: getPathnameWithoutId(location.pathname), - app_action_value: 'search-sidebar-highlight-delete', - }); + if (window.confirm('Do you really want to remove this highlighting?')) { + sendTelemetry(TELEMETRY_EVENT_TYPE.SEARCH_SIDEBAR_HIGHLIGHT_DELETED, { + app_pathname: getPathnameWithoutId(location.pathname), + app_action_value: 'search-sidebar-highlight-delete', + }); + + return onDelete(rule); + // return dispatch(removeHighlightingRule(rule)); + } - return dispatch(onDelete(rule)); - }, [dispatch, location.pathname, rule, sendTelemetry]); + return Promise.resolve(); + // return dispatch(onDelete(rule)); + }, [location.pathname, onDelete, rule, sendTelemetry]); return ( @@ -159,7 +159,11 @@ const HighlightingRule = forwardRef(({ )} - {showForm && setShowForm(false)} rule={rule} />} + {showForm && ( + setShowForm(false)} + rule={rule} + onSubmit={(newField, newValue, newCondition, newColor) => onUpdate(rule, newField, newValue, newCondition, newColor)} /> + )} ); }); diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx index 0798bc67cdfc..992dccb68c7c 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx @@ -15,15 +15,18 @@ * . */ import * as React from 'react'; -import { useCallback, useContext, useState } from 'react'; +import { useCallback, useContext, useState, forwardRef } from 'react'; import { DEFAULT_HIGHLIGHT_COLOR } from 'views/Constants'; import HighlightingRulesContext from 'views/components/contexts/HighlightingRulesContext'; import IconButton from 'components/common/IconButton'; import { SortableList } from 'components/common'; -import { updateHighlightingRules } from 'views/logic/slices/highlightActions'; -import useAppDispatch from 'stores/useAppDispatch'; -import type HighlightingRuleType from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import type { + Value, + Condition, + Color, +} from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import HighlightingRuleClass from 'views/logic/views/formatting/highlighting/HighlightingRule'; import type { DraggableProps, DragHandleProps } from 'components/common/SortableList'; import HighlightingRule, { Container, RuleContainer } from './HighlightingRule'; @@ -34,39 +37,64 @@ import SectionInfo from '../SectionInfo'; import SectionSubheadline from '../SectionSubheadline'; type SortableHighlightingRuleProps = { - item: { id: string, rule: HighlightingRuleType }, + item: { id: string, rule: HighlightingRuleClass }, draggableProps: DraggableProps, dragHandleProps: DragHandleProps, className?: string, - ref: React.Ref + onUpdate: (existingRule: HighlightingRuleClass, field: string, value: Value, condition: Condition, color: Color) => Promise, + onDelete: (rule: HighlightingRuleClass) => Promise, } -const SortableHighlightingRule = ({ item: { id, rule }, draggableProps, dragHandleProps, className, ref }: SortableHighlightingRuleProps) => ( + +const SortableHighlightingRule = forwardRef(({ + item: { id, rule }, draggableProps, dragHandleProps, className = undefined, + onUpdate, onDelete, +}, ref) => ( -); +)); + +type Props = { + description: string, + onUpdateRules: (newRules: Array) => Promise, + onCreateRule: (newRule: HighlightingRuleClass) => Promise, + onUpdateRule: (targetRule: HighlightingRuleClass, field: string, value: Value, condition: Condition, color: Color) => Promise, + onDeleteRule: (rule: HighlightingRuleClass) => Promise, +} -const HighlightingRules = () => { +const HighlightingRules = ({ description, onUpdateRules, onCreateRule: onCreateRuleProp, onUpdateRule, onDeleteRule }: Props) => { const [showForm, setShowForm] = useState(false); const rules = useContext(HighlightingRulesContext) ?? []; const rulesWithId = rules.map((rule) => ({ rule, id: `${rule.field}-${rule.value}-${rule.color}-${rule.condition}` })); - const dispatch = useAppDispatch(); - const updateRules = useCallback((newRulesWithId: Array<{ id: string, rule: HighlightingRuleType }>) => { + const updateRules = useCallback((newRulesWithId: Array<{ id: string, rule: HighlightingRuleClass }>) => { const newRules = newRulesWithId.map(({ rule }) => rule); - return dispatch(updateHighlightingRules(newRules)); - }, [dispatch]); + return onUpdateRules(newRules); + }, [onUpdateRules]); + + const onCreateRule = useCallback((field: string, value: Value, condition: Condition, color: Color) => ( + onCreateRuleProp(HighlightingRuleClass.create(field, value, condition, color)) + ), [onCreateRuleProp]); + + const listItemRender = useCallback((props: { + item: { id: string, rule: HighlightingRuleClass }, + draggableProps: DraggableProps, + dragHandleProps: DragHandleProps, + className?: string, + }) => ( + + ), [onUpdateRule]); return ( <> - Search terms and field values can be highlighted. Highlighting your search query in the results can be enabled/disabled in the graylog server config. - Any field value can be highlighted by clicking on the value and selecting "Highlight this value". - If a term or a value has more than one rule, the first matching rule is used. + {description} Active highlights { onClick={() => setShowForm(!showForm)} title="Add highlighting rule" /> - {showForm && setShowForm(false)} />} + {showForm && setShowForm(false)} onSubmit={onCreateRule} />} Search terms + customListItemRender={listItemRender} /> ); }; diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx new file mode 100644 index 000000000000..5a8f0bb1c355 --- /dev/null +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx @@ -0,0 +1,62 @@ +/* + * 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 React, { useCallback } from 'react'; + +import { + updateHighlightingRules, + addHighlightingRule, + updateHighlightingRule, removeHighlightingRule, +} from 'views/logic/slices/highlightActions'; +import HighlightingRules from 'views/components/sidebar/highlighting/HighlightingRules'; +import type { + Value, + Condition, + Color, +} from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import type HighlightingRule from 'views/logic/views/formatting/highlighting/HighlightingRule'; +import useAppDispatch from 'stores/useAppDispatch'; + +const DESCRIPTION = 'Search terms and field values can be highlighted. Highlighting your search query in the results can be enabled/disabled in the graylog server config.\n' + + 'Any field value can be highlighted by clicking on the value and selecting "Highlight this value".\n' + + 'If a term or a value has more than one rule, the first matching rule is used.'; + +const ViewsHighlightingRules = () => { + const dispatch = useAppDispatch(); + const onUpdateRules = useCallback((newRules: Array) => dispatch(updateHighlightingRules(newRules)).then(() => {}), [dispatch]); + + const onCreateRule = useCallback((newRule: HighlightingRule) => ( + dispatch(addHighlightingRule(newRule)).then(() => {}) + ), [dispatch]); + + const onUpdateRule = useCallback((targetRule: HighlightingRule, field: string, value: Value, condition: Condition, color: Color) => ( + dispatch(updateHighlightingRule(targetRule, { field, value, condition, color })).then(() => {}) + ), [dispatch]); + + const onDeleteRule = useCallback((rule: HighlightingRule) => ( + dispatch(removeHighlightingRule(rule)).then(() => {}) + ), [dispatch]); + + return ( + + ); +}; + +export default ViewsHighlightingRules; diff --git a/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx b/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx index 14bbc0d62f1d..b98f10d32f3b 100644 --- a/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/sidebarSections.tsx @@ -17,10 +17,10 @@ import * as React from 'react'; import type { IconName } from 'components/common/Icon'; +import ViewsHighlightingRules from 'views/components/sidebar/highlighting/ViewsHighlightingRules'; import ViewDescription from './description/ViewDescription'; import AddWidgetButton from './create/AddWidgetButton'; -import HighlightingRules from './highlighting/HighlightingRules'; export type SidebarSectionProps = { sidebarChildren: React.ReactElement, @@ -55,7 +55,7 @@ const sidebarSections: Array = [ key: 'highlighting', icon: 'format_paragraph', title: 'Highlighting', - content: () => , + content: () => , }, { key: 'fieldList', From eaa9179d2b453a47c2ab41aea942273f06a9d0b4 Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Fri, 10 Jan 2025 10:06:52 +0100 Subject: [PATCH 3/4] Update tests --- .../views/components/sidebar/Sidebar.test.tsx | 4 +- .../highlighting/HighlightForm.test.tsx | 54 ++++++++----------- .../highlighting/HighlightingRule.test.tsx | 52 ++++++++---------- .../sidebar/highlighting/HighlightingRule.tsx | 5 -- .../highlighting/HighlightingRules.test.tsx | 6 ++- 5 files changed, 50 insertions(+), 71 deletions(-) diff --git a/graylog2-web-interface/src/views/components/sidebar/Sidebar.test.tsx b/graylog2-web-interface/src/views/components/sidebar/Sidebar.test.tsx index 6d2fe4216db7..4e5e20ae2b7d 100644 --- a/graylog2-web-interface/src/views/components/sidebar/Sidebar.test.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/Sidebar.test.tsx @@ -80,7 +80,7 @@ describe('', () => { const renderSidebar = () => render( - + , @@ -227,7 +227,7 @@ describe('', () => { await screen.findByText('Execution'); - fireEvent.click(await screen.findByText('Query Title')); + fireEvent.click(await screen.findByText('Sidebar Title')); expect(screen.queryByText('Execution')).toBeNull(); }); 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 1fde7d37e084..0bf298460aca 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 @@ -28,12 +28,6 @@ import FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping'; import FieldType, { Properties } from 'views/logic/fieldtypes/FieldType'; import TestStoreProvider from 'views/test/TestStoreProvider'; import useViewsPlugin from 'views/test/testViewsPlugin'; -import { updateHighlightingRule } from 'views/logic/slices/highlightActions'; - -jest.mock('views/logic/slices/highlightActions', () => ({ - addHighlightingRule: jest.fn(() => () => Promise.resolve()), - updateHighlightingRule: jest.fn(() => () => Promise.resolve()), -})); const rule = HighlightingRule.builder() .color(StaticColor.create('#333333')) @@ -54,10 +48,10 @@ describe('HighlightForm', () => { all: Immutable.List([FieldTypeMapping.create('foob', FieldType.create('long', [Properties.Numeric]))]), queryFields: Immutable.Map(), }; - const HighlightFormWithContext = (props: React.ComponentProps) => ( + const SUT = (props: Partial>) => ( - + {}} rule={undefined} onSubmit={() => Promise.resolve()} {...props} /> ); @@ -70,7 +64,7 @@ describe('HighlightForm', () => { useViewsPlugin(); it('should render for edit', async () => { - const { findByText } = render( {}} rule={rule} />); + const { findByText } = render(); const form = await findByText('Edit Highlighting Rule'); const input = await screen.findByLabelText('Value'); @@ -80,15 +74,14 @@ describe('HighlightForm', () => { }); it('should render for new', async () => { - const { findByText } = render( {}} />); + const { findByText } = render(); await findByText('Create Highlighting Rule'); }); it('should fire onClose on cancel', async () => { const onClose = jest.fn(); - const { findByText } = render(); - + const { findByText } = render(); const elem = await findByText('Cancel'); fireEvent.click(elem); @@ -97,29 +90,28 @@ describe('HighlightForm', () => { }); it('should fire update action when saving a existing rule', async () => { - render( {}} rule={rule} />); + const onSubmit = jest.fn(() => Promise.resolve()); + render(); await triggerSaveButtonClick(); - await waitFor(() => expect(updateHighlightingRule) - .toHaveBeenCalledWith(rule, { field: rule.field, value: rule.value, condition: rule.condition, color: rule.color })); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(rule.field, rule.value, rule.condition, rule.color)); }); it('assigns a new static color when type is selected', async () => { - render( {}} rule={rule} />); + const onSubmit = jest.fn(() => Promise.resolve()); + render(); userEvent.click(screen.getByLabelText('Static Color')); await triggerSaveButtonClick(); - await waitFor(() => expect(updateHighlightingRule) - .toHaveBeenCalledWith(rule, expect.objectContaining({ - color: expect.objectContaining({ type: 'static', color: expect.any(String) }), - }))); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(rule.field, rule.value, rule.condition, expect.objectContaining({ type: 'static', color: expect.any(String) }))); }); it('creates a new gradient when type is selected', async () => { - render( {}} rule={rule} />); + const onSubmit = jest.fn(() => Promise.resolve()); + render(); userEvent.click(screen.getByLabelText('Gradient')); @@ -129,27 +121,23 @@ describe('HighlightForm', () => { await triggerSaveButtonClick(); - await waitFor(() => expect(updateHighlightingRule) - .toHaveBeenCalledWith(rule, expect.objectContaining({ - color: expect.objectContaining({ gradient: 'Viridis' }), - }))); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(rule.field, rule.value, rule.condition, expect.objectContaining({ gradient: 'Viridis' }))); }); it('should be able to click submit when has value 0 with type number', async () => { - render( {}} rule={ruleWithValueZero} />); + const onSubmit = jest.fn(() => Promise.resolve()); + render(); await triggerSaveButtonClick(); - - await waitFor(() => expect(updateHighlightingRule) - .toHaveBeenCalledWith(ruleWithValueZero, { field: ruleWithValueZero.field, value: '0', condition: ruleWithValueZero.condition, color: ruleWithValueZero.color })); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(ruleWithValueZero.field, '0', ruleWithValueZero.condition, ruleWithValueZero.color)); }); - it('should be able to click submit when has value false with type boolean', async () => { - render( {}} rule={ruleWithValueFalse} />); + it('should be able to click submit when has value false with type boolean', async () => { + const onSubmit = jest.fn(() => Promise.resolve()); + render(); await triggerSaveButtonClick(); - await waitFor(() => expect(updateHighlightingRule) - .toHaveBeenCalledWith(ruleWithValueFalse, { field: ruleWithValueFalse.field, value: 'false', condition: ruleWithValueFalse.condition, color: ruleWithValueFalse.color })); + await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(ruleWithValueFalse.field, 'false', ruleWithValueFalse.condition, ruleWithValueFalse.color)); }); }); diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.test.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.test.tsx index 18effdae9b5f..699b329c1724 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.test.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.test.tsx @@ -21,12 +21,6 @@ import userEvent from '@testing-library/user-event'; import Rule from 'views/logic/views/formatting/highlighting/HighlightingRule'; import { StaticColor } from 'views/logic/views/formatting/highlighting/HighlightingColor'; import useViewsPlugin from 'views/test/testViewsPlugin'; -import { asMock } from 'helpers/mocking'; -import useAppDispatch from 'stores/useAppDispatch'; -import mockDispatch from 'views/test/mockDispatch'; -import { createSearch } from 'fixtures/searches'; -import type { RootState } from 'views/types'; -import { updateHighlightingRule, removeHighlightingRule } from 'views/logic/slices/highlightActions'; import HighlightingRule from './HighlightingRule'; @@ -41,35 +35,36 @@ describe('HighlightingRule', () => { useViewsPlugin(); const rule = Rule.create('response_time', '250', undefined, StaticColor.create('#f44242')); - const view = createSearch(); - const dispatch = mockDispatch({ view: { view, activeQuery: 'query-id-1' } } as RootState); - beforeEach(() => { - asMock(useAppDispatch).mockReturnValue(dispatch); - }); + const SUT = (props: Partial>) => ( + Promise.resolve()} + onDelete={() => Promise.resolve()} + {...props} /> + ); it('should display field and value of rule', async () => { - render(); + render(); await screen.findByText('response_time'); await screen.findByText(/250/); }); it('should update rule if color was changed', async () => { - render(); + const onUpdate = jest.fn(() => Promise.resolve()); + render(); const staticColorPicker = await screen.findByTestId('static-color-preview'); userEvent.click(staticColorPicker); - userEvent.click(await screen.findByTitle(/#fbfdd8/i)); await waitFor(() => { - expect(updateHighlightingRule).toHaveBeenCalledWith(rule, { color: StaticColor.create('#fbfdd8') }); + expect(onUpdate).toHaveBeenCalledWith(rule, rule.field, rule.value, rule.condition, StaticColor.create('#fbfdd8')); }); }); it('should close popover when color was changed', async () => { - render(); + render(); const staticColorPicker = await screen.findByTestId('static-color-preview'); userEvent.click(staticColorPicker); @@ -83,7 +78,7 @@ describe('HighlightingRule', () => { describe('rule edit', () => { it('should show a edit modal', async () => { - render(); + render(); const editIcon = await screen.findByTitle('Edit this Highlighting Rule'); expect(screen.queryByText('Edit Highlighting Rule')).not.toBeInTheDocument(); @@ -96,41 +91,38 @@ describe('HighlightingRule', () => { describe('rule removal:', () => { let oldConfirm = null; - let deleteIcon; + const findDeleteIcon = () => screen.findByTitle('Remove this Highlighting Rule'); beforeEach(async () => { oldConfirm = window.confirm; window.confirm = jest.fn(() => false); - - // eslint-disable-next-line testing-library/no-render-in-setup - render(); - - deleteIcon = await screen.findByTitle('Remove this Highlighting Rule'); }); afterEach(() => { window.confirm = oldConfirm; }); - it('asks for confirmation before rule is removed', () => { - userEvent.click(deleteIcon); + it('asks for confirmation before rule is removed', async () => { + render(); + userEvent.click(await findDeleteIcon()); expect(window.confirm).toHaveBeenCalledWith('Do you really want to remove this highlighting?'); }); it('does not remove rule if confirmation was cancelled', async () => { - userEvent.click(deleteIcon); + render(); + userEvent.click(await findDeleteIcon()); await screen.findByText('response_time'); }); it('removes rule rule if confirmation was acknowledged', async () => { + const onDelete = jest.fn(() => Promise.resolve()); + render(); window.confirm = jest.fn(() => true); - userEvent.click(deleteIcon); + userEvent.click(await findDeleteIcon()); - await waitFor(() => { - expect(removeHighlightingRule).toHaveBeenCalledWith(rule); - }); + await waitFor(() => expect(onDelete).toHaveBeenCalledWith(rule)); }); }); }); diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx index 9e511785c4cd..c3f4d36a98c6 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRule.tsx @@ -26,8 +26,6 @@ import { ColorPickerPopover, Icon, IconButton } from 'components/common'; import HighlightForm from 'views/components/sidebar/highlighting/HighlightForm'; import type HighlightingColor from 'views/logic/views/formatting/highlighting/HighlightingColor'; import { StaticColor } from 'views/logic/views/formatting/highlighting/HighlightingColor'; -import type { AppDispatch } from 'stores/useAppDispatch'; -import { updateHighlightingRule, removeHighlightingRule } from 'views/logic/slices/highlightActions'; import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; import { getPathnameWithoutId } from 'util/URLUtils'; @@ -124,7 +122,6 @@ const HighlightingRule = forwardRef(({ }); return onUpdate(rule, rule.field, rule.value, rule.condition, newColor).then(hidePopover); - // return dispatch(updateColor(rule, newColor, hidePopover)); }, [location.pathname, onUpdate, rule, sendTelemetry]); const _onDelete = useCallback(() => { @@ -135,11 +132,9 @@ const HighlightingRule = forwardRef(({ }); return onDelete(rule); - // return dispatch(removeHighlightingRule(rule)); } return Promise.resolve(); - // return dispatch(onDelete(rule)); }, [location.pathname, onDelete, rule, sendTelemetry]); return ( diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.test.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.test.tsx index 190bd225d8bf..831a235c2f40 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.test.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.test.tsx @@ -38,7 +38,11 @@ const HighlightingRules = ({ rules = [] }: { rules?: Array }) return ( - + Promise.resolve()} + onCreateRule={() => Promise.resolve()} + onUpdateRule={() => Promise.resolve()} + onDeleteRule={() => Promise.resolve()} /> ); From 9beccab45c37cc5245e062a9d5a879bef2b4cfa8 Mon Sep 17 00:00:00 2001 From: Linus Pahl Date: Fri, 10 Jan 2025 10:47:31 +0100 Subject: [PATCH 4/4] Cleanup --- .../views/components/sidebar/ContentColumn.tsx | 11 ++++++----- .../src/views/components/sidebar/Sidebar.tsx | 5 ++++- .../sidebar/highlighting/HighlightForm.tsx | 2 +- .../sidebar/highlighting/HighlightingRules.tsx | 16 ++++++++++------ .../highlighting/ViewsHighlightingRules.tsx | 2 +- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx b/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx index d2c9e202c396..3564fd5c403e 100644 --- a/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/ContentColumn.tsx @@ -21,12 +21,13 @@ import type { SearchPreferencesLayout } from 'views/components/contexts/SearchPa import { IconButton } from 'components/common'; type Props = { - title: string, children: React.ReactNode, closeSidebar: () => void, + enableSidebarPinning: boolean, + forceSideBarPinned: boolean, searchPageLayout: SearchPreferencesLayout | undefined | null, sectionTitle: string, - forceSideBarPinned: boolean, + title: string, }; export const Container = styled.div<{ $sidebarIsPinned: boolean }>(({ theme, $sidebarIsPinned }) => css` @@ -127,7 +128,7 @@ const SectionContent = styled.div` } `; -const toggleSidebarPinning = (searchPageLayout) => { +const toggleSidebarPinning = (searchPageLayout: SearchPreferencesLayout) => { if (!searchPageLayout) { return; } @@ -137,7 +138,7 @@ const toggleSidebarPinning = (searchPageLayout) => { togglePinning(); }; -const ContentColumn = ({ children, title, sectionTitle, closeSidebar, searchPageLayout, forceSideBarPinned }: Props) => { +const ContentColumn = ({ children, title, sectionTitle, closeSidebar, searchPageLayout, forceSideBarPinned, enableSidebarPinning }: Props) => { const sidebarIsPinned = searchPageLayout?.config.sidebar.isPinned || forceSideBarPinned; return ( @@ -148,7 +149,7 @@ const ContentColumn = ({ children, title, sectionTitle, closeSidebar, searchPage {title} - {!forceSideBarPinned && ( + {!forceSideBarPinned && enableSidebarPinning && ( toggleSidebarPinning(searchPageLayout)} diff --git a/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx b/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx index 5d1c5cad8d4d..32b8dc880e9e 100644 --- a/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/Sidebar.tsx @@ -37,6 +37,7 @@ import sidebarActions from './sidebarActions'; type Props = { actions?: Array, children?: React.ReactElement, + enableSidebarPinning?: boolean, forceSideBarPinned?: boolean, results?: QueryResult searchPageLayout?: SearchPreferencesLayout, @@ -80,6 +81,7 @@ const _selectSidebarSection = (sectionKey, activeSectionKey, setActiveSectionKey const Sidebar = ({ searchPageLayout = undefined, results = undefined, children = undefined, title, sections = sidebarSections, actions = sidebarActions, forceSideBarPinned = false, + enableSidebarPinning = true, }: Props) => { const sendTelemetry = useSendTelemetry(); const location = useLocation(); @@ -112,6 +114,7 @@ const Sidebar = ({ {activeSection && !!SectionContent && ( @@ -128,7 +131,7 @@ const Sidebar = ({ ); }; -const SidebarWithContext = ({ children, ...props }: React.ComponentProps) => ( +const SidebarWithContext = ({ children = undefined, ...props }: React.ComponentProps) => ( {(searchPageLayout) => {children}} 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 5f8a9ea50523..5d247a9df127 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightForm.tsx @@ -169,7 +169,7 @@ const HighlightForm = ({ onClose, rule = undefined, onSubmit: onSubmitProp }: Pr options={fieldOptions} allowCreate value={value} - placeholder="Pick a field" /> + placeholder="Select or type field name" /> )} diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx index 992dccb68c7c..df070cbdd3af 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/HighlightingRules.tsx @@ -65,9 +65,10 @@ type Props = { onCreateRule: (newRule: HighlightingRuleClass) => Promise, onUpdateRule: (targetRule: HighlightingRuleClass, field: string, value: Value, condition: Condition, color: Color) => Promise, onDeleteRule: (rule: HighlightingRuleClass) => Promise, + showSearchHighlightInfo?: boolean, } -const HighlightingRules = ({ description, onUpdateRules, onCreateRule: onCreateRuleProp, onUpdateRule, onDeleteRule }: Props) => { +const HighlightingRules = ({ description, onUpdateRules, onCreateRule: onCreateRuleProp, onUpdateRule, onDeleteRule, showSearchHighlightInfo = true }: Props) => { const [showForm, setShowForm] = useState(false); const rules = useContext(HighlightingRulesContext) ?? []; const rulesWithId = rules.map((rule) => ({ rule, id: `${rule.field}-${rule.value}-${rule.color}-${rule.condition}` })); @@ -89,7 +90,7 @@ const HighlightingRules = ({ description, onUpdateRules, onCreateRule: onCreateR className?: string, }) => ( - ), [onUpdateRule]); + ), [onDeleteRule, onUpdateRule]); return ( <> @@ -103,10 +104,13 @@ const HighlightingRules = ({ description, onUpdateRules, onCreateRule: onCreateR title="Add highlighting rule" /> {showForm && setShowForm(false)} onSubmit={onCreateRule} />} - - - Search terms - + + {showSearchHighlightInfo && ( + + + Search terms + + )} diff --git a/graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx b/graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx index 5a8f0bb1c355..bb3fe7da2ee5 100644 --- a/graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx +++ b/graylog2-web-interface/src/views/components/sidebar/highlighting/ViewsHighlightingRules.tsx @@ -31,7 +31,7 @@ import type HighlightingRule from 'views/logic/views/formatting/highlighting/Hig import useAppDispatch from 'stores/useAppDispatch'; const DESCRIPTION = 'Search terms and field values can be highlighted. Highlighting your search query in the results can be enabled/disabled in the graylog server config.\n' - + 'Any field value can be highlighted by clicking on the value and selecting "Highlight this value".\n' + + 'Any field value can be highlighted by clicking on the value and selecting "Highlight this value".\n' + 'If a term or a value has more than one rule, the first matching rule is used.'; const ViewsHighlightingRules = () => {