From 335241d93d54c1b026be67ddc2e991749d118450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 Nov 2024 15:06:00 +0100 Subject: [PATCH] ElementSelection: New element selection context to support selecting elements (#97029) * ElementSelection: New element selction context to support selecting elements like panels * Update * Update --- .betterer.results | 3 +- .../ElementSelectionContext.tsx | 52 +++++++++++++++++++ .../components/PanelChrome/PanelChrome.tsx | 17 +++++- packages/grafana-ui/src/components/index.ts | 6 +++ .../src/themes/GlobalStyles/dashboardGrid.ts | 6 +++ public/app/features/sandbox/TestStuffPage.tsx | 5 +- 6 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 packages/grafana-ui/src/components/ElementSelectionContext/ElementSelectionContext.tsx diff --git a/.betterer.results b/.betterer.results index dc61ba89ee0..7b4ddd5ca1f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3762,7 +3762,8 @@ exports[`better eslint`] = { "public/app/features/sandbox/TestStuffPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], "public/app/features/scopes/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./instance\`)", "0"], diff --git a/packages/grafana-ui/src/components/ElementSelectionContext/ElementSelectionContext.tsx b/packages/grafana-ui/src/components/ElementSelectionContext/ElementSelectionContext.tsx new file mode 100644 index 00000000000..2e6b1b9fd46 --- /dev/null +++ b/packages/grafana-ui/src/components/ElementSelectionContext/ElementSelectionContext.tsx @@ -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(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( + (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 }; +} diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 14da2a42c06..45d597378c4 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -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
+
{statusMessage && (
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index a88b49db681..ec327ebf293 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts index 6fd77b0e77e..2e4ef8924de 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/dashboardGrid.ts @@ -67,5 +67,11 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) { }, }, }, + + '.dashboard-selected-element': { + outline: `2px dashed ${theme.colors.primary.border}`, + outlineOffset: '0px', + borderRadius: '2px', + }, }); } diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx index 36368f5b508..19672e36f2b 100644 --- a/public/app/features/sandbox/TestStuffPage.tsx +++ b/public/app/features/sandbox/TestStuffPage.tsx @@ -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 ( + + Application notifications (toasts) testing -