mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 23:53:20 +08:00
QueryLibrary: Add From Library Icon Action (#103317)
* QueryLibrary: AddfromIcon init work * Removed console log * updated i18n * Added unit tests for button * Fixed linting * Updates per feedback - new replace method * Remove run query call when replace * lint fixes * Feedback + method renaming to 'replace' * Fix await
This commit is contained in:
@ -4,7 +4,7 @@ import { QueriesDrawerContext, Tabs } from './QueriesDrawerContext';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setDrawerOpened?: (value: boolean) => {};
|
setDrawerOpened?: (value: boolean) => {};
|
||||||
queryLibraryAvailable?: boolean;
|
queryLibraryEnabled?: boolean;
|
||||||
} & PropsWithChildren;
|
} & PropsWithChildren;
|
||||||
|
|
||||||
export function QueriesDrawerContextProviderMock(props: Props) {
|
export function QueriesDrawerContextProviderMock(props: Props) {
|
||||||
|
@ -3,7 +3,7 @@ import { PropsWithChildren } from 'react';
|
|||||||
import { QueryLibraryContext } from './QueryLibraryContext';
|
import { QueryLibraryContext } from './QueryLibraryContext';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryLibraryAvailable?: boolean;
|
queryLibraryEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function QueryLibraryContextProviderMock(props: PropsWithChildren<Props>) {
|
export function QueryLibraryContextProviderMock(props: PropsWithChildren<Props>) {
|
||||||
@ -16,7 +16,7 @@ export function QueryLibraryContextProviderMock(props: PropsWithChildren<Props>)
|
|||||||
openAddQueryModal: jest.fn(),
|
openAddQueryModal: jest.fn(),
|
||||||
closeAddQueryModal: jest.fn(),
|
closeAddQueryModal: jest.fn(),
|
||||||
renderSaveQueryButton: jest.fn(),
|
renderSaveQueryButton: jest.fn(),
|
||||||
queryLibraryEnabled: false,
|
queryLibraryEnabled: Boolean(props.queryLibraryEnabled),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -9,6 +9,7 @@ import { ExploreState } from 'app/types';
|
|||||||
|
|
||||||
import { UserState } from '../profile/state/reducers';
|
import { UserState } from '../profile/state/reducers';
|
||||||
|
|
||||||
|
import { QueryLibraryContextProviderMock } from './QueryLibrary/mocks';
|
||||||
import { QueryRows } from './QueryRows';
|
import { QueryRows } from './QueryRows';
|
||||||
import { makeExplorePaneState } from './state/utils';
|
import { makeExplorePaneState } from './state/utils';
|
||||||
|
|
||||||
@ -92,4 +93,37 @@ describe('Explore QueryRows', () => {
|
|||||||
// We should have another row with refId B
|
// We should have another row with refId B
|
||||||
expect(await screen.findByLabelText('Query editor row title B')).toBeInTheDocument();
|
expect(await screen.findByLabelText('Query editor row title B')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should contain a select query from library button when query library is enabled', async () => {
|
||||||
|
const { store } = setup([{ refId: 'A' }]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryLibraryContextProviderMock queryLibraryEnabled={true}>
|
||||||
|
<QueryRows exploreId={'left'} />
|
||||||
|
</QueryLibraryContextProviderMock>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// waiting for the component to fully render.
|
||||||
|
await screen.findAllByText('someDs query editor');
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/Replace with query from library/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not contain a select query from library button when query library is disabled', async () => {
|
||||||
|
const { store } = setup([{ refId: 'A' }]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryLibraryContextProviderMock queryLibraryEnabled={false}>
|
||||||
|
<QueryRows exploreId={'left'} />
|
||||||
|
</QueryLibraryContextProviderMock>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findAllByText('someDs query editor');
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(/Replace with query from library/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,13 +3,14 @@ import { useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { CoreApp, getNextRefId } from '@grafana/data';
|
import { CoreApp, getNextRefId } from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||||
import { useDispatch, useSelector } from 'app/types';
|
import { useDispatch, useSelector } from 'app/types';
|
||||||
|
|
||||||
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
||||||
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
||||||
|
|
||||||
import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem';
|
import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem';
|
||||||
|
import { changeDatasource } from './state/datasource';
|
||||||
import { changeQueries, runQueries } from './state/query';
|
import { changeQueries, runQueries } from './state/query';
|
||||||
import { getExploreItemSelector } from './state/selectors';
|
import { getExploreItemSelector } from './state/selectors';
|
||||||
|
|
||||||
@ -55,6 +56,13 @@ export const QueryRows = ({ exploreId }: Props) => {
|
|||||||
[dispatch, exploreId]
|
[dispatch, exploreId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onUpdateDatasources = useCallback(
|
||||||
|
(datasource: DataSourceRef) => {
|
||||||
|
dispatch(changeDatasource({ exploreId, datasource }));
|
||||||
|
},
|
||||||
|
[dispatch, exploreId]
|
||||||
|
);
|
||||||
|
|
||||||
const onAddQuery = useCallback(
|
const onAddQuery = useCallback(
|
||||||
(query: DataQuery) => {
|
(query: DataQuery) => {
|
||||||
onChange([...queries, { ...query, refId: getNextRefId(queries) }]);
|
onChange([...queries, { ...query, refId: getNextRefId(queries) }]);
|
||||||
@ -66,6 +74,10 @@ export const QueryRows = ({ exploreId }: Props) => {
|
|||||||
reportInteraction('grafana_explore_query_row_copy');
|
reportInteraction('grafana_explore_query_row_copy');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onQueryReplacedFromLibrary = () => {
|
||||||
|
reportInteraction('grafana_explore_query_replaced_from_library');
|
||||||
|
};
|
||||||
|
|
||||||
const onQueryRemoved = () => {
|
const onQueryRemoved = () => {
|
||||||
reportInteraction('grafana_explore_query_row_remove');
|
reportInteraction('grafana_explore_query_row_remove');
|
||||||
};
|
};
|
||||||
@ -79,11 +91,13 @@ export const QueryRows = ({ exploreId }: Props) => {
|
|||||||
dsSettings={dsSettings}
|
dsSettings={dsSettings}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
onQueriesChange={onChange}
|
onQueriesChange={onChange}
|
||||||
|
onUpdateDatasources={onUpdateDatasources}
|
||||||
onAddQuery={onAddQuery}
|
onAddQuery={onAddQuery}
|
||||||
onRunQueries={onRunQueries}
|
onRunQueries={onRunQueries}
|
||||||
onQueryCopied={onQueryCopied}
|
onQueryCopied={onQueryCopied}
|
||||||
onQueryRemoved={onQueryRemoved}
|
onQueryRemoved={onQueryRemoved}
|
||||||
onQueryToggled={onQueryToggled}
|
onQueryToggled={onQueryToggled}
|
||||||
|
onQueryReplacedFromLibrary={onQueryReplacedFromLibrary}
|
||||||
data={queryResponse}
|
data={queryResponse}
|
||||||
app={CoreApp.Explore}
|
app={CoreApp.Explore}
|
||||||
history={history}
|
history={history}
|
||||||
|
@ -35,7 +35,7 @@ describe('SecondaryActions', () => {
|
|||||||
|
|
||||||
it('should not render hidden elements', () => {
|
it('should not render hidden elements', () => {
|
||||||
render(
|
render(
|
||||||
<QueriesDrawerContextProviderMock queryLibraryAvailable={false}>
|
<QueriesDrawerContextProviderMock queryLibraryEnabled={false}>
|
||||||
<SecondaryActions
|
<SecondaryActions
|
||||||
addQueryRowButtonHidden={true}
|
addQueryRowButtonHidden={true}
|
||||||
richHistoryRowButtonHidden={true}
|
richHistoryRowButtonHidden={true}
|
||||||
|
@ -354,6 +354,7 @@ describe('QueryEditorRow', () => {
|
|||||||
onRunQuery: jest.fn(),
|
onRunQuery: jest.fn(),
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
onRemoveQuery: jest.fn(),
|
onRemoveQuery: jest.fn(),
|
||||||
|
onReplace: jest.fn(),
|
||||||
index: 0,
|
index: 0,
|
||||||
range: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } },
|
range: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } },
|
||||||
});
|
});
|
||||||
|
@ -52,6 +52,7 @@ export interface Props<TQuery extends DataQuery> {
|
|||||||
onAddQuery: (query: TQuery) => void;
|
onAddQuery: (query: TQuery) => void;
|
||||||
onRemoveQuery: (query: TQuery) => void;
|
onRemoveQuery: (query: TQuery) => void;
|
||||||
onChange: (query: TQuery) => void;
|
onChange: (query: TQuery) => void;
|
||||||
|
onReplace?: (query: DataQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
visualization?: ReactNode;
|
visualization?: ReactNode;
|
||||||
hideHideQueryButton?: boolean;
|
hideHideQueryButton?: boolean;
|
||||||
@ -63,6 +64,7 @@ export interface Props<TQuery extends DataQuery> {
|
|||||||
onQueryCopied?: () => void;
|
onQueryCopied?: () => void;
|
||||||
onQueryRemoved?: () => void;
|
onQueryRemoved?: () => void;
|
||||||
onQueryToggled?: (queryStatus?: boolean | undefined) => void;
|
onQueryToggled?: (queryStatus?: boolean | undefined) => void;
|
||||||
|
onQueryReplacedFromLibrary?: () => void;
|
||||||
collapsable?: boolean;
|
collapsable?: boolean;
|
||||||
hideRefId?: boolean;
|
hideRefId?: boolean;
|
||||||
}
|
}
|
||||||
@ -354,7 +356,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderActions = (props: QueryOperationRowRenderProps) => {
|
renderActions = (props: QueryOperationRowRenderProps) => {
|
||||||
const { query, hideHideQueryButton: hideHideQueryButton = false } = this.props;
|
const {
|
||||||
|
query,
|
||||||
|
hideHideQueryButton: hideHideQueryButton = false,
|
||||||
|
onReplace,
|
||||||
|
onQueryReplacedFromLibrary,
|
||||||
|
} = this.props;
|
||||||
const { datasource, showingHelp } = this.state;
|
const { datasource, showingHelp } = this.state;
|
||||||
const isHidden = !!query.hide;
|
const isHidden = !!query.hide;
|
||||||
|
|
||||||
@ -377,6 +384,13 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
icon="copy"
|
icon="copy"
|
||||||
onClick={this.onCopyQuery}
|
onClick={this.onCopyQuery}
|
||||||
/>
|
/>
|
||||||
|
<ReplaceQueryFromLibrary
|
||||||
|
datasourceFilters={datasource?.name ? [datasource.name] : []}
|
||||||
|
onSelectQuery={(query) => {
|
||||||
|
onQueryReplacedFromLibrary?.();
|
||||||
|
onReplace?.(query);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{!hideHideQueryButton ? (
|
{!hideHideQueryButton ? (
|
||||||
<QueryOperationToggleAction
|
<QueryOperationToggleAction
|
||||||
dataTestId={selectors.components.QueryEditorRow.actionButton('Hide response')}
|
dataTestId={selectors.components.QueryEditorRow.actionButton('Hide response')}
|
||||||
@ -515,6 +529,30 @@ function MaybeQueryLibrarySaveButton(props: { query: DataQuery }) {
|
|||||||
return renderSaveQueryButton(props.query);
|
return renderSaveQueryButton(props.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReplaceQueryFromLibraryProps<TQuery extends DataQuery> {
|
||||||
|
datasourceFilters: string[];
|
||||||
|
onSelectQuery: (query: DataQuery) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReplaceQueryFromLibrary<TQuery extends DataQuery>({
|
||||||
|
datasourceFilters,
|
||||||
|
onSelectQuery,
|
||||||
|
}: ReplaceQueryFromLibraryProps<TQuery>) {
|
||||||
|
const { openDrawer, queryLibraryEnabled } = useQueryLibraryContext();
|
||||||
|
|
||||||
|
const onReplaceQueryFromLibrary = () => {
|
||||||
|
openDrawer(datasourceFilters, onSelectQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
return queryLibraryEnabled ? (
|
||||||
|
<QueryOperationAction
|
||||||
|
title={t('query-operation.header.replace-query-from-library', 'Replace with query from library')}
|
||||||
|
icon="book"
|
||||||
|
onClick={onReplaceQueryFromLibrary}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
function AdaptiveTelemetryQueryActions({ query }: { query: DataQuery }) {
|
function AdaptiveTelemetryQueryActions({ query }: { query: DataQuery }) {
|
||||||
try {
|
try {
|
||||||
const { isLoading, components } = usePluginComponents<PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context>({
|
const { isLoading, components } = usePluginComponents<PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context>({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, queryByLabelText, render, screen } from '@testing-library/react';
|
import { fireEvent, queryByLabelText, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import { type DataQuery } from '@grafana/schema';
|
import { DataSourceRef, type DataQuery } from '@grafana/schema';
|
||||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||||
import createMockPanelData from 'app/plugins/datasource/azuremonitor/__mocks__/panelData';
|
import createMockPanelData from 'app/plugins/datasource/azuremonitor/__mocks__/panelData';
|
||||||
@ -54,6 +54,9 @@ const props: Props = {
|
|||||||
onRunQueries: function (): void {
|
onRunQueries: function (): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
|
onUpdateDatasources: function (datasource: DataSourceRef): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
data: createMockPanelData(),
|
data: createMockPanelData(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ import {
|
|||||||
getDataSourceRef,
|
getDataSourceRef,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getDataSourceSrv, reportInteraction } from '@grafana/runtime';
|
import { getDataSourceSrv, reportInteraction } from '@grafana/runtime';
|
||||||
|
import { DataSourceRef } from '@grafana/schema';
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
|
|
||||||
import { QueryEditorRow } from './QueryEditorRow';
|
import { QueryEditorRow } from './QueryEditorRow';
|
||||||
|
|
||||||
@ -35,6 +37,8 @@ export interface Props {
|
|||||||
onQueryCopied?: () => void;
|
onQueryCopied?: () => void;
|
||||||
onQueryRemoved?: () => void;
|
onQueryRemoved?: () => void;
|
||||||
onQueryToggled?: (queryStatus?: boolean | undefined) => void;
|
onQueryToggled?: (queryStatus?: boolean | undefined) => void;
|
||||||
|
onUpdateDatasources?: (datasource: DataSourceRef) => void;
|
||||||
|
onQueryReplacedFromLibrary?: () => void;
|
||||||
queryRowWrapper?: (children: ReactNode, refId: string) => ReactNode;
|
queryRowWrapper?: (children: ReactNode, refId: string) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +61,32 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onReplaceQuery(query: DataQuery, index: number) {
|
||||||
|
const { queries, onQueriesChange, onUpdateDatasources, dsSettings } = this.props;
|
||||||
|
|
||||||
|
// Replace old query with new query
|
||||||
|
const newQueries = queries.map((item, itemIndex) => {
|
||||||
|
if (itemIndex === index) {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
onQueriesChange(newQueries);
|
||||||
|
|
||||||
|
// Update datasources based on the new query set
|
||||||
|
if (query.datasource?.uid) {
|
||||||
|
const uniqueDatasources = new Set(newQueries.map((q) => q.datasource?.uid));
|
||||||
|
const isMixed = uniqueDatasources.size > 1;
|
||||||
|
const newDatasourceRef = {
|
||||||
|
uid: isMixed ? MIXED_DATASOURCE_NAME : query.datasource.uid,
|
||||||
|
};
|
||||||
|
const shouldChangeDatasource = dsSettings.uid !== newDatasourceRef.uid;
|
||||||
|
if (shouldChangeDatasource) {
|
||||||
|
onUpdateDatasources?.(newDatasourceRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onDataSourceChange(dataSource: DataSourceInstanceSettings, index: number) {
|
onDataSourceChange(dataSource: DataSourceInstanceSettings, index: number) {
|
||||||
const { queries, onQueriesChange } = this.props;
|
const { queries, onQueriesChange } = this.props;
|
||||||
|
|
||||||
@ -143,6 +173,7 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
onQueryCopied,
|
onQueryCopied,
|
||||||
onQueryRemoved,
|
onQueryRemoved,
|
||||||
onQueryToggled,
|
onQueryToggled,
|
||||||
|
onQueryReplacedFromLibrary,
|
||||||
queryRowWrapper,
|
queryRowWrapper,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -168,12 +199,14 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
dataSource={dataSourceSettings}
|
dataSource={dataSourceSettings}
|
||||||
onChangeDataSource={onChangeDataSourceSettings}
|
onChangeDataSource={onChangeDataSourceSettings}
|
||||||
onChange={(query) => this.onChangeQuery(query, index)}
|
onChange={(query) => this.onChangeQuery(query, index)}
|
||||||
|
onReplace={(query) => this.onReplaceQuery(query, index)}
|
||||||
onRemoveQuery={this.onRemoveQuery}
|
onRemoveQuery={this.onRemoveQuery}
|
||||||
onAddQuery={onAddQuery}
|
onAddQuery={onAddQuery}
|
||||||
onRunQuery={onRunQueries}
|
onRunQuery={onRunQueries}
|
||||||
onQueryCopied={onQueryCopied}
|
onQueryCopied={onQueryCopied}
|
||||||
onQueryRemoved={onQueryRemoved}
|
onQueryRemoved={onQueryRemoved}
|
||||||
onQueryToggled={onQueryToggled}
|
onQueryToggled={onQueryToggled}
|
||||||
|
onQueryReplacedFromLibrary={onQueryReplacedFromLibrary}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
app={app}
|
app={app}
|
||||||
range={getTimeSrv().timeRange()}
|
range={getTimeSrv().timeRange()}
|
||||||
|
@ -6864,6 +6864,7 @@
|
|||||||
"expand-row": "Expand query row",
|
"expand-row": "Expand query row",
|
||||||
"hide-response": "Hide response",
|
"hide-response": "Hide response",
|
||||||
"remove-query": "Remove query",
|
"remove-query": "Remove query",
|
||||||
|
"replace-query-from-library": "Replace with query from library",
|
||||||
"show-response": "Show response"
|
"show-response": "Show response"
|
||||||
},
|
},
|
||||||
"query-editor-not-exported": "Data source plugin does not export any Query Editor component"
|
"query-editor-not-exported": "Data source plugin does not export any Query Editor component"
|
||||||
|
Reference in New Issue
Block a user