Skip to content
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
965ec55
[ES|QL Controls] Highlight related panels on click
Zacqary Apr 16, 2026
f0ca65c
Changes from node scripts/lint_ts_projects --fix
kibanamachine Apr 21, 2026
7ccad3b
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Apr 21, 2026
e7264f2
Fix ES|QL controls mistakenly counting data controls as related panels
Zacqary Apr 21, 2026
b0c3745
Highlight chained ES|QL controls
Zacqary Apr 21, 2026
2d8d7c9
Fix circular dependency
Zacqary Apr 22, 2026
19ad17b
tsconfig and moon fix
Zacqary Apr 22, 2026
1bb43e0
Resolve circ dep from presentation_util
Zacqary Apr 22, 2026
e478e82
Changes from node scripts/lint_ts_projects --fix
kibanamachine Apr 22, 2026
8d857bd
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Apr 22, 2026
452834a
Create controls folder for esql-utils
Zacqary Apr 22, 2026
5344093
Restyle panel highlighting and fix chained control relationships
Zacqary Apr 23, 2026
d8d1108
Remove unused isOpen prop
Zacqary Apr 27, 2026
6e76e34
Fix typecheck, simplify panel relationship comparators
Zacqary Apr 27, 2026
045453d
Fix typecheck
Zacqary Apr 27, 2026
0042842
Update limits.yml
Zacqary Apr 27, 2026
ea6c92c
Fix tests
Zacqary Apr 28, 2026
5d9bf7c
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 28, 2026
43dcf2e
Fix typecheck
Zacqary Apr 29, 2026
9f1a0dc
Fix typecheck and limits.yml
Zacqary May 1, 2026
e771d3d
Fix jest snapshots
Zacqary May 1, 2026
a394bf6
Merge main@upstream
Zacqary May 11, 2026
a9f4f58
Fix bad merge
Zacqary May 11, 2026
b51a10f
Apply suggestions from code review
Zacqary May 11, 2026
ea947dc
Simplify esql related panel determination
Zacqary May 11, 2026
18ab5f2
Fix jest
Zacqary May 12, 2026
88becd8
Merge main@upstream
Zacqary May 12, 2026
f22f0de
Merge branch 'main' into 204508-esql-highlight
Zacqary May 13, 2026
ced9948
Fix label logic to avoid breaking cypress
Zacqary May 13, 2026
4c6e808
Merge branch 'main' into 204508-esql-highlight
Zacqary May 14, 2026
2a22acb
Refactor related panels to move comparators to controls
Zacqary May 14, 2026
46eec2c
Merge main@upstream
Zacqary May 15, 2026
de765f2
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine May 15, 2026
14e6349
Fix tests and typecheck
Zacqary May 15, 2026
4a73f7d
Merge main@upstream
Zacqary May 18, 2026
50dbf49
Update src/platform/packages/private/kbn-controls-renderer/src/compon…
Zacqary May 19, 2026
521f47c
Remove comment for reverted box shadow style
Zacqary May 19, 2026
aac5372
Lighten selected panel background
Zacqary May 19, 2026
3bdca02
Lighten pinned control bkg
Zacqary May 19, 2026
272c6b7
Update src/platform/packages/private/kbn-controls-renderer/src/compon…
Zacqary May 20, 2026
c0773ff
Merge main@upstream
Zacqary May 20, 2026
01fc9aa
Fix bad merge
Zacqary May 21, 2026
e98887a
Fix jest
Zacqary May 22, 2026
273f040
Merge branch 'main' into 204508-esql-highlight
Zacqary May 22, 2026
cf0af2c
Merge branch 'main' into 204508-esql-highlight
Heenawter May 26, 2026
807e937
Move initializeRelatedPanels only to controls, add blurred panels sub…
Zacqary May 26, 2026
f0cd795
Changes from node scripts/lint.js --fix
kibanamachine May 27, 2026
8f48868
Changes from node scripts/lint_ts_projects --fix
kibanamachine May 27, 2026
a385cb5
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine May 27, 2026
8329dc5
Merge main@upstream
Zacqary May 27, 2026
a3e7c4c
Fix bad merge
Zacqary May 27, 2026
cb6179f
Fix circ dep
Zacqary May 28, 2026
ed37f59
Changes from node scripts/check
kibanamachine May 28, 2026
6e3f83e
Fix typecheck and bad merge
Zacqary May 29, 2026
c7fe38c
Changes from node scripts/check
kibanamachine May 29, 2026
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
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ pageLoadAssetSize:
visTypeTable: 18999
visTypeTagcloud: 7876
visTypeTimelion: 12512
visTypeTimeseries: 20000
visTypeTimeseries: 21819
visTypeVega: 38538
visTypeVislib: 14679
visTypeXy: 32342
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
*/

export { ControlsRenderer } from './src/controls_renderer';
export { ControlLabelTooltip } from './src/components/control_label_tooltip';
export type { ControlsRendererParentApi, ControlsLayout } from './src/types';
export { useIndicateRelatedPanelsSelector } from './src/hooks';
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { EuiToolTip, EuiBadge, type EuiToolTipProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';

interface RelatedPanelProps {
canIndicateRelatedPanels: boolean;
isIndicatingRelatedPanels: boolean;
numberOfRelatedPanels?: number;
}
// Throw a typescript error if one, but not all, of the related panel props are defined
// Prevents us from e.g. setting canIndicateRelatedPanels to true and forgetting to pass isIndicatingRelatedPanels
type AllRelatedPanelPropsOrNone = RelatedPanelProps | { [K in keyof RelatedPanelProps]?: never };

type Props = Omit<Partial<EuiToolTipProps>, 'children'> & {
panelLabel?: string;
panelTooltipLabel?: string;
children: EuiToolTipProps['children'];
} & AllRelatedPanelPropsOrNone;

export const ControlLabelTooltip = ({
canIndicateRelatedPanels,
isIndicatingRelatedPanels,
numberOfRelatedPanels,
panelLabel,
panelTooltipLabel,
children,
...rest
}: Props) => {
const relatedPanelCountBadge =
canIndicateRelatedPanels && numberOfRelatedPanels !== undefined ? (
<EuiBadge color="hollow">
{i18n.translate('controls.controlGroup.numberOfRelatedPanels', {
defaultMessage: '{numberOfRelatedPanels, plural, one {# panel} other {# panels}}',
values: { numberOfRelatedPanels },
})}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
</EuiBadge>
) : null;

const tooltipContent =
numberOfRelatedPanels === 0
? i18n.translate('controls.controlGroup.noRelatedPanels', {
defaultMessage:
// In practice, this message can only appear for ES|QL controls
"This variable control isn't used by any visualization on the dashboard. Variable controls only apply to ES|QL visualizations that include them in their query.",
})
: isIndicatingRelatedPanels
? i18n.translate('controls.controlGroup.clickToStopHighlighting', {
defaultMessage: 'Click to stop highlighting panels.',
})
: i18n.translate('controls.controlGroup.clickToHighlight', {
defaultMessage: 'Click to highlight panels.',
Comment thread
Zacqary marked this conversation as resolved.
Outdated
});

const tooltipProps = canIndicateRelatedPanels
? {
title: (
<>
{panelTooltipLabel ?? panelLabel} {relatedPanelCountBadge}
</>
),
content: tooltipContent,
}
: { content: panelTooltipLabel ?? panelLabel };

return (
<EuiToolTip {...tooltipProps} {...rest} id={rest.id}>
{children}
</EuiToolTip>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import classNames from 'classnames';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Subscription, of } from 'rxjs';

import { useSortable } from '@dnd-kit/sortable';
Expand All @@ -18,21 +18,31 @@ import {
EuiFormControlLayout,
EuiFormLabel,
EuiFormRow,
EuiToolTip,
EuiIcon,
transparentize,
type UseEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import type { HasCustomPrepend, PinnedControlLayoutState } from '@kbn/controls-schemas';
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
import { EmbeddableRenderer, type DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { useBatchedPublishingSubjects, type PublishingSubject } from '@kbn/presentation-publishing';

import {
apiPublishesRelatedPanels,
useBatchedPublishingSubjects,
type PublishingSubject,
} from '@kbn/presentation-publishing';
import {
apiPublishesTooltipLabel,
type PublishesTooltipLabel,
} from '@kbn/controls-schemas/src/types';
import type { ControlsRendererParentApi } from '../types';
import { apiPublishesLabel } from '../utils';
import { controlWidthStyles } from './control_panel.styles';
import { DragHandle } from './drag_handle';
import { FloatingActions } from './floating_actions';
import { ControlLabelTooltip } from './control_label_tooltip';
import { useIndicateRelatedPanelsSelector } from '../hooks';

export const ControlPanel = ({
parentApi,
Expand All @@ -45,21 +55,42 @@ export const ControlPanel = ({
}) => {
const styles = useMemoCss(controlPanelStyles);

const [api, setApi] = useState<(DefaultEmbeddableApi & Partial<HasCustomPrepend>) | null>(null);
const [api, setApi] = useState<
(DefaultEmbeddableApi & Partial<HasCustomPrepend> & Partial<PublishesTooltipLabel>) | null
>(null);

const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id,
});

const [viewMode, disabledActionIds] = useBatchedPublishingSubjects(
const [viewMode, disabledActionIds, indicateRelatedPanelsId] = useBatchedPublishingSubjects(
parentApi.viewMode$,
parentApi.disabledActionIds$ ?? (of([] as string[]) as PublishingSubject<string[]>)
parentApi.disabledActionIds$ ?? (of([] as string[]) as PublishingSubject<string[]>),
parentApi.indicateRelatedPanelsId$ ?? (of(undefined) as PublishingSubject<undefined>)
);

const [panelLabel, setPanelLabel] = useState<string | undefined>();
const [panelTooltipLabel, setPanelTooltipLabel] = useState<string | undefined>();
const [relatedPanels, setRelatedPanels] = useState<string[]>([]);

const prependWrapperRef = useRef<HTMLDivElement>(null);

const indicateControl = useMemo(
() =>
Boolean(
api &&
indicateRelatedPanelsId !== undefined &&
relatedPanels.includes(indicateRelatedPanelsId)
),
[relatedPanels, indicateRelatedPanelsId, api]
);
const {
canIndicateRelatedPanels,
isIndicatingRelatedPanels,
onToggleIndicateRelatedPanels,
numberOfRelatedPanels,
} = useIndicateRelatedPanelsSelector(api);

useEffect(() => {
if (!api) return;

Expand All @@ -72,6 +103,16 @@ export const ControlPanel = ({
})
);
}
if (apiPublishesTooltipLabel(api)) {
subscriptions.add(
api.tooltipLabel$.subscribe((result) => {
setPanelTooltipLabel(result);
})
);
}
if (apiPublishesRelatedPanels(api)) {
subscriptions.add(api.relatedPanels$.subscribe(setRelatedPanels));
}
return () => {
subscriptions.unsubscribe();
};
Expand All @@ -94,6 +135,49 @@ export const ControlPanel = ({
);

const isEditable = viewMode === 'edit';
const enableIndicateRelatedPanels = Boolean(canIndicateRelatedPanels && numberOfRelatedPanels);
const handleToggleIndicateRelated = useCallback(
() => (enableIndicateRelatedPanels ? onToggleIndicateRelatedPanels() : null),
[enableIndicateRelatedPanels, onToggleIndicateRelatedPanels]
);

const controlLabel = (
<ControlLabelTooltip
canIndicateRelatedPanels={canIndicateRelatedPanels}
isIndicatingRelatedPanels={isIndicatingRelatedPanels}
numberOfRelatedPanels={numberOfRelatedPanels}
panelLabel={panelLabel}
panelTooltipLabel={panelTooltipLabel}
anchorProps={{ className: 'eui-textTruncate', css: styles.tooltipStyles }}
>
<EuiFormLabel
className="controlPanel--label"
onClick={handleToggleIndicateRelated}
onKeyDown={(e) =>
e.key === 'Enter' || e.key === ' ' ? handleToggleIndicateRelated() : null
}
Comment thread
Zacqary marked this conversation as resolved.
role={enableIndicateRelatedPanels ? 'button' : undefined}
tabIndex={enableIndicateRelatedPanels ? 0 : undefined}
>
<span css={styles.prependWrapperStyles} ref={prependWrapperRef}>
{panelLabel}
{canIndicateRelatedPanels && numberOfRelatedPanels === 0 && (
<>
{' '}
<EuiIcon
size="s"
aria-label={i18n.translate('controls.controlGroup.warningNoRelatedPanels', {
defaultMessage: 'Warning: No related panels',
})}
type="warning"
/>
</>
)}
</span>
</EuiFormLabel>
</ControlLabelTooltip>
);

return (
<EuiFlexItem
component="li"
Expand Down Expand Up @@ -128,6 +212,8 @@ export const ControlPanel = ({
fullWidth
className={classNames('controlFrame__formControlLayout', {
'controlFrame__formControlLayout--edit': isEditable,
'controlFrame__formControlLayout--focused': indicateControl,
'controlFrame__formControlLayout--selected': isIndicatingRelatedPanels,
type,
})}
css={styles.formControl}
Expand All @@ -145,24 +231,19 @@ export const ControlPanel = ({
<api.CustomPrependComponent />
</>
) : (
<DragHandle
isEditable={isEditable}
controlTitle={panelLabel}
className="controlFrame__dragHandle"
{...attributes}
{...listeners}
>
<EuiToolTip
content={panelLabel}
anchorProps={{ className: 'eui-textTruncate', css: styles.tooltipStyles }}
<>
<DragHandle
isEditable={isEditable}
controlTitle={panelLabel}
className="controlFrame__dragHandle"
highContrast={isIndicatingRelatedPanels}
{...attributes}
{...listeners}
>
<EuiFormLabel className="controlPanel--label">
<span css={styles.prependWrapperStyles} ref={prependWrapperRef}>
{panelLabel}
</span>
</EuiFormLabel>
</EuiToolTip>
</DragHandle>
{!enableIndicateRelatedPanels && controlLabel}
</DragHandle>
{enableIndicateRelatedPanels && controlLabel}
</>
)}
</>
}
Expand Down Expand Up @@ -217,10 +298,23 @@ const controlPanelStyles = {
paddingInlineStart: `${euiTheme.size.xxs} !important`, // corrected syntax for skinny icon
},
},
'&.controlFrame__formControlLayout--focused': {
outline: `${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0}`,
},
'&.controlFrame__formControlLayout--selected': {
outline: `${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0}`,
backgroundColor: transparentize(euiTheme.colors.vis.euiColorVis0, 0.1),
'& div, & button': {
backgroundColor: 'transparent',
},
},
Comment thread
Heenawter marked this conversation as resolved.
'.controlPanel--label': {
padding: '0 !important',
height: '100%',
maxWidth: '100%',
'&[role="button"]': {
cursor: 'pointer',
},
},
}),
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we okay that ES|QL controls cannot be dragged from their label now? cc @andreadelrio

Screen.Recording.2026-05-05.at.4.47.38.PM.mov

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zacqary is it possible to preserve the drag behavior for ES|QL controls?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreadelrio I can probably figure something out to detect the difference between a click and a drag on the label by checking if the user moves their mouse a significant amount before releasing the mouse button. Might involve fighting EUI a bit, and I won't be able to get the cursor to change to drag on anything but the drag handle, but I'll see what I can do.

If it ends up being more complicated than I expect, is it acceptable to ship this where the user now has to grab the drag handle explicitly instead of being able to drag from the entire label?

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
interface DragHandleProps {
isEditable: boolean;
controlTitle?: string;
highContrast?: boolean; // If true, set the icon color to higher contrast instead of subdued
[key: string]: any; // Allows passing additional props (like drag info)
}

Expand All @@ -37,12 +38,19 @@ const dragHandleStyles = {
pointerEvents: 'none', // Prevent label from blocking drag events
},
}),
dragHandleHighContrast: ({ euiTheme }: UseEuiTheme) =>
css({
'.euiIcon': {
color: euiTheme.colors.textParagraph,
},
}),
};

export const DragHandle = ({
isEditable,
controlTitle = '',
children,
highContrast,
...rest
}: DragHandleProps) => {
const styles = useMemoCss(dragHandleStyles);
Expand All @@ -56,7 +64,7 @@ export const DragHandle = ({
defaultMessage: 'Move control {controlTitle}',
values: { controlTitle },
})}
css={styles.dragHandle}
css={[styles.dragHandle, highContrast ? styles.dragHandleHighContrast : null]}
>
<EuiIcon type="dragHorizontal" aria-hidden={true} />
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { useIndicateRelatedPanelsSelector } from './use_indicate_related_panels_selector';
Loading