Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion graylog2-web-interface/src/views/components/WidgetGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import useViewsDispatch from 'views/stores/useViewsDispatch';
import { updateWidgetPositions, updateWidgetPosition } from 'views/logic/slices/widgetActions';
import { setIsDirty } from 'views/logic/slices/viewSlice';
import { widgetDragHandleClass } from 'views/components/widgets/Constants';
import { selectNewWidget } from 'views/logic/slices/widgetsSlice';
import { selectNewWidget, clearNewWidget } from 'views/logic/slices/widgetsSlice';
import ScrollToHint from 'views/components/common/ScrollToHint';

import WidgetContainer from './WidgetContainer';
Expand Down Expand Up @@ -91,8 +91,10 @@ type WidgetsProps = {
};

const WidgetGridItem = ({ onPositionsChange, positions, widgetId, focusedWidget, isNewWidget }: WidgetsProps) => {
const dispatch = useViewsDispatch();
const editing = focusedWidget?.id === widgetId && focusedWidget?.editing;
const widgetPosition = positions[widgetId];
const onSettled = useCallback(() => dispatch(clearNewWidget(widgetId)), [dispatch, widgetId]);

return (
<>
Expand All @@ -101,6 +103,7 @@ const WidgetGridItem = ({ onPositionsChange, positions, widgetId, focusedWidget,
scrollContainer={{ current: document.body }}
title="Scroll to new widget"
ifTrue={isNewWidget}
onSettled={onSettled}
/>
<WidgetComponent
editing={editing}
Expand Down
31 changes: 21 additions & 10 deletions graylog2-web-interface/src/views/components/common/ScrollToHint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
ifValueChanges?: unknown;
ifTrue?: boolean;
autoScroll?: boolean;
// Called once the target is confirmed visible, either because it already was or because this
// component just scrolled it into view. Lets callers that track "still needs attention" state
// (e.g. a newly created widget) clear it based on the outcome instead of guessing a delay.
onSettled?: () => void;
};

const ScrollToHint = ({
Expand All @@ -62,6 +66,7 @@
title,
ifTrue = true,
autoScroll = false,
onSettled = undefined,
}: Props) => {
const scrollTargetRef = useRef<HTMLDivElement | null>(null);
const [showHint, setShowHint] = useState(false);
Expand All @@ -78,22 +83,28 @@

// show the scroll hint if necessary
useEffect(() => {
if (!ifTrue || !scrollTargetRef.current || !scrollContainer.current) {
return;
}

if (
ifTrue &&
scrollTargetRef.current &&
scrollContainer.current &&
!isElementVisibleInContainer(
isElementVisibleInContainer(
scrollTargetRef.current.parentElement ?? scrollTargetRef.current,
scrollContainer.current,
)
) {
if (autoScroll) {
scrollToTarget();
} else {
setShowHint(true);
}
onSettled?.();

return;
}

if (autoScroll) {
scrollToTarget();

Check warning on line 102 in graylog2-web-interface/src/views/components/common/ScrollToHint.tsx

View workflow job for this annotation

GitHub Actions / Reviewbot

Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). /home/runner/work/graylog2-server/graylog2-server/graylog2-web-interface/src/views/components/common/ScrollToHint.tsx:102:7 100 | 101 | if (autoScroll) { > 102 | scrollToTarget(); | ^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect 103 | onSettled?.(); 104 | } else { 105 |

No further rule information available.
onSettled?.();
} else {
setShowHint(true);
}
}, [ifTrue, ifValueChanges, setShowHint, scrollContainer, scrollToTarget, autoScroll]);
}, [ifTrue, ifValueChanges, setShowHint, scrollContainer, scrollToTarget, autoScroll, onSettled]);

// hide the hint automatically
useEffect(() => {
Expand Down
16 changes: 11 additions & 5 deletions graylog2-web-interface/src/views/logic/slices/widgetsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';

import type { ViewsDispatch } from 'views/stores/useViewsDispatch';
import type { GetState } from 'views/types';

export type WidgetsState = {
newWidget: string | undefined;
Expand All @@ -28,18 +29,23 @@ const widgetsSlice = createSlice({
newWidget: undefined,
},
reducers: {
setNewWidget: (state, action: PayloadAction<string>) => ({
setNewWidget: (state, action: PayloadAction<string | undefined>) => ({
...state,
newWidget: action.payload,
}),
},
});
export const widgetsSliceReducer = widgetsSlice.reducer;
export const setNewWidget = (id: string) => (dispatch: ViewsDispatch) => {
const result = dispatch(widgetsSlice.actions.setNewWidget(id));
setTimeout(() => dispatch(widgetsSlice.actions.setNewWidget(undefined)), 1000);
export const setNewWidget = (id: string) => (dispatch: ViewsDispatch) =>
dispatch(widgetsSlice.actions.setNewWidget(id));

return result;
// Widgets stay flagged as "new" until the grid actually scrolls them into view (or finds them
// already visible) rather than after a fixed delay, since the widget-creation/edit round trip
// (including a full search re-execution) can easily take longer than any fixed guess.
export const clearNewWidget = (id: string) => (dispatch: ViewsDispatch, getState: GetState) => {
if (getState().widgets.newWidget === id) {
dispatch(widgetsSlice.actions.setNewWidget(undefined));
}
};

export const selectNewWidget = (state: { widgets: WidgetsState }) => state.widgets.newWidget;
Loading