Compare commits
12 Commits
sriram/SQL
...
alexspence
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6424cc1d8d | ||
|
|
4e7e11b906 | ||
|
|
6630e37f31 | ||
|
|
4818b227b4 | ||
|
|
ed27956dd0 | ||
|
|
b7aaf00c6c | ||
|
|
21d9b0ddcf | ||
|
|
4badb645ba | ||
|
|
d912dbfc8f | ||
|
|
e2f99e10b2 | ||
|
|
2bd7d5ddab | ||
|
|
f459b36026 |
@@ -40,10 +40,13 @@ test.describe(
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(1);
|
||||
|
||||
// Duplicate refId B
|
||||
// Open the actions menu
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Duplicate query'))
|
||||
.getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Actions menu'))
|
||||
.first()
|
||||
.click();
|
||||
// Click duplicate query in the menu
|
||||
await page.getByText('Duplicate query').click();
|
||||
|
||||
// We expect row with refId B and A to exist and be visible
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2);
|
||||
|
||||
@@ -35,7 +35,10 @@ describe('Panel edit tests - queries', () => {
|
||||
e2e.components.QueryEditorRows.rows({ timeout: flakyTimeout }).should('have.length', 1);
|
||||
|
||||
// Duplicate refId B
|
||||
e2e.components.QueryEditorRow.actionButton('Duplicate query').eq(0).should('be.visible').click();
|
||||
// Open the actions menu
|
||||
e2e.components.QueryEditorRow.actionButton('Actions menu').eq(0).should('be.visible').click();
|
||||
// Click duplicate query in the menu
|
||||
cy.contains('Duplicate query').should('be.visible').click();
|
||||
|
||||
// We expect row with refId Band and A to exist and be visible
|
||||
e2e.components.QueryEditorRows.rows().should('have.length', 2);
|
||||
|
||||
@@ -41,6 +41,7 @@ export const availableIconsIndex = {
|
||||
asserts: true,
|
||||
'expand-arrows': true,
|
||||
'expand-arrows-alt': true,
|
||||
'expand-screen': true,
|
||||
at: true,
|
||||
ai: true,
|
||||
backward: true,
|
||||
@@ -85,6 +86,7 @@ export const availableIconsIndex = {
|
||||
'comments-alt': true,
|
||||
compass: true,
|
||||
'compress-arrows': true,
|
||||
'compress-screen': true,
|
||||
copy: true,
|
||||
'corner-up-left': true,
|
||||
'corner-up-right': true,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface QueryOperationRowProps {
|
||||
collapsable?: boolean;
|
||||
disabled?: boolean;
|
||||
expanderMessages?: ExpanderMessages;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
export type QueryOperationRowRenderProp = ((props: QueryOperationRowRenderProps) => React.ReactNode) | React.ReactNode;
|
||||
@@ -48,6 +49,7 @@ export function QueryOperationRow({
|
||||
index,
|
||||
id,
|
||||
expanderMessages,
|
||||
highlight = 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}
|
||||
highlight={highlight}
|
||||
/>
|
||||
</div>
|
||||
{isContentVisible && <div className={styles.content}>{children}</div>}
|
||||
@@ -152,6 +155,7 @@ export function QueryOperationRow({
|
||||
reportDragMousePosition={reportDragMousePosition}
|
||||
title={title}
|
||||
expanderMessages={expanderMessages}
|
||||
highlight={highlight}
|
||||
/>
|
||||
{isContentVisible && <div className={styles.content}>{children}</div>}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface QueryOperationRowHeaderProps {
|
||||
title?: string;
|
||||
id: string;
|
||||
expanderMessages?: ExpanderMessages;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
export interface ExpanderMessages {
|
||||
@@ -40,6 +41,7 @@ export const QueryOperationRowHeader = ({
|
||||
title,
|
||||
id,
|
||||
expanderMessages,
|
||||
highlight = 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, { [styles.highlighted]: highlight })}>
|
||||
<div className={styles.column}>
|
||||
{collapsable && (
|
||||
<IconButton
|
||||
@@ -107,6 +109,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
outline: 'none',
|
||||
},
|
||||
}),
|
||||
highlighted: css({
|
||||
border: `2px solid ${theme.colors.primary.border}`,
|
||||
}),
|
||||
column: css({
|
||||
label: 'Column',
|
||||
display: 'flex',
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"unicons/comment-alt",
|
||||
"unicons/comment-alt-share",
|
||||
"unicons/comments-alt",
|
||||
"unicons/compress-screen",
|
||||
"unicons/compass",
|
||||
"unicons/copy",
|
||||
"unicons/corner-down-right-alt",
|
||||
@@ -73,6 +74,7 @@
|
||||
"unicons/exclamation-circle",
|
||||
"unicons/exclamation-triangle",
|
||||
"unicons/external-link-alt",
|
||||
"unicons/expand-screen",
|
||||
"unicons/eye",
|
||||
"unicons/eye-slash",
|
||||
"unicons/file-alt",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
SceneDataQuery,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
import { Button, Stack, Tab } from '@grafana/ui';
|
||||
import { Button, Stack, Tab, useStyles2 } from '@grafana/ui';
|
||||
import { addQuery } from 'app/core/utils/query';
|
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
@@ -46,6 +47,7 @@ interface PanelDataQueriesTabState extends SceneObjectState {
|
||||
datasource?: DataSourceApi;
|
||||
dsSettings?: DataSourceInstanceSettings;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
hasFocusedQuery?: boolean;
|
||||
}
|
||||
export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabState> implements PanelDataPaneTab {
|
||||
static Component = PanelDataQueriesTabRendered;
|
||||
@@ -93,7 +95,7 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
|
||||
// do we have a last used datasource for this dashboard
|
||||
if (lastUsedDatasource?.datasourceUid !== null) {
|
||||
// get datasource from dashbopard uid
|
||||
// get datasource from dashboard uid
|
||||
dsSettings = getDataSourceSrv().getInstanceSettings({ uid: lastUsedDatasource?.datasourceUid });
|
||||
if (dsSettings) {
|
||||
datasource = await getDataSourceSrv().get({
|
||||
@@ -250,6 +252,10 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
this.queryRunner.runQueries();
|
||||
};
|
||||
|
||||
public onFocusQuery = (hasFocusedQuery: boolean) => {
|
||||
this.setState({ hasFocusedQuery });
|
||||
};
|
||||
|
||||
public getQueries() {
|
||||
return this.queryRunner.state.queries;
|
||||
}
|
||||
@@ -335,9 +341,10 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
}
|
||||
|
||||
export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
|
||||
const { datasource, dsSettings } = model.useState();
|
||||
const { datasource, dsSettings, hasFocusedQuery } = model.useState();
|
||||
const { data, queries } = model.queryRunner.useState();
|
||||
const { openDrawer: openQueryLibraryDrawer, queryLibraryEnabled } = useQueryLibraryContext();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const handleAddExpression = useCallback(
|
||||
(type: ExpressionQueryType) => {
|
||||
@@ -355,8 +362,6 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
|
||||
return null;
|
||||
}
|
||||
|
||||
const showAddButton = !isSharedDashboardQuery(dsSettings.name);
|
||||
|
||||
const onSelectQueryFromLibrary = async (query: DataQuery) => {
|
||||
// ensure all queries explicitly define a datasource
|
||||
const enrichedQueries = queries.map((q) =>
|
||||
@@ -382,68 +387,80 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
|
||||
}
|
||||
};
|
||||
|
||||
const canAddQueries = !isSharedDashboardQuery(dsSettings.name);
|
||||
const canAddExpressions = config.expressionsEnabled && model.isExpressionsSupported(dsSettings);
|
||||
const showActionButtons = !hasFocusedQuery;
|
||||
|
||||
return (
|
||||
<div data-testid={selectors.components.QueryTab.content}>
|
||||
<QueryGroupTopSection
|
||||
data={data}
|
||||
dsSettings={dsSettings}
|
||||
dataSource={datasource}
|
||||
options={model.buildQueryOptions()}
|
||||
onDataSourceChange={model.onChangeDataSource}
|
||||
onOptionsChange={model.onQueryOptionsChange}
|
||||
onOpenQueryInspector={model.onOpenInspector}
|
||||
/>
|
||||
<div
|
||||
data-testid={selectors.components.QueryTab.content}
|
||||
className={cx(styles.container, { [styles.focused]: hasFocusedQuery })}
|
||||
>
|
||||
{!hasFocusedQuery && (
|
||||
<QueryGroupTopSection
|
||||
data={data}
|
||||
dsSettings={dsSettings}
|
||||
dataSource={datasource}
|
||||
options={model.buildQueryOptions()}
|
||||
onDataSourceChange={model.onChangeDataSource}
|
||||
onOptionsChange={model.onQueryOptionsChange}
|
||||
onOpenQueryInspector={model.onOpenInspector}
|
||||
/>
|
||||
)}
|
||||
|
||||
<QueryEditorRows
|
||||
data={data}
|
||||
queries={queries}
|
||||
dsSettings={dsSettings}
|
||||
onAddQuery={model.onAddQuery}
|
||||
onFocusQuery={model.onFocusQuery}
|
||||
onQueriesChange={model.onQueriesChange}
|
||||
onRunQueries={model.onRunQueries}
|
||||
onUpdateDatasources={queryLibraryEnabled ? model.updateDatasourceIfNeeded : undefined}
|
||||
app={CoreApp.PanelEditor}
|
||||
/>
|
||||
|
||||
<Stack gap={2}>
|
||||
{showAddButton && (
|
||||
<>
|
||||
<Button
|
||||
icon="plus"
|
||||
onClick={model.addQueryClick}
|
||||
variant="secondary"
|
||||
data-testid={selectors.components.QueryTab.addQuery}
|
||||
>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.add-query">Add query</Trans>
|
||||
</Button>
|
||||
{queryLibraryEnabled && (
|
||||
{showActionButtons && (
|
||||
<Stack gap={2}>
|
||||
{canAddQueries && (
|
||||
<>
|
||||
<Button
|
||||
icon="plus"
|
||||
onClick={() =>
|
||||
openQueryLibraryDrawer({
|
||||
onSelectQuery: onSelectQueryFromLibrary,
|
||||
options: {
|
||||
context: CoreApp.PanelEditor,
|
||||
},
|
||||
})
|
||||
}
|
||||
onClick={model.addQueryClick}
|
||||
variant="secondary"
|
||||
data-testid={selectors.components.QueryTab.addQueryFromLibrary}
|
||||
data-testid={selectors.components.QueryTab.addQuery}
|
||||
>
|
||||
<Trans i18nKey={'dashboards.panel-queries.add-from-saved-queries'}>Add from saved queries</Trans>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.add-query">Add query</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && (
|
||||
<ExpressionTypeDropdown handleOnSelect={handleAddExpression}>
|
||||
<Button icon="plus" variant="secondary" data-testid={selectors.components.QueryTab.addExpression}>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.expression">Expression </Trans>
|
||||
</Button>
|
||||
</ExpressionTypeDropdown>
|
||||
)}
|
||||
{model.renderExtraActions()}
|
||||
</Stack>
|
||||
{queryLibraryEnabled && (
|
||||
<Button
|
||||
icon="plus"
|
||||
onClick={() =>
|
||||
openQueryLibraryDrawer({
|
||||
onSelectQuery: onSelectQueryFromLibrary,
|
||||
options: {
|
||||
context: CoreApp.PanelEditor,
|
||||
},
|
||||
})
|
||||
}
|
||||
variant="secondary"
|
||||
data-testid={selectors.components.QueryTab.addQueryFromLibrary}
|
||||
>
|
||||
<Trans i18nKey={'dashboards.panel-queries.add-from-saved-queries'}>Add from saved queries</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canAddExpressions && (
|
||||
<ExpressionTypeDropdown handleOnSelect={handleAddExpression}>
|
||||
<Button icon="plus" variant="secondary" data-testid={selectors.components.QueryTab.addExpression}>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.expression">Expression </Trans>
|
||||
</Button>
|
||||
</ExpressionTypeDropdown>
|
||||
)}
|
||||
{model.renderExtraActions()}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -467,3 +484,14 @@ function QueriesTab(props: QueriesTabProps) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => ({
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
focused: css({
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -85,9 +85,13 @@ describe('Explore QueryRows', () => {
|
||||
// waiting for the d&d component to fully render.
|
||||
await screen.findAllByText('someDs query editor');
|
||||
|
||||
let duplicateButton = screen.getByLabelText(/Duplicate query/i);
|
||||
// Open the actions menu
|
||||
const actionsMenuButton = screen.getByLabelText(/Query actions menu/i);
|
||||
fireEvent.click(actionsMenuButton);
|
||||
|
||||
fireEvent.click(duplicateButton);
|
||||
// Click duplicate query in the menu
|
||||
const duplicateMenuItem = await screen.findByText(/Duplicate query/i);
|
||||
fireEvent.click(duplicateMenuItem);
|
||||
|
||||
// We should have another row with refId B
|
||||
expect(await screen.findByLabelText('Query editor row title B')).toBeInTheDocument();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import classNames from 'classnames';
|
||||
import { cloneDeep, filter, uniqBy, uniqueId } from 'lodash';
|
||||
import pluralize from 'pluralize';
|
||||
@@ -22,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 } from '@grafana/ui';
|
||||
import { Badge, Button, Dropdown, ErrorBoundaryAlert, Icon, List, Menu, Stack, Text } from '@grafana/ui';
|
||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||
import {
|
||||
QueryOperationAction,
|
||||
@@ -73,6 +74,8 @@ export interface Props<TQuery extends DataQuery> {
|
||||
queryLibraryRef?: string;
|
||||
onCancelQueryLibraryEdit?: () => void;
|
||||
isOpen?: boolean;
|
||||
isFocused?: boolean;
|
||||
onFocusQuery?: () => void;
|
||||
}
|
||||
|
||||
interface State<TQuery extends DataQuery> {
|
||||
@@ -130,9 +133,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
datasource = await this.dataSourceSrv.get();
|
||||
}
|
||||
|
||||
if (typeof this.props.onDataSourceLoaded === 'function') {
|
||||
this.props.onDataSourceLoaded(datasource);
|
||||
}
|
||||
this.props.onDataSourceLoaded?.(datasource);
|
||||
|
||||
this.setState({
|
||||
datasource: datasource as unknown as DataSourceApi<TQuery>,
|
||||
@@ -242,10 +243,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
}
|
||||
|
||||
onRemoveQuery(query);
|
||||
|
||||
if (onQueryRemoved) {
|
||||
onQueryRemoved();
|
||||
}
|
||||
onQueryRemoved?.();
|
||||
};
|
||||
|
||||
onCancelQueryLibraryEdit = () => {
|
||||
@@ -265,20 +263,14 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
const { query, onAddQuery, onQueryCopied } = this.props;
|
||||
const copy = cloneDeep(query);
|
||||
onAddQuery(copy);
|
||||
|
||||
if (onQueryCopied) {
|
||||
onQueryCopied();
|
||||
}
|
||||
onQueryCopied?.();
|
||||
};
|
||||
|
||||
onHideQuery = () => {
|
||||
const { query, onChange, onRunQuery, onQueryToggled } = this.props;
|
||||
onChange({ ...query, hide: !query.hide });
|
||||
onRunQuery();
|
||||
|
||||
if (onQueryToggled) {
|
||||
onQueryToggled(query.hide);
|
||||
}
|
||||
onQueryToggled?.(query.hide);
|
||||
|
||||
reportInteraction('query_editor_row_hide_query_clicked', {
|
||||
hide: !query.hide,
|
||||
@@ -397,49 +389,109 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
return extraActions;
|
||||
};
|
||||
|
||||
renderActions = (props: QueryOperationRowRenderProps) => {
|
||||
const { query, hideHideQueryButton: hideHideQueryButton = false, queryLibraryRef, app } = this.props;
|
||||
buildMenuItems = (): ReactNode[] => {
|
||||
const { isFocused, onFocusQuery } = this.props;
|
||||
const { datasource, showingHelp } = this.state;
|
||||
const isHidden = !!query.hide;
|
||||
|
||||
const hasEditorHelp = datasource?.components?.QueryEditorHelp;
|
||||
const isEditingQueryLibrary = queryLibraryRef !== undefined;
|
||||
const isEditingQueryLibrary = this.props.queryLibraryRef !== undefined;
|
||||
|
||||
return [
|
||||
// Data source help
|
||||
hasEditorHelp && (
|
||||
<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
|
||||
!isEditingQueryLibrary && (
|
||||
<Menu.Item
|
||||
key="duplicate-query"
|
||||
label={t('query-operation.header.duplicate-query', 'Duplicate query')}
|
||||
icon="copy"
|
||||
onClick={this.onCopyQuery}
|
||||
/>
|
||||
),
|
||||
// Focus query
|
||||
onFocusQuery && (
|
||||
<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')}
|
||||
/>
|
||||
),
|
||||
].filter((item): item is JSX.Element => Boolean(item));
|
||||
};
|
||||
|
||||
renderSavedQueryButtons = (): ReactNode => {
|
||||
const { query, app, queryLibraryRef } = this.props;
|
||||
const { datasource } = this.state;
|
||||
const isUnifiedAlerting = app === CoreApp.UnifiedAlerting;
|
||||
const isExpressionQuery = query.datasource?.uid === ExpressionDatasourceUID;
|
||||
const isEditingQueryLibrary = queryLibraryRef !== undefined;
|
||||
|
||||
if (isEditingQueryLibrary || isUnifiedAlerting || isExpressionQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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] : []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderActions = () => {
|
||||
const { query, hideHideQueryButton = false, queryLibraryRef } = this.props;
|
||||
const isHidden = !!query.hide;
|
||||
const isEditingQueryLibrary = queryLibraryRef !== undefined;
|
||||
|
||||
// Build all action components
|
||||
const savedQueryButtons = this.renderSavedQueryButtons();
|
||||
const extraActions = this.renderExtraActions();
|
||||
const menuItems = this.buildMenuItems();
|
||||
|
||||
// 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"
|
||||
fill="text"
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hideHideQueryButton ? (
|
||||
{savedQueryButtons}
|
||||
{extraActions}
|
||||
{!hideHideQueryButton && (
|
||||
<QueryOperationToggleAction
|
||||
dataTestId={selectors.components.QueryEditorRow.actionButton('Hide response')}
|
||||
title={
|
||||
@@ -451,7 +503,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
active={isHidden}
|
||||
onClick={this.onHideQuery}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
{!isEditingQueryLibrary && (
|
||||
<QueryOperationAction
|
||||
title={t('query-operation.header.remove-query', 'Remove query')}
|
||||
@@ -459,6 +511,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
onClick={this.onRemoveQuery}
|
||||
/>
|
||||
)}
|
||||
{actionsDropdown}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -485,6 +538,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
render() {
|
||||
const {
|
||||
query,
|
||||
queries,
|
||||
index,
|
||||
visualization,
|
||||
collapsable,
|
||||
@@ -494,14 +548,18 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
app,
|
||||
queryLibraryRef,
|
||||
onCancelQueryLibraryEdit,
|
||||
isFocused,
|
||||
onFocusQuery,
|
||||
} = this.props;
|
||||
const { datasource, showingHelp, data } = this.state;
|
||||
const isHidden = query.hide;
|
||||
const error =
|
||||
data?.error && data.error.refId === query.refId ? data.error : data?.errors?.find((e) => e.refId === query.refId);
|
||||
// Note: We can't use hooks in class components, so we use static styles
|
||||
const rowClasses = classNames('query-editor-row', {
|
||||
'query-editor-row--disabled': isHidden,
|
||||
'gf-form-disabled': isHidden,
|
||||
[focusedRowStyle]: isFocused,
|
||||
});
|
||||
|
||||
if (!datasource) {
|
||||
@@ -521,6 +579,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
actions={hideActionButtons ? undefined : this.renderActions}
|
||||
isOpen={isOpen}
|
||||
onOpen={onQueryOpenChanged}
|
||||
highlight={isFocused}
|
||||
>
|
||||
<div className={rowClasses} id={this.id}>
|
||||
<ErrorBoundaryAlert boundaryName="query-editor-operation-row">
|
||||
@@ -541,8 +600,32 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
</QueryOperationRow>
|
||||
);
|
||||
|
||||
const hiddenQueriesCount = Math.max(0, queries.length - 1); // Total queries minus the focused one
|
||||
|
||||
return (
|
||||
<div data-testid="query-editor-row" aria-label={selectors.components.QueryEditorRows.rows}>
|
||||
<div
|
||||
data-testid="query-editor-row"
|
||||
aria-label={selectors.components.QueryEditorRows.rows}
|
||||
className={classNames({ [focusedWrapperStyle]: isFocused })}
|
||||
>
|
||||
{isFocused && hiddenQueriesCount > 0 && (
|
||||
<div className={focusedBannerStyle}>
|
||||
<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 {'{{queryName}}'} 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 && (
|
||||
<MaybeQueryLibraryEditingHeader
|
||||
query={query}
|
||||
@@ -659,3 +742,26 @@ function AdaptiveTelemetryQueryActions({ query }: { query: DataQuery }) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Static styles for focused state - used in class component
|
||||
// Transitions are handled in parent components where we have theme access
|
||||
const focusedWrapperStyle = css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
});
|
||||
|
||||
const focusedRowStyle = css({
|
||||
flex: '1 1 100%',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// Remove margins when focused to maximize space
|
||||
marginBottom: 0,
|
||||
});
|
||||
|
||||
const focusedBannerStyle = css({
|
||||
marginBottom: '10px',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fireEvent, queryByLabelText, render, screen, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, queryByLabelText, render, screen, waitFor, within } from '@testing-library/react';
|
||||
|
||||
import type { DataSourceApi } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import type { DataSourceSrv, GetDataSourceListFilters } from '@grafana/runtime';
|
||||
import { DataSourceRef, type DataQuery } from '@grafana/schema';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
@@ -251,13 +252,18 @@ describe('QueryEditorRows', () => {
|
||||
|
||||
renderScenario({ onAddQuery, onQueryCopied });
|
||||
const queryEditorRows = await screen.findAllByTestId('query-editor-row');
|
||||
queryEditorRows.map(async (childQuery) => {
|
||||
const duplicateQueryButton = queryByLabelText(childQuery, 'Duplicate query') as HTMLElement;
|
||||
|
||||
expect(duplicateQueryButton).toBeInTheDocument();
|
||||
for (const childQuery of queryEditorRows) {
|
||||
// Open the actions menu
|
||||
const actionsMenuButton = queryByLabelText(childQuery, 'Query actions menu') as HTMLElement;
|
||||
expect(actionsMenuButton).toBeInTheDocument();
|
||||
fireEvent.click(actionsMenuButton);
|
||||
|
||||
fireEvent.click(duplicateQueryButton);
|
||||
});
|
||||
// Click duplicate query in the menu
|
||||
const duplicateMenuItem = await screen.findByText('Duplicate query');
|
||||
expect(duplicateMenuItem).toBeInTheDocument();
|
||||
fireEvent.click(duplicateMenuItem);
|
||||
}
|
||||
|
||||
expect(onAddQuery).toHaveBeenCalledTimes(queryEditorRows.length);
|
||||
expect(onQueryCopied).toHaveBeenCalledTimes(queryEditorRows.length);
|
||||
@@ -269,13 +275,15 @@ describe('QueryEditorRows', () => {
|
||||
renderScenario({ onQueriesChange, onQueryRemoved });
|
||||
|
||||
const queryEditorRows = await screen.findAllByTestId('query-editor-row');
|
||||
queryEditorRows.map(async (childQuery) => {
|
||||
const deleteQueryButton = queryByLabelText(childQuery, 'Remove query') as HTMLElement;
|
||||
for (const childQuery of queryEditorRows) {
|
||||
const deleteQueryButton = within(childQuery).getByTestId(
|
||||
selectors.components.QueryEditorRow.actionButton('Remove query')
|
||||
);
|
||||
|
||||
expect(deleteQueryButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(deleteQueryButton);
|
||||
});
|
||||
}
|
||||
|
||||
expect(onQueriesChange).toHaveBeenCalledTimes(queryEditorRows.length);
|
||||
expect(onQueryRemoved).toHaveBeenCalledTimes(queryEditorRows.length);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { DragDropContext, DragStart, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { PureComponent, ReactNode } from 'react';
|
||||
|
||||
@@ -44,9 +45,15 @@ export interface Props {
|
||||
queryLibraryRef?: string;
|
||||
onCancelQueryLibraryEdit?: () => void;
|
||||
isOpen?: boolean;
|
||||
onFocusQuery?: (arg0: boolean) => void;
|
||||
}
|
||||
|
||||
export class QueryEditorRows extends PureComponent<Props> {
|
||||
export class QueryEditorRows extends PureComponent<Props, { focusedQueryRefId: string | null }> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { focusedQueryRefId: null };
|
||||
}
|
||||
|
||||
onRemoveQuery = (query: DataQuery) => {
|
||||
this.props.onQueriesChange(this.props.queries.filter((item) => item !== query));
|
||||
};
|
||||
@@ -169,6 +176,16 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
});
|
||||
};
|
||||
|
||||
setFocusedQueryRefId = (refId: string | null) => {
|
||||
this.setState({ focusedQueryRefId: refId });
|
||||
this.props.onFocusQuery?.(Boolean(refId));
|
||||
};
|
||||
|
||||
toggleFocusedQuery = (refId: string) => {
|
||||
const newFocusedRefId = this.state.focusedQueryRefId === refId ? null : refId;
|
||||
this.setFocusedQueryRefId(newFocusedRefId);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dsSettings,
|
||||
@@ -189,14 +206,26 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
onCancelQueryLibraryEdit,
|
||||
isOpen,
|
||||
} = this.props;
|
||||
const { focusedQueryRefId } = this.state;
|
||||
const isFocused = Boolean(focusedQueryRefId);
|
||||
const containerStyle = getStyles(isFocused);
|
||||
|
||||
return (
|
||||
<DragDropContext onDragStart={this.onDragStart} onDragEnd={this.onDragEnd}>
|
||||
<Droppable droppableId="transformations-list" direction="vertical">
|
||||
{(provided) => {
|
||||
return (
|
||||
<div data-testid="query-editor-rows" ref={provided.innerRef} {...provided.droppableProps}>
|
||||
<div
|
||||
data-testid="query-editor-rows"
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={containerStyle}
|
||||
>
|
||||
{queries.map((query, index) => {
|
||||
// If a query is focused, don't render any other queries
|
||||
if (focusedQueryRefId && focusedQueryRefId !== query.refId) {
|
||||
return null;
|
||||
}
|
||||
const dataSourceSettings = getDataSourceSettings(query, dsSettings);
|
||||
const onChangeDataSourceSettings = dsSettings.meta.mixed
|
||||
? (settings: DataSourceInstanceSettings) => this.onDataSourceChange(settings, index)
|
||||
@@ -229,6 +258,8 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
queryLibraryRef={queryLibraryRef}
|
||||
onCancelQueryLibraryEdit={onCancelQueryLibraryEdit}
|
||||
isOpen={isOpen}
|
||||
isFocused={focusedQueryRefId === query.refId}
|
||||
onFocusQuery={() => this.toggleFocusedQuery(query.refId)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -254,3 +285,17 @@ const getDataSourceSettings = (
|
||||
const querySettings = getDataSourceSrv().getInstanceSettings(query.datasource);
|
||||
return querySettings || groupSettings;
|
||||
};
|
||||
|
||||
// Styles for focused container - using static styles since this is a class component
|
||||
// Transitions are handled in the parent functional component where we have theme access
|
||||
const getStyles = (isFocused: boolean) => {
|
||||
if (!isFocused) {
|
||||
return undefined;
|
||||
}
|
||||
return css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
});
|
||||
};
|
||||
|
||||
1
public/img/icons/unicons/compress-screen.svg
Normal file
1
public/img/icons/unicons/compress-screen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M256 128C256 110.3 241.7 96 224 96C206.3 96 192 110.3 192 128L192 192L128 192C110.3 192 96 206.3 96 224C96 241.7 110.3 256 128 256L224 256C241.7 256 256 241.7 256 224L256 128zM128 384C110.3 384 96 398.3 96 416C96 433.7 110.3 448 128 448L192 448L192 512C192 529.7 206.3 544 224 544C241.7 544 256 529.7 256 512L256 416C256 398.3 241.7 384 224 384L128 384zM448 128C448 110.3 433.7 96 416 96C398.3 96 384 110.3 384 128L384 224C384 241.7 398.3 256 416 256L512 256C529.7 256 544 241.7 544 224C544 206.3 529.7 192 512 192L448 192L448 128zM416 384C398.3 384 384 398.3 384 416L384 512C384 529.7 398.3 544 416 544C433.7 544 448 529.7 448 512L448 448L512 448C529.7 448 544 433.7 544 416C544 398.3 529.7 384 512 384L416 384z"/></svg>
|
||||
|
After Width: | Height: | Size: 943 B |
1
public/img/icons/unicons/expand-screen.svg
Normal file
1
public/img/icons/unicons/expand-screen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 96C110.3 96 96 110.3 96 128L96 224C96 241.7 110.3 256 128 256C145.7 256 160 241.7 160 224L160 160L224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L128 96zM160 416C160 398.3 145.7 384 128 384C110.3 384 96 398.3 96 416L96 512C96 529.7 110.3 544 128 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480L160 416zM416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160L480 160L480 224C480 241.7 494.3 256 512 256C529.7 256 544 241.7 544 224L544 128C544 110.3 529.7 96 512 96L416 96zM544 416C544 398.3 529.7 384 512 384C494.3 384 480 398.3 480 416L480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L512 544C529.7 544 544 529.7 544 512L544 416z"/></svg>
|
||||
|
After Width: | Height: | Size: 937 B |
@@ -12248,6 +12248,10 @@
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"query-editor-row": {
|
||||
"focused-message_one": "Query {{queryName}} is focused, {{count}} queries are hidden from view.",
|
||||
"focused-message_other": "Query {{queryName}} is focused, {{count}} queries are hidden from view."
|
||||
},
|
||||
"query-editor-row-header": {
|
||||
"hidden": "Hidden",
|
||||
"query-name-div-title-edit-query-name": "Edit query name"
|
||||
@@ -12297,11 +12301,15 @@
|
||||
},
|
||||
"query-operation": {
|
||||
"header": {
|
||||
"actions-menu": "Query actions menu",
|
||||
"collapse": "Show all queries",
|
||||
"collapse-row": "Collapse query row",
|
||||
"datasource-help": "Show data source help",
|
||||
"drag-and-drop": "Drag and drop to reorder",
|
||||
"duplicate-query": "Duplicate query",
|
||||
"expand-row": "Expand query row",
|
||||
"focus": "Focus query",
|
||||
"hide-datasource-help": "Hide data source help",
|
||||
"hide-response": "Hide response",
|
||||
"remove-query": "Remove query",
|
||||
"show-response": "Show response"
|
||||
|
||||
Reference in New Issue
Block a user