mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
6974f77d18
commit
335241d93d
@ -3762,7 +3762,8 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/sandbox/TestStuffPage.tsx:5381": [
|
"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 />", "0"],
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
[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": [
|
"public/app/features/scopes/index.ts:5381": [
|
||||||
[0, 0, 0, "Do not re-export imported variable (\`./instance\`)", "0"],
|
[0, 0, 0, "Do not re-export imported variable (\`./instance\`)", "0"],
|
||||||
|
@ -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 };
|
||||||
|
}
|
@ -9,6 +9,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { useStyles2, useTheme2 } from '../../themes';
|
import { useStyles2, useTheme2 } from '../../themes';
|
||||||
import { getFocusStyles } from '../../themes/mixins';
|
import { getFocusStyles } from '../../themes/mixins';
|
||||||
import { DelayRender } from '../../utils/DelayRender';
|
import { DelayRender } from '../../utils/DelayRender';
|
||||||
|
import { useElementSelection } from '../ElementSelectionContext/ElementSelectionContext';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { LoadingBar } from '../LoadingBar/LoadingBar';
|
import { LoadingBar } from '../LoadingBar/LoadingBar';
|
||||||
import { Text } from '../Text/Text';
|
import { Text } from '../Text/Text';
|
||||||
@ -33,6 +34,7 @@ interface BaseProps {
|
|||||||
menu?: ReactElement | (() => ReactElement);
|
menu?: ReactElement | (() => ReactElement);
|
||||||
dragClass?: string;
|
dragClass?: string;
|
||||||
dragClassCancel?: string;
|
dragClassCancel?: string;
|
||||||
|
selectionId?: string;
|
||||||
/**
|
/**
|
||||||
* Use only to indicate loading or streaming data in the panel.
|
* Use only to indicate loading or streaming data in the panel.
|
||||||
* Any other values of loadingState are ignored.
|
* Any other values of loadingState are ignored.
|
||||||
@ -131,6 +133,7 @@ export function PanelChrome({
|
|||||||
statusMessageOnClick,
|
statusMessageOnClick,
|
||||||
leftItems,
|
leftItems,
|
||||||
actions,
|
actions,
|
||||||
|
selectionId,
|
||||||
onCancelQuery,
|
onCancelQuery,
|
||||||
onOpenMenu,
|
onOpenMenu,
|
||||||
collapsible = false,
|
collapsible = false,
|
||||||
@ -145,6 +148,7 @@ export function PanelChrome({
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const panelContentId = useId();
|
const panelContentId = useId();
|
||||||
const panelTitleId = useId().replace(/:/g, '_');
|
const panelTitleId = useId().replace(/:/g, '_');
|
||||||
|
const { isSelected, onSelect } = useElementSelection(selectionId);
|
||||||
|
|
||||||
const hasHeader = !hoverHeader;
|
const hasHeader = !hoverHeader;
|
||||||
|
|
||||||
@ -263,7 +267,11 @@ export function PanelChrome({
|
|||||||
return (
|
return (
|
||||||
// tabIndex={0} is needed for keyboard accessibility in the plot area
|
// tabIndex={0} is needed for keyboard accessibility in the plot area
|
||||||
<section
|
<section
|
||||||
className={cx(styles.container, { [styles.transparentContainer]: isPanelTransparent })}
|
className={cx(
|
||||||
|
styles.container,
|
||||||
|
isPanelTransparent && styles.transparentContainer,
|
||||||
|
isSelected && 'dashboard-selected-element'
|
||||||
|
)}
|
||||||
style={containerStyles}
|
style={containerStyles}
|
||||||
aria-labelledby={!!title ? panelTitleId : undefined}
|
aria-labelledby={!!title ? panelTitleId : undefined}
|
||||||
data-testid={testid}
|
data-testid={testid}
|
||||||
@ -300,7 +308,12 @@ export function PanelChrome({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{hasHeader && (
|
{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 && (
|
{statusMessage && (
|
||||||
<div className={dragClassCancel}>
|
<div className={dragClassCancel}>
|
||||||
<PanelStatus message={statusMessage} onClick={statusMessageOnClick} ariaLabel="Panel status" />
|
<PanelStatus message={statusMessage} onClick={statusMessageOnClick} ariaLabel="Panel status" />
|
||||||
|
@ -322,3 +322,9 @@ export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types';
|
|||||||
|
|
||||||
export { ZoomPlugin } from '../graveyard/uPlot/plugins/ZoomPlugin';
|
export { ZoomPlugin } from '../graveyard/uPlot/plugins/ZoomPlugin';
|
||||||
export { TooltipPlugin } from '../graveyard/uPlot/plugins/TooltipPlugin';
|
export { TooltipPlugin } from '../graveyard/uPlot/plugins/TooltipPlugin';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ElementSelectionContext,
|
||||||
|
useElementSelection,
|
||||||
|
type ElementSelectionContextState,
|
||||||
|
} from './ElementSelectionContext/ElementSelectionContext';
|
||||||
|
@ -67,5 +67,11 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'.dashboard-selected-element': {
|
||||||
|
outline: `2px dashed ${theme.colors.primary.border}`,
|
||||||
|
outlineOffset: '0px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { getPluginExtensions, isPluginExtensionLink } from '@grafana/runtime';
|
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 { Page } from 'app/core/components/Page/Page';
|
||||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
|
|
||||||
@ -17,8 +17,9 @@ export const TestStuffPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={{ node: node, main: node }}>
|
<Page navModel={{ node: node, main: node }}>
|
||||||
|
<LinkToBasicApp extensionPointId="grafana/sandbox/testing" />
|
||||||
|
<Text variant="h5">Application notifications (toasts) testing</Text>
|
||||||
<Stack>
|
<Stack>
|
||||||
<LinkToBasicApp extensionPointId="grafana/sandbox/testing" />
|
|
||||||
<Button onClick={() => notifyApp.success('Success toast', 'some more text goes here')} variant="primary">
|
<Button onClick={() => notifyApp.success('Success toast', 'some more text goes here')} variant="primary">
|
||||||
Success
|
Success
|
||||||
</Button>
|
</Button>
|
||||||
|
Loading…
Reference in New Issue
Block a user