draft: actions dropdown, need to clean up

This commit is contained in:
Alex Spencer
2025-11-14 07:54:58 -08:00
parent 2bd7d5ddab
commit e2f99e10b2
3 changed files with 115 additions and 62 deletions
@@ -24,6 +24,7 @@ export interface QueryOperationRowProps {
collapsable?: boolean;
disabled?: boolean;
expanderMessages?: ExpanderMessages;
isFocused?: boolean;
}
export type QueryOperationRowRenderProp = ((props: QueryOperationRowRenderProps) => React.ReactNode) | React.ReactNode;
@@ -48,6 +49,7 @@ export function QueryOperationRow({
index,
id,
expanderMessages,
isFocused = false,
}: QueryOperationRowProps) {
const [isContentVisible, setIsContentVisible] = useState(isOpen !== undefined ? isOpen : true);
const styles = useStyles2(getQueryOperationRowStyles);
@@ -127,6 +129,7 @@ export function QueryOperationRow({
reportDragMousePosition={reportDragMousePosition}
title={title}
expanderMessages={expanderMessages}
isFocused={isFocused}
/>
</div>
{isContentVisible && <div className={styles.content}>{children}</div>}
@@ -152,6 +155,7 @@ export function QueryOperationRow({
reportDragMousePosition={reportDragMousePosition}
title={title}
expanderMessages={expanderMessages}
isFocused={isFocused}
/>
{isContentVisible && <div className={styles.content}>{children}</div>}
</div>
@@ -20,6 +20,7 @@ export interface QueryOperationRowHeaderProps {
title?: string;
id: string;
expanderMessages?: ExpanderMessages;
isFocused?: boolean;
}
export interface ExpanderMessages {
@@ -40,6 +41,7 @@ export const QueryOperationRowHeader = ({
title,
id,
expanderMessages,
isFocused = false,
}: QueryOperationRowHeaderProps) => {
const styles = useStyles2(getStyles);
@@ -55,7 +57,7 @@ export const QueryOperationRowHeader = ({
const dragAndDropLabel = t('query-operation.header.drag-and-drop', 'Drag and drop to reorder');
return (
<div className={styles.header}>
<div className={cx(styles.header, isFocused && styles.focused)}>
<div className={styles.column}>
{collapsable && (
<IconButton
@@ -107,6 +109,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
outline: 'none',
},
}),
focused: css({
border: `2px solid ${theme.colors.primary.border}`,
}),
column: css({
label: 'Column',
display: 'flex',
@@ -23,7 +23,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv, renderLimitedComponents, reportInteraction, usePluginComponents } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { Badge, ErrorBoundaryAlert, List, Text } from '@grafana/ui';
import { Badge, Button, Dropdown, ErrorBoundaryAlert, Icon, List, Menu, Stack, Text, TextLink } from '@grafana/ui';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
import {
QueryOperationAction,
@@ -410,38 +410,92 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
const isUnifiedAlerting = app === CoreApp.UnifiedAlerting;
const isExpressionQuery = query.datasource?.uid === ExpressionDatasourceUID;
// Build menu items for the actions dropdown
const menuItems: ReactNode[] = [];
// Saved query buttons (if applicable) - keep separate as it returns complex ReactNode
const savedQueryButtons =
!isEditingQueryLibrary && !isUnifiedAlerting && !isExpressionQuery ? (
<SavedQueryButtons
query={{
...query,
datasource: datasource ? { uid: datasource.uid, type: datasource.type } : query.datasource,
}}
app={app}
onUpdateSuccess={this.onExitQueryLibraryEditingMode}
onSelectQuery={this.onSelectQueryFromLibrary}
datasourceFilters={datasource?.name ? [datasource.name] : []}
/>
) : null;
// Data source help (toggle action)
if (hasEditorHelp) {
menuItems.push(
<Menu.Item
key="datasource-help"
label={
showingHelp
? t('query-operation.header.hide-datasource-help', 'Hide data source help')
: t('query-operation.header.datasource-help', 'Show data source help')
}
icon="question-circle"
onClick={this.onToggleHelp}
active={showingHelp}
/>
);
}
// Duplicate query
if (!isEditingQueryLibrary) {
menuItems.push(
<Menu.Item
key="duplicate-query"
label={t('query-operation.header.duplicate-query', 'Duplicate query')}
icon="copy"
onClick={this.onCopyQuery}
/>
);
}
// Focus query
if (onFocusQuery) {
menuItems.push(
<Menu.Item
key="focus-query"
label={
isFocused
? t('query-operation.header.collapse', 'Show all queries')
: t('query-operation.header.focus', 'Focus query')
}
icon={isFocused ? 'compress-screen' : 'expand-screen'}
onClick={onFocusQuery}
active={Boolean(isFocused)}
testId={selectors.components.QueryEditorRow.actionButton('Focus query')}
/>
);
}
// Extra actions (warnings, badges, etc.) - keep separate as they return complex ReactNodes
const extraActions = this.renderExtraActions();
// Only render dropdown if there are menu items
const actionsDropdown =
menuItems.length > 0 ? (
<Dropdown overlay={<Menu>{menuItems}</Menu>} placement="bottom-end">
<Button
icon="ellipsis-v"
variant="secondary"
size="sm"
aria-label={t('query-operation.header.actions-menu', 'Query actions menu')}
data-testid={selectors.components.QueryEditorRow.actionButton('Actions menu')}
/>
</Dropdown>
) : null;
return (
<>
{!isEditingQueryLibrary && !isUnifiedAlerting && !isExpressionQuery && (
<SavedQueryButtons
query={{
...query,
datasource: datasource ? { uid: datasource.uid, type: datasource.type } : query.datasource,
}}
app={app}
onUpdateSuccess={this.onExitQueryLibraryEditingMode}
onSelectQuery={this.onSelectQueryFromLibrary}
datasourceFilters={datasource?.name ? [datasource.name] : []}
/>
)}
{hasEditorHelp && (
<QueryOperationToggleAction
title={t('query-operation.header.datasource-help', 'Show data source help')}
icon="question-circle"
onClick={this.onToggleHelp}
active={showingHelp}
/>
)}
{this.renderExtraActions()}
{!isEditingQueryLibrary && (
<QueryOperationAction
title={t('query-operation.header.duplicate-query', 'Duplicate query')}
icon="copy"
onClick={this.onCopyQuery}
/>
)}
{savedQueryButtons}
{extraActions}
{!hideHideQueryButton ? (
<QueryOperationToggleAction
dataTestId={selectors.components.QueryEditorRow.actionButton('Hide response')}
@@ -462,19 +516,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
onClick={this.onRemoveQuery}
/>
)}
{onFocusQuery && (
<QueryOperationToggleAction
dataTestId={selectors.components.QueryEditorRow.actionButton('Focus query')}
title={
isFocused
? t('query-operation.header.collapse', 'Show all queries')
: t('query-operation.header.focus', 'Focus query')
}
icon={isFocused ? 'compress-screen' : 'expand-screen'}
active={Boolean(isFocused)}
onClick={onFocusQuery}
/>
)}
{actionsDropdown}
</>
);
};
@@ -501,6 +543,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
render() {
const {
query,
queries,
index,
visualization,
collapsable,
@@ -511,6 +554,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
queryLibraryRef,
onCancelQueryLibraryEdit,
isFocused,
onFocusQuery,
} = this.props;
const { datasource, showingHelp, data } = this.state;
const isHidden = query.hide;
@@ -542,6 +586,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
actions={hideActionButtons ? undefined : this.renderActions}
isOpen={isOpen}
onOpen={onQueryOpenChanged}
isFocused={isFocused}
>
<div className={rowClasses} id={this.id}>
<ErrorBoundaryAlert boundaryName="query-editor-operation-row">
@@ -562,8 +607,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
</QueryOperationRow>
);
const { queries } = this.props;
const hiddenQueriesCount = queries.length - 1; // Total queries minus the focused one
const hiddenQueriesCount = Math.max(0, queries.length - 1); // Total queries minus the focused one
return (
<div
@@ -571,22 +615,22 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
aria-label={selectors.components.QueryEditorRows.rows}
className={focusedWrapperStyle}
>
{isFocused && (
{isFocused && hiddenQueriesCount > 0 && (
<div className={getFocusedBannerStyle()}>
<Text color="primary" variant="bodySmall" italic>
<Trans
i18nKey="query.query-editor-row.focused-message"
values={{ queryName: query.refId, count: hiddenQueriesCount }}
>
Query {query.refId} is focused, {hiddenQueriesCount}{' '}
{hiddenQueriesCount === 1 ? (
<Trans i18nKey="query.query-editor-row.focused-singular">query is</Trans>
) : (
<Trans i18nKey="query.query-editor-row.focused-plural">all other queries are</Trans>
)}{' '}
hidden from view.
</Trans>
</Text>
<Stack direction="row" alignItems="center" gap={1}>
<Icon name="expand-screen" />
<Text color="primary" variant="bodySmall" italic>
<Trans
i18nKey="query.query-editor-row.focused-message"
values={{ queryName: query.refId, count: hiddenQueriesCount }}
>
Query {query.refId} is focused, {'{{count}}'} queries are hidden from view.
</Trans>
</Text>
<Button fill="text" size="sm" onClick={onFocusQuery}>
<Trans i18nKey="query-operation.header.collapse">Show all queries</Trans>
</Button>
</Stack>
</div>
)}
{queryLibraryRef && (