ElementSelection: New element selection context to support selecting elements (#97029)

* ElementSelection: New element selction context to support selecting elements like panels

* Update

* Update
This commit is contained in:
Torkel Ödegaard 2024-11-28 15:06:00 +01:00 committed by GitHub
parent 6974f77d18
commit 335241d93d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 5 deletions

View File

@ -3762,7 +3762,8 @@ exports[`better eslint`] = {
"public/app/features/sandbox/TestStuffPage.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
],
"public/app/features/scopes/index.ts:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./instance\`)", "0"],

View File

@ -0,0 +1,52 @@
import React, { createContext, useCallback, useContext } from 'react';
/** @alpha */
export interface ElementSelectionContextState {
/**
* Turn on selection mode & show selection state
*/
enabled?: boolean;
/** List of currently selected elements */
selected: ElementSelectionContextItem[];
onSelect: (item: ElementSelectionContextItem, multi?: boolean) => void;
}
export interface ElementSelectionContextItem {
id: string;
}
export const ElementSelectionContext = createContext<ElementSelectionContextState | undefined>(undefined);
export interface UseElementSelectionResult {
isSelected?: boolean;
isSelectable?: boolean;
onSelect?: (evt: React.PointerEvent) => void;
}
export function useElementSelection(id: string | undefined): UseElementSelectionResult {
if (!id) {
return {};
}
const context = useContext(ElementSelectionContext);
if (!context) {
return {};
}
const isSelected = context.selected.some((item) => item.id === id);
const onSelect = useCallback<React.PointerEventHandler>(
(evt) => {
if (!context.enabled) {
return;
}
// To prevent this click form clearing the selection
evt.stopPropagation();
context.onSelect({ id }, evt.shiftKey);
},
[context, id]
);
return { isSelected, onSelect, isSelectable: context.enabled };
}

View File

@ -9,6 +9,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { useStyles2, useTheme2 } from '../../themes';
import { getFocusStyles } from '../../themes/mixins';
import { DelayRender } from '../../utils/DelayRender';
import { useElementSelection } from '../ElementSelectionContext/ElementSelectionContext';
import { Icon } from '../Icon/Icon';
import { LoadingBar } from '../LoadingBar/LoadingBar';
import { Text } from '../Text/Text';
@ -33,6 +34,7 @@ interface BaseProps {
menu?: ReactElement | (() => ReactElement);
dragClass?: string;
dragClassCancel?: string;
selectionId?: string;
/**
* Use only to indicate loading or streaming data in the panel.
* Any other values of loadingState are ignored.
@ -131,6 +133,7 @@ export function PanelChrome({
statusMessageOnClick,
leftItems,
actions,
selectionId,
onCancelQuery,
onOpenMenu,
collapsible = false,
@ -145,6 +148,7 @@ export function PanelChrome({
const styles = useStyles2(getStyles);
const panelContentId = useId();
const panelTitleId = useId().replace(/:/g, '_');
const { isSelected, onSelect } = useElementSelection(selectionId);
const hasHeader = !hoverHeader;
@ -263,7 +267,11 @@ export function PanelChrome({
return (
// tabIndex={0} is needed for keyboard accessibility in the plot area
<section
className={cx(styles.container, { [styles.transparentContainer]: isPanelTransparent })}
className={cx(
styles.container,
isPanelTransparent && styles.transparentContainer,
isSelected && 'dashboard-selected-element'
)}
style={containerStyles}
aria-labelledby={!!title ? panelTitleId : undefined}
data-testid={testid}
@ -300,7 +308,12 @@ export function PanelChrome({
)}
{hasHeader && (
<div className={cx(styles.headerContainer, dragClass)} style={headerStyles} data-testid="header-container">
<div
className={cx(styles.headerContainer, dragClass)}
style={headerStyles}
data-testid="header-container"
onPointerUp={onSelect}
>
{statusMessage && (
<div className={dragClassCancel}>
<PanelStatus message={statusMessage} onClick={statusMessageOnClick} ariaLabel="Panel status" />

View File

@ -322,3 +322,9 @@ export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types';
export { ZoomPlugin } from '../graveyard/uPlot/plugins/ZoomPlugin';
export { TooltipPlugin } from '../graveyard/uPlot/plugins/TooltipPlugin';
export {
ElementSelectionContext,
useElementSelection,
type ElementSelectionContextState,
} from './ElementSelectionContext/ElementSelectionContext';

View File

@ -67,5 +67,11 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) {
},
},
},
'.dashboard-selected-element': {
outline: `2px dashed ${theme.colors.primary.border}`,
outlineOffset: '0px',
borderRadius: '2px',
},
});
}

View File

@ -1,6 +1,6 @@
import { NavModelItem } from '@grafana/data';
import { getPluginExtensions, isPluginExtensionLink } from '@grafana/runtime';
import { Button, LinkButton, Stack } from '@grafana/ui';
import { Button, LinkButton, Stack, Text } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useAppNotification } from 'app/core/copy/appNotification';
@ -17,8 +17,9 @@ export const TestStuffPage = () => {
return (
<Page navModel={{ node: node, main: node }}>
<Stack>
<LinkToBasicApp extensionPointId="grafana/sandbox/testing" />
<Text variant="h5">Application notifications (toasts) testing</Text>
<Stack>
<Button onClick={() => notifyApp.success('Success toast', 'some more text goes here')} variant="primary">
Success
</Button>