From 82028349b918e65c21025587d5c6c0e928faf5cf Mon Sep 17 00:00:00 2001 From: Belyakin Alexander Date: Thu, 29 Jan 2026 11:11:37 +0300 Subject: [PATCH 1/2] feat: add configuration for panel header actions visibility Signed-off-by: Belyakin Alexander --- dashboards/src/components/Panel/Panel.tsx | 8 ++ .../src/components/Panel/PanelActions.tsx | 115 +++++++++++++----- .../src/components/Panel/PanelHeader.tsx | 5 +- 3 files changed, 99 insertions(+), 29 deletions(-) diff --git a/dashboards/src/components/Panel/Panel.tsx b/dashboards/src/components/Panel/Panel.tsx index 810484b..5aecc0f 100644 --- a/dashboards/src/components/Panel/Panel.tsx +++ b/dashboards/src/components/Panel/Panel.tsx @@ -18,6 +18,7 @@ import { useDataQueriesContext, usePluginRegistry } from '@perses-dev/plugin-sys import { ReactNode, memo, useMemo, useState, useEffect } from 'react'; import useResizeObserver from 'use-resize-observer'; import { PanelContent } from './PanelContent'; +import type { PanelActionConfig } from './PanelActions'; import { PanelHeader, PanelHeaderProps } from './PanelHeader'; export interface PanelProps extends CardProps<'section'> { @@ -45,6 +46,12 @@ export type PanelOptions = { * It will only be rendered when the panel is in edit mode. */ extra?: (props: PanelExtraProps) => ReactNode; + /** + * Controls which actions are visible in the panel header. + * - undefined: show all actions (default Perses behavior) + * - defined entries: set per-action visibility (false hides) + */ + actions?: PanelActionConfig; }; export type PanelExtraProps = { @@ -234,6 +241,7 @@ export const Panel = memo(function Panel(props: PanelProps) { links={definition.spec.links} pluginActions={pluginActions} showIcons={showIcons} + actions={panelOptions?.actions} sx={{ py: '2px', pl: '8px', pr: '2px' }} dimension={contentDimensions} /> diff --git a/dashboards/src/components/Panel/PanelActions.tsx b/dashboards/src/components/Panel/PanelActions.tsx index 544a609..711bd23 100644 --- a/dashboards/src/components/Panel/PanelActions.tsx +++ b/dashboards/src/components/Panel/PanelActions.tsx @@ -36,7 +36,28 @@ import { } from '../../constants'; import { HeaderIconButton } from './HeaderIconButton'; import { PanelLinks } from './PanelLinks'; -import { PanelOptions } from './Panel'; +import type { PanelOptions } from './Panel'; + +/** + * Constants for panel header actions + */ +export const PANEL_ACTIONS = { + // Info icon showing panel description tooltip + DESCRIPTION: 'description', + // External links dropdown + LINKS: 'links', + // Warning/info notices from query results + NOTICES: 'notices', + // Button to open query inspector dialog + QUERY_INSPECTOR: 'viewQueries', + // Expand/collapse panel to fullscreen + FULLSCREEN: 'fullscreen', + // Custom actions from panel plugins + PLUGIN_ACTIONS: 'pluginActions', +} as const; + +export type PanelActionType = (typeof PANEL_ACTIONS)[keyof typeof PANEL_ACTIONS]; +export type PanelActionConfig = Partial>; const noticeTypeToIcon: Record = { error: , @@ -65,6 +86,7 @@ export interface PanelActionsProps { queryResults: QueryData[]; pluginActions?: ReactNode[]; showIcons: PanelOptions['showIcons']; + actions?: PanelOptions['actions']; } const ConditionalBox = styled(Box)({ @@ -86,8 +108,10 @@ export const PanelActions: React.FC = ({ queryResults, pluginActions = [], showIcons, + actions, }) => { - const descriptionAction = useMemo((): ReactNode | undefined => { + const isVisible = (id: PanelActionType): boolean => actions?.[id] ?? true; + const descriptionAction = useMemo((): ReactNode => { if (description && description.trim().length > 0) { return ( @@ -102,13 +126,24 @@ export const PanelActions: React.FC = ({ ); } - return undefined; + return null; }, [descriptionTooltipId, description]); - const linksAction = links && links.length > 0 && ; - const extraActions = editHandlers === undefined && extra; + const linksAction = useMemo((): ReactNode => { + if (links && links.length > 0) { + return ; + } + return null; + }, [links]); - const queryStateIndicator = useMemo((): ReactNode | undefined => { + const extraActions = useMemo((): ReactNode => { + if (editHandlers === undefined && extra) { + return <>{extra}; + } + return null; + }, [editHandlers, extra]); + + const queryStateIndicator = useMemo((): ReactNode => { const hasData = queryResults.some((q) => q.data); const isFetching = queryResults.some((q) => q.isFetching); const queryErrors = queryResults.filter((q) => q.error); @@ -134,9 +169,10 @@ export const PanelActions: React.FC = ({ ); } + return null; }, [queryResults]); - const noticesIndicator = useMemo(() => { + const noticesIndicator = useMemo((): ReactNode => { const notices = queryResults.flatMap((q) => { return q.data?.metadata?.notices ?? []; }); @@ -152,9 +188,10 @@ export const PanelActions: React.FC = ({ ); } + return null; }, [queryResults]); - const readActions = useMemo((): ReactNode | undefined => { + const readActions = useMemo((): ReactNode => { if (readHandlers !== undefined) { return ( @@ -172,10 +209,10 @@ export const PanelActions: React.FC = ({ ); } - return undefined; + return null; }, [readHandlers, title]); - const viewQueryAction = useMemo(() => { + const viewQueryAction = useMemo((): ReactNode => { if (!viewQueriesHandler?.onClick) return null; return ( @@ -190,9 +227,8 @@ export const PanelActions: React.FC = ({ ); }, [viewQueriesHandler, title]); - const editActions = useMemo((): ReactNode | undefined => { + const editActions = useMemo((): ReactNode => { if (editHandlers !== undefined) { - // If there are edit handlers, always just show the edit buttons return ( <> @@ -232,10 +268,10 @@ export const PanelActions: React.FC = ({ ); } - return undefined; + return null; }, [editHandlers, title]); - const moveAction = useMemo((): ReactNode | undefined => { + const moveAction = useMemo((): ReactNode => { if (editActions && !readHandlers?.isPanelViewed) { return ( @@ -245,9 +281,14 @@ export const PanelActions: React.FC = ({ ); } - return undefined; + return null; }, [editActions, readHandlers, title]); + const renderedPluginActions = useMemo((): ReactNode => { + if (pluginActions.length === 0) return null; + return <>{pluginActions}; + }, [pluginActions]); + const divider = ; // By default, the panel header shows certain icons only on hover if the panel is in non-editing, non-fullscreen mode @@ -265,8 +306,14 @@ export const PanelActions: React.FC = ({ {divider} - {descriptionAction} {linksAction} {queryStateIndicator} {noticesIndicator} {extraActions} {viewQueryAction} - {readActions} {pluginActions} + {isVisible(PANEL_ACTIONS.DESCRIPTION) && descriptionAction} + {isVisible(PANEL_ACTIONS.LINKS) && linksAction} + {queryStateIndicator} + {isVisible(PANEL_ACTIONS.NOTICES) && noticesIndicator} + {extraActions} + {isVisible(PANEL_ACTIONS.QUERY_INSPECTOR) && viewQueryAction} + {isVisible(PANEL_ACTIONS.FULLSCREEN) && readActions} + {isVisible(PANEL_ACTIONS.PLUGIN_ACTIONS) && renderedPluginActions} {editActions} {moveAction} @@ -282,15 +329,19 @@ export const PanelActions: React.FC = ({ })} > - {descriptionAction} {linksAction} + {isVisible(PANEL_ACTIONS.DESCRIPTION) && descriptionAction} + {isVisible(PANEL_ACTIONS.LINKS) && linksAction} - {divider} {queryStateIndicator} - {noticesIndicator} + {divider} + {queryStateIndicator} + {isVisible(PANEL_ACTIONS.NOTICES) && noticesIndicator} {extraActions} - {readActions} + {isVisible(PANEL_ACTIONS.FULLSCREEN) && readActions} - {editActions} {viewQueryAction} {pluginActions} + {editActions} + {isVisible(PANEL_ACTIONS.QUERY_INSPECTOR) && viewQueryAction} + {isVisible(PANEL_ACTIONS.PLUGIN_ACTIONS) && renderedPluginActions} {moveAction} @@ -305,16 +356,24 @@ export const PanelActions: React.FC = ({ })} > - {descriptionAction} {linksAction} + {isVisible(PANEL_ACTIONS.DESCRIPTION) && descriptionAction} + {isVisible(PANEL_ACTIONS.LINKS) && linksAction} - {divider} {queryStateIndicator} - {noticesIndicator} + {divider} + {queryStateIndicator} + {isVisible(PANEL_ACTIONS.NOTICES) && noticesIndicator} {extraActions} - {viewQueryAction} - {readActions} {editActions} + {isVisible(PANEL_ACTIONS.QUERY_INSPECTOR) && viewQueryAction} + {isVisible(PANEL_ACTIONS.FULLSCREEN) && readActions} + {editActions} {/* Show plugin actions inside a menu if it gets crowded */} - {pluginActions.length <= 1 ? pluginActions : {pluginActions}} + {isVisible(PANEL_ACTIONS.PLUGIN_ACTIONS) && renderedPluginActions && + (pluginActions.length <= 1 ? ( + renderedPluginActions + ) : ( + {renderedPluginActions} + ))} {moveAction} diff --git a/dashboards/src/components/Panel/PanelHeader.tsx b/dashboards/src/components/Panel/PanelHeader.tsx index 53237e3..ba06a11 100644 --- a/dashboards/src/components/Panel/PanelHeader.tsx +++ b/dashboards/src/components/Panel/PanelHeader.tsx @@ -32,8 +32,9 @@ export interface PanelHeaderProps extends Omit { viewQueriesHandler?: PanelActionsProps['viewQueriesHandler']; readHandlers?: PanelActionsProps['readHandlers']; editHandlers?: PanelActionsProps['editHandlers']; - pluginActions?: ReactNode[]; // Add pluginActions prop + pluginActions?: ReactNode[]; showIcons: PanelOptions['showIcons']; + actions?: PanelOptions['actions']; dimension?: { width: number }; } @@ -49,6 +50,7 @@ export function PanelHeader({ extra, pluginActions, showIcons, + actions, viewQueriesHandler, dimension, ...rest @@ -103,6 +105,7 @@ export function PanelHeader({ queryResults={queryResults} pluginActions={pluginActions} showIcons={showIcons} + actions={actions} /> } From 51c805ade5e5d19c971e751ab10989a1c8dec70d Mon Sep 17 00:00:00 2001 From: Belyakin Alexander Date: Thu, 29 Jan 2026 12:23:48 +0300 Subject: [PATCH 2/2] lint fix Signed-off-by: Belyakin Alexander --- dashboards/src/components/Panel/PanelActions.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dashboards/src/components/Panel/PanelActions.tsx b/dashboards/src/components/Panel/PanelActions.tsx index 711bd23..7e77bfc 100644 --- a/dashboards/src/components/Panel/PanelActions.tsx +++ b/dashboards/src/components/Panel/PanelActions.tsx @@ -368,7 +368,8 @@ export const PanelActions: React.FC = ({ {isVisible(PANEL_ACTIONS.FULLSCREEN) && readActions} {editActions} {/* Show plugin actions inside a menu if it gets crowded */} - {isVisible(PANEL_ACTIONS.PLUGIN_ACTIONS) && renderedPluginActions && + {isVisible(PANEL_ACTIONS.PLUGIN_ACTIONS) && + renderedPluginActions && (pluginActions.length <= 1 ? ( renderedPluginActions ) : (