Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dashboards/src/components/Panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'> {
Expand Down Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
actions?: PanelActionConfig;
actionsConfig?: PanelActionConfig;

};

export type PanelExtraProps = {
Expand Down Expand Up @@ -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}
/>
Expand Down
115 changes: 87 additions & 28 deletions dashboards/src/components/Panel/PanelActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,28 @@
} 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<Record<PanelActionType, boolean>>;

const noticeTypeToIcon: Record<Notice['type'], ReactNode> = {
error: <AlertCircleIcon color="error" />,
Expand Down Expand Up @@ -65,6 +86,7 @@
queryResults: QueryData[];
pluginActions?: ReactNode[];
showIcons: PanelOptions['showIcons'];
actions?: PanelOptions['actions'];
}

const ConditionalBox = styled(Box)({
Expand All @@ -86,8 +108,10 @@
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 (
<InfoTooltip id={descriptionTooltipId} description={description} enterDelay={100}>
Expand All @@ -102,13 +126,24 @@
</InfoTooltip>
);
}
return undefined;
return null;
}, [descriptionTooltipId, description]);

const linksAction = links && links.length > 0 && <PanelLinks links={links} />;
const extraActions = editHandlers === undefined && extra;
const linksAction = useMemo((): ReactNode => {
if (links && links.length > 0) {
return <PanelLinks links={links} />;
}
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);
Expand All @@ -134,9 +169,10 @@
</InfoTooltip>
);
}
return null;
}, [queryResults]);

const noticesIndicator = useMemo(() => {
const noticesIndicator = useMemo((): ReactNode => {
const notices = queryResults.flatMap((q) => {
return q.data?.metadata?.notices ?? [];
});
Expand All @@ -152,9 +188,10 @@
</InfoTooltip>
);
}
return null;
}, [queryResults]);

const readActions = useMemo((): ReactNode | undefined => {
const readActions = useMemo((): ReactNode => {
if (readHandlers !== undefined) {
return (
<InfoTooltip description={TOOLTIP_TEXT.viewPanel}>
Expand All @@ -172,10 +209,10 @@
</InfoTooltip>
);
}
return undefined;
return null;
}, [readHandlers, title]);

const viewQueryAction = useMemo(() => {
const viewQueryAction = useMemo((): ReactNode => {
if (!viewQueriesHandler?.onClick) return null;
return (
<InfoTooltip description={TOOLTIP_TEXT.queryView}>
Expand All @@ -190,9 +227,8 @@
);
}, [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 (
<>
<InfoTooltip description={TOOLTIP_TEXT.editPanel}>
Expand Down Expand Up @@ -232,10 +268,10 @@
</>
);
}
return undefined;
return null;
}, [editHandlers, title]);

const moveAction = useMemo((): ReactNode | undefined => {
const moveAction = useMemo((): ReactNode => {
if (editActions && !readHandlers?.isPanelViewed) {
return (
<InfoTooltip description={TOOLTIP_TEXT.movePanel}>
Expand All @@ -245,9 +281,14 @@
</InfoTooltip>
);
}
return undefined;
return null;
}, [editActions, readHandlers, title]);

const renderedPluginActions = useMemo((): ReactNode => {
if (pluginActions.length === 0) return null;
return <>{pluginActions}</>;
}, [pluginActions]);

const divider = <Box sx={{ flexGrow: 1 }}></Box>;

// By default, the panel header shows certain icons only on hover if the panel is in non-editing, non-fullscreen mode
Expand All @@ -265,8 +306,14 @@
{divider}
<OnHover>
<OverflowMenu title={title}>
{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}
</OverflowMenu>
{moveAction}
Expand All @@ -282,15 +329,19 @@
})}
>
<OnHover>
{descriptionAction} {linksAction}
{isVisible(PANEL_ACTIONS.DESCRIPTION) && descriptionAction}
{isVisible(PANEL_ACTIONS.LINKS) && linksAction}
</OnHover>
{divider} {queryStateIndicator}
{noticesIndicator}
{divider}
{queryStateIndicator}
{isVisible(PANEL_ACTIONS.NOTICES) && noticesIndicator}
<OnHover>
{extraActions}
{readActions}
{isVisible(PANEL_ACTIONS.FULLSCREEN) && readActions}
<OverflowMenu title={title}>
{editActions} {viewQueryAction} {pluginActions}
{editActions}
{isVisible(PANEL_ACTIONS.QUERY_INSPECTOR) && viewQueryAction}
{isVisible(PANEL_ACTIONS.PLUGIN_ACTIONS) && renderedPluginActions}
</OverflowMenu>
{moveAction}
</OnHover>
Expand All @@ -305,16 +356,24 @@
})}
>
<OnHover>
{descriptionAction} {linksAction}
{isVisible(PANEL_ACTIONS.DESCRIPTION) && descriptionAction}
{isVisible(PANEL_ACTIONS.LINKS) && linksAction}
</OnHover>
{divider} {queryStateIndicator}
{noticesIndicator}
{divider}
{queryStateIndicator}
{isVisible(PANEL_ACTIONS.NOTICES) && noticesIndicator}
<OnHover>
{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 : <OverflowMenu title={title}>{pluginActions}</OverflowMenu>}
{isVisible(PANEL_ACTIONS.PLUGIN_ACTIONS) && renderedPluginActions &&

Check failure on line 371 in dashboards/src/components/Panel/PanelActions.tsx

View workflow job for this annotation

GitHub Actions / lint-npm

Insert `⏎···········`
(pluginActions.length <= 1 ? (
renderedPluginActions
) : (
<OverflowMenu title={title}>{renderedPluginActions}</OverflowMenu>
))}
{moveAction}
</OnHover>
</ConditionalBox>
Expand Down
5 changes: 4 additions & 1 deletion dashboards/src/components/Panel/PanelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ export interface PanelHeaderProps extends Omit<CardHeaderProps, OmittedProps> {
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 };
}

Expand All @@ -49,6 +50,7 @@ export function PanelHeader({
extra,
pluginActions,
showIcons,
actions,
viewQueriesHandler,
dimension,
...rest
Expand Down Expand Up @@ -103,6 +105,7 @@ export function PanelHeader({
queryResults={queryResults}
pluginActions={pluginActions}
showIcons={showIcons}
actions={actions}
/>
</Stack>
}
Expand Down
Loading