Skip to content

Commit ea0fda0

Browse files
committed
[ES|QL Controls] Highlight related panels on click
1 parent bdb984b commit ea0fda0

30 files changed

Lines changed: 690 additions & 72 deletions

File tree

src/platform/packages/private/kbn-controls-renderer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
*/
99

1010
export { ControlsRenderer } from './src/controls_renderer';
11+
export { ControlLabelTooltip } from './src/components/control_label_tooltip';
1112
export type { ControlsRendererParentApi, ControlsLayout } from './src/types';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { EuiToolTip, EuiBadge, type EuiToolTipProps } from '@elastic/eui';
11+
import { i18n } from '@kbn/i18n';
12+
import React from 'react';
13+
14+
interface RelatedPanelProps {
15+
canIndicateRelatedPanels: boolean;
16+
isIndicatingRelatedPanels: boolean;
17+
numberOfRelatedPanels?: number;
18+
}
19+
// Throw a typescript error if one, but not all, of the related panel props are defined
20+
// Prevents us from e.g. setting canIndicateRelatedPanels to true and forgetting to pass isIndicatingRelatedPanels
21+
type AllRelatedPanelPropsOrNone = RelatedPanelProps | { [K in keyof RelatedPanelProps]?: never };
22+
23+
type Props = Partial<EuiToolTipProps> & {
24+
panelLabel?: string;
25+
panelTooltipLabel?: string;
26+
isOpen?: boolean;
27+
} & AllRelatedPanelPropsOrNone;
28+
29+
export const ControlLabelTooltip: React.FC<Props> = ({
30+
canIndicateRelatedPanels,
31+
isIndicatingRelatedPanels,
32+
numberOfRelatedPanels,
33+
panelLabel,
34+
panelTooltipLabel,
35+
isOpen,
36+
...rest
37+
}) => {
38+
const relatedPanelCountBadge =
39+
canIndicateRelatedPanels && numberOfRelatedPanels !== undefined ? (
40+
<EuiBadge color="hollow">
41+
{i18n.translate('controls.controlGroup.numberOfRelatedPanels', {
42+
defaultMessage: '{numberOfRelatedPanels, plural, one {# panel} other {# panels}}',
43+
values: { numberOfRelatedPanels },
44+
})}
45+
</EuiBadge>
46+
) : null;
47+
48+
const tooltipContent =
49+
numberOfRelatedPanels === 0
50+
? i18n.translate('controls.controlGroup.noRelatedPanels', {
51+
defaultMessage:
52+
// In practice, this message can only appear for ES|QL controls
53+
"This control isn't used in any ES|QL visualizations. Update your visualizations to use it.",
54+
})
55+
: isIndicatingRelatedPanels
56+
? i18n.translate('controls.controlGroup.clickToStopHighlighting', {
57+
defaultMessage: 'Click to stop highlighting',
58+
})
59+
: i18n.translate('controls.controlGroup.clickToHighlight', {
60+
defaultMessage: 'Click to highlight',
61+
});
62+
63+
const tooltipProps = canIndicateRelatedPanels
64+
? {
65+
title: (
66+
<>
67+
{panelTooltipLabel ?? panelLabel} {relatedPanelCountBadge}
68+
</>
69+
),
70+
content: tooltipContent,
71+
}
72+
: { content: panelTooltipLabel ?? panelLabel };
73+
74+
return <EuiToolTip {...tooltipProps} {...rest} id={rest.id} />;
75+
};

src/platform/packages/private/kbn-controls-renderer/src/components/control_panel.tsx

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
EuiFormControlLayout,
1919
EuiFormLabel,
2020
EuiFormRow,
21-
EuiToolTip,
21+
EuiIcon,
2222
type UseEuiTheme,
2323
} from '@elastic/eui';
2424
import { css } from '@emotion/react';
@@ -27,12 +27,17 @@ import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
2727
import { EmbeddableRenderer, type DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
2828
import { i18n } from '@kbn/i18n';
2929
import { useBatchedPublishingSubjects, type PublishingSubject } from '@kbn/presentation-publishing';
30-
30+
import { useIndicateRelatedPanelsSelector } from '@kbn/presentation-util';
31+
import {
32+
apiPublishesTooltipLabel,
33+
type PublishesTooltipLabel,
34+
} from '@kbn/controls-schemas/src/types';
3135
import type { ControlsRendererParentApi } from '../types';
3236
import { apiPublishesLabel } from '../utils';
3337
import { controlWidthStyles } from './control_panel.styles';
3438
import { DragHandle } from './drag_handle';
3539
import { FloatingActions } from './floating_actions';
40+
import { ControlLabelTooltip } from './control_label_tooltip';
3641

3742
export const ControlPanel = ({
3843
parentApi,
@@ -45,7 +50,9 @@ export const ControlPanel = ({
4550
}) => {
4651
const styles = useMemoCss(controlPanelStyles);
4752

48-
const [api, setApi] = useState<(DefaultEmbeddableApi & Partial<HasCustomPrepend>) | null>(null);
53+
const [api, setApi] = useState<
54+
(DefaultEmbeddableApi & Partial<HasCustomPrepend> & Partial<PublishesTooltipLabel>) | null
55+
>(null);
4956

5057
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
5158
id,
@@ -57,9 +64,17 @@ export const ControlPanel = ({
5764
);
5865

5966
const [panelLabel, setPanelLabel] = useState<string | undefined>();
67+
const [panelTooltipLabel, setPanelTooltipLabel] = useState<string | undefined>();
6068

6169
const prependWrapperRef = useRef<HTMLDivElement>(null);
6270

71+
const {
72+
canIndicateRelatedPanels,
73+
isIndicatingRelatedPanels,
74+
onToggleIndicateRelatedPanels,
75+
numberOfRelatedPanels,
76+
} = useIndicateRelatedPanelsSelector(api);
77+
6378
useEffect(() => {
6479
if (!api) return;
6580

@@ -72,6 +87,13 @@ export const ControlPanel = ({
7287
})
7388
);
7489
}
90+
if (apiPublishesTooltipLabel(api)) {
91+
subscriptions.add(
92+
api.tooltipLabel$.subscribe((result) => {
93+
setPanelTooltipLabel(result);
94+
})
95+
);
96+
}
7597
return () => {
7698
subscriptions.unsubscribe();
7799
};
@@ -94,6 +116,46 @@ export const ControlPanel = ({
94116
);
95117

96118
const isEditable = viewMode === 'edit';
119+
const enableIndicateRelatedPanels = Boolean(canIndicateRelatedPanels && numberOfRelatedPanels);
120+
const handleToggleIndicateRelated = useCallback(
121+
() => (enableIndicateRelatedPanels ? onToggleIndicateRelatedPanels() : null),
122+
[enableIndicateRelatedPanels, onToggleIndicateRelatedPanels]
123+
);
124+
125+
const controlLabel = (
126+
<ControlLabelTooltip
127+
canIndicateRelatedPanels={canIndicateRelatedPanels}
128+
isIndicatingRelatedPanels={isIndicatingRelatedPanels}
129+
numberOfRelatedPanels={numberOfRelatedPanels}
130+
panelLabel={panelLabel}
131+
panelTooltipLabel={panelTooltipLabel}
132+
anchorProps={{ className: 'eui-textTruncate', css: styles.tooltipStyles }}
133+
>
134+
<EuiFormLabel
135+
className="controlPanel--label"
136+
onClick={handleToggleIndicateRelated}
137+
onKeyDown={(e) =>
138+
e.key === 'Enter' || e.key === ' ' ? handleToggleIndicateRelated() : null
139+
}
140+
role={enableIndicateRelatedPanels ? 'button' : undefined}
141+
tabIndex={enableIndicateRelatedPanels ? 0 : undefined}
142+
>
143+
<span css={styles.prependWrapperStyles} ref={prependWrapperRef}>
144+
{panelLabel}{' '}
145+
{canIndicateRelatedPanels && numberOfRelatedPanels === 0 && (
146+
<EuiIcon
147+
size="s"
148+
aria-label={i18n.translate('controls.controlGroup.warningNoRelatedPanels', {
149+
defaultMessage: 'Warning: No related panels',
150+
})}
151+
type="warning"
152+
/>
153+
)}
154+
</span>
155+
</EuiFormLabel>
156+
</ControlLabelTooltip>
157+
);
158+
97159
return (
98160
<EuiFlexItem
99161
component="li"
@@ -128,6 +190,7 @@ export const ControlPanel = ({
128190
fullWidth
129191
className={classNames('controlFrame__formControlLayout', {
130192
'controlFrame__formControlLayout--edit': isEditable,
193+
'controlFrame__formControlLayout--selected': isIndicatingRelatedPanels,
131194
type,
132195
})}
133196
css={styles.formControl}
@@ -145,24 +208,19 @@ export const ControlPanel = ({
145208
<api.CustomPrependComponent />
146209
</>
147210
) : (
148-
<DragHandle
149-
isEditable={isEditable}
150-
controlTitle={panelLabel}
151-
className="controlFrame__dragHandle"
152-
{...attributes}
153-
{...listeners}
154-
>
155-
<EuiToolTip
156-
content={panelLabel}
157-
anchorProps={{ className: 'eui-textTruncate', css: styles.tooltipStyles }}
211+
<>
212+
<DragHandle
213+
isEditable={isEditable}
214+
controlTitle={panelLabel}
215+
className="controlFrame__dragHandle"
216+
highContrast={isIndicatingRelatedPanels}
217+
{...attributes}
218+
{...listeners}
158219
>
159-
<EuiFormLabel className="controlPanel--label">
160-
<span css={styles.prependWrapperStyles} ref={prependWrapperRef}>
161-
{panelLabel}
162-
</span>
163-
</EuiFormLabel>
164-
</EuiToolTip>
165-
</DragHandle>
220+
{!enableIndicateRelatedPanels && controlLabel}
221+
</DragHandle>
222+
{enableIndicateRelatedPanels && controlLabel}
223+
</>
166224
)}
167225
</>
168226
}
@@ -217,10 +275,18 @@ const controlPanelStyles = {
217275
paddingInlineStart: `${euiTheme.size.xxs} !important`, // corrected syntax for skinny icon
218276
},
219277
},
278+
'&.controlFrame__formControlLayout--selected': {
279+
'.euiFormControlLayout__prepend': {
280+
backgroundColor: euiTheme.colors.vis.euiColorVis0,
281+
},
282+
},
220283
'.controlPanel--label': {
221284
padding: '0 !important',
222285
height: '100%',
223286
maxWidth: '100%',
287+
'&[role="button"]': {
288+
cursor: 'pointer',
289+
},
224290
},
225291
}),
226292
};

src/platform/packages/private/kbn-controls-renderer/src/components/drag_handle.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
1616
interface DragHandleProps {
1717
isEditable: boolean;
1818
controlTitle?: string;
19+
highContrast?: boolean; // If true, set the icon color to higher contrast instead of subdued
1920
[key: string]: any; // Allows passing additional props (like drag info)
2021
}
2122

@@ -37,12 +38,19 @@ const dragHandleStyles = {
3738
pointerEvents: 'none', // Prevent label from blocking drag events
3839
},
3940
}),
41+
dragHandleHighContrast: ({ euiTheme }: UseEuiTheme) =>
42+
css({
43+
'.euiIcon': {
44+
color: euiTheme.colors.textParagraph,
45+
},
46+
}),
4047
};
4148

4249
export const DragHandle = ({
4350
isEditable,
4451
controlTitle = '',
4552
children,
53+
highContrast,
4654
...rest
4755
}: DragHandleProps) => {
4856
const styles = useMemoCss(dragHandleStyles);
@@ -56,7 +64,7 @@ export const DragHandle = ({
5664
defaultMessage: 'Move control {controlTitle}',
5765
values: { controlTitle },
5866
})}
59-
css={styles.dragHandle}
67+
css={[styles.dragHandle, highContrast ? styles.dragHandleHighContrast : null]}
6068
>
6169
<EuiIcon type="dragHorizontal" aria-hidden={true} />
6270
{children}

src/platform/packages/private/kbn-controls-renderer/src/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ export interface ControlsLayout {
3636

3737
export type ControlsRendererParentApi = Pick<
3838
PresentationContainer,
39-
'children$' | 'addNewPanel' | 'replacePanel' | 'removePanel'
39+
| 'children$'
40+
| 'addNewPanel'
41+
| 'replacePanel'
42+
| 'removePanel'
43+
| 'setIndicateRelatedPanelsId'
44+
| 'indicateRelatedPanelsId$'
45+
| 'getRelatedPanelIds$'
4046
> &
4147
Partial<PublishesUnifiedSearch> &
4248
PublishesViewMode &

src/platform/packages/shared/controls/controls-schemas/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type React from 'react';
11+
import type { PublishingSubject } from '@kbn/presentation-publishing';
1112

1213
import type { TypeOf } from '@kbn/config-schema';
1314
import type { controlTitleSchema, dataControlSchema } from './control_schema';
@@ -58,3 +59,9 @@ export type TimeSliderControlState = TypeOf<typeof timeSliderControlSchema>;
5859
export interface HasCustomPrepend {
5960
CustomPrependComponent: React.FC<{}>;
6061
}
62+
export interface PublishesTooltipLabel {
63+
tooltipLabel$: PublishingSubject<string>;
64+
}
65+
66+
export const apiPublishesTooltipLabel = (api: unknown): api is PublishesTooltipLabel =>
67+
Boolean((api as PublishesTooltipLabel)?.tooltipLabel$);

src/platform/packages/shared/kbn-esql-utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export {
7272
getQuerySummary,
7373
getEsqlControls,
7474
type ESQLStatsQueryMeta,
75+
getVariableNamePrefix,
7576
} from './src';
7677

7778
export { ENABLE_ESQL, GROUP_NOT_SET_VALUE } from './constants';

src/platform/packages/shared/kbn-esql-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export {
7474
export { getProjectRoutingFromEsqlQuery } from './utils/set_instructions_helpers';
7575
export { isComputedColumn, getQuerySummary } from './utils/get_query_summary';
7676
export { getEsqlControls } from './utils/get_esql_controls';
77+
export { getVariableNamePrefix } from './utils/get_variable_name_prefix';
7778

7879
// Callback functions
7980
export * from './utils/callbacks';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { ESQLVariableType, VariableNamePrefix } from '@kbn/esql-types';
11+
12+
export const getVariableNamePrefix = (type: ESQLVariableType) => {
13+
switch (type) {
14+
case ESQLVariableType.FIELDS:
15+
case ESQLVariableType.FUNCTIONS:
16+
return VariableNamePrefix.IDENTIFIER;
17+
case ESQLVariableType.VALUES:
18+
case ESQLVariableType.TIME_LITERAL:
19+
case ESQLVariableType.MULTI_VALUES:
20+
default:
21+
return VariableNamePrefix.VALUE;
22+
}
23+
};

src/platform/packages/shared/presentation/presentation_publishing/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ export type { PublishesSearchSession } from './interfaces/fetch/publishes_search
201201
// =============================================
202202

203203
export { apiCanAddNewPanel, type CanAddNewPanel } from './interfaces/containers/can_add_new_panel';
204+
export {
205+
apiCanIndicateRelatedPanels,
206+
type CanIndicateRelatedPanels,
207+
} from './interfaces/containers/can_indicate_related_panels';
204208

205209
export {
206210
apiHasSerializedChildState,
@@ -226,10 +230,12 @@ export {
226230
apiCanBeCustomized,
227231
apiCanBeExpanded,
228232
apiCanBePinned,
233+
apiCanBeSelectedToIndicateRelated,
229234
type IsDuplicable,
230235
type IsExpandable,
231236
type IsCustomizable,
232237
type IsPinnable,
238+
type CanBeRelatedPanelsIndicator,
233239
type HasPanelCapabilities,
234240
} from './interfaces/containers/panel_capabilities';
235241

0 commit comments

Comments
 (0)