Query Library: Edit Query Library in explore using queryLibraryRef (#108863)
* add suport for queryLibraryRef in explore
* add feature flag check on badge
* add test for migrator
* Add QueryEditor.test and useStateSync test
* implement onCancelEditingQuery library
* run i18n extract
* push mod changes
* Revert "push mod changes"
This reverts commit 29c410f126.
* remove queryRef from the URL
* remove queryRef from url in explore page, keep redux logic
* restore tests and remove old queryRef from URL behaviour
* clean up test
* remove unnecessary useCallback from onCancelQueryLibraryEdit
* apply PR suggestions about naming of functions
* rename queryRef to queryLibraryRef
* update missing files with the new queryLibraryRef
* fix i18n check
* Disabled add query buttons when queryLibraryRef is present
* Add unit test to components
* add missing queryLibraryRef after merge
* add placeholder for highlighted code
* clear up queryLibraryRef after sucess update
* add monitoring
* remove unnecessary change
* remove placeholder and integrate highlightQuery
* change "Editing From Query Library" to "Update query from library"
* update tootlip text
* fix interaction importing issue
* updating i18n for query library editing badge
* update copy on saving actions
* keep copy short
---------
Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com>
This commit is contained in:
co-authored by
Juan Cabanas
parent
94f7602786
commit
48edf1808e
@@ -93,7 +93,11 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
|
||||
.map((q) => q.value);
|
||||
|
||||
const exploreState = JSON.stringify({
|
||||
[generateExploreId()]: { range: toURLRange(timeRange.raw), queries: interpolatedQueries, datasource: dsRef?.uid },
|
||||
[generateExploreId()]: {
|
||||
range: toURLRange(timeRange.raw),
|
||||
queries: interpolatedQueries,
|
||||
datasource: dsRef?.uid,
|
||||
},
|
||||
});
|
||||
return locationUtil.assureBaseUrl(urlUtil.renderUrl('/explore', { panes: exploreState, schemaVersion: 1 }));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext';
|
||||
import { Explore, Props } from './Explore';
|
||||
import { QueryLibraryContextProviderMock } from './QueryLibrary/mocks';
|
||||
import { initialExploreState } from './state/main';
|
||||
import { scanStopAction } from './state/query';
|
||||
import { createEmptyQueryResponse, makeExplorePaneState } from './state/utils';
|
||||
@@ -110,6 +111,7 @@ const dummyProps: Props = {
|
||||
changeDatasource: jest.fn(),
|
||||
compact: false,
|
||||
changeCompactMode: jest.fn(),
|
||||
queryLibraryRef: undefined,
|
||||
};
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
@@ -240,4 +242,57 @@ describe('Explore', () => {
|
||||
getBoolMock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query Library Integration', () => {
|
||||
it('should enable add query buttons when queryLibraryRef is undefined', async () => {
|
||||
setup({ queryLibraryRef: undefined });
|
||||
|
||||
// Wait for the Explore component to render
|
||||
await screen.findByTestId(selectors.components.DataSourcePicker.container);
|
||||
|
||||
const addQueryButton = screen.getByRole('button', { name: /Add query$/i });
|
||||
expect(addQueryButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it('should disable add query buttons when queryLibraryRef is set (editing from library)', async () => {
|
||||
setup({ queryLibraryRef: 'library-query-123' });
|
||||
|
||||
// Wait for the Explore component to render
|
||||
await screen.findByTestId(selectors.components.DataSourcePicker.container);
|
||||
|
||||
const addQueryButton = screen.getByRole('button', { name: /Add query$/i });
|
||||
expect(addQueryButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should disable both add query and add from library buttons when editing from library', async () => {
|
||||
const store = configureStore({
|
||||
explore: {
|
||||
...initialExploreState,
|
||||
panes: {
|
||||
left: makeExplorePaneState(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const exploreProps = { ...dummyProps, queryLibraryRef: 'library-query-123' };
|
||||
|
||||
render(
|
||||
<TestProvider store={store}>
|
||||
<QueryLibraryContextProviderMock queryLibraryEnabled={true}>
|
||||
<ContentOutlineContextProvider>
|
||||
<Explore {...exploreProps} />
|
||||
</ContentOutlineContextProvider>
|
||||
</QueryLibraryContextProviderMock>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
// Wait for the Explore component to render
|
||||
await screen.findByTestId(selectors.components.DataSourcePicker.container);
|
||||
|
||||
const addQueryButton = screen.getByRole('button', { name: /Add query$/i });
|
||||
const addFromLibraryButton = screen.getByRole('button', { name: /Add query from library/i });
|
||||
|
||||
expect(addQueryButton).toBeDisabled();
|
||||
expect(addFromLibraryButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -580,6 +580,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
|
||||
setShowQueryInspector,
|
||||
splitted,
|
||||
compact,
|
||||
queryLibraryRef,
|
||||
} = this.props;
|
||||
const { contentOutlineVisible } = this.state;
|
||||
const styles = getStyles(theme);
|
||||
@@ -654,7 +655,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
|
||||
<SecondaryActions
|
||||
// do not allow people to add queries with potentially different datasources in correlations editor mode
|
||||
addQueryRowButtonDisabled={
|
||||
isLive || (isCorrelationsEditorMode && datasourceInstance.meta.mixed)
|
||||
isLive || (isCorrelationsEditorMode && datasourceInstance.meta.mixed) || !!queryLibraryRef
|
||||
}
|
||||
// We cannot show multiple traces at the same time right now so we do not show add query button.
|
||||
//TODO:unification
|
||||
@@ -766,6 +767,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
||||
supplementaryQueries,
|
||||
correlationEditorHelperData,
|
||||
compact,
|
||||
queryLibraryRef,
|
||||
} = item;
|
||||
|
||||
const loading = selectIsWaitingForData(exploreId)(state);
|
||||
@@ -799,6 +801,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
||||
correlationEditorHelperData,
|
||||
correlationEditorDetails: explore.correlationEditorDetails,
|
||||
exploreActiveDS: selectExploreDSMaps(state),
|
||||
queryLibraryRef,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,12 @@ export type QueryLibraryContextType = {
|
||||
* Returns a predefined small button that can be used to save a query to the library.
|
||||
* @param query
|
||||
*/
|
||||
renderSaveQueryButton: (query: DataQuery, app?: CoreApp) => ReactNode;
|
||||
renderSaveQueryButton: (
|
||||
query: DataQuery,
|
||||
app?: CoreApp,
|
||||
queryLibraryRef?: string,
|
||||
onUpdateSuccess?: () => void
|
||||
) => ReactNode;
|
||||
queryLibraryEnabled: boolean;
|
||||
context: string;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,9 @@ import { getDatasourceSrv } from '../plugins/datasource_srv';
|
||||
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
||||
|
||||
import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem';
|
||||
import { useQueryLibraryContext } from './QueryLibrary/QueryLibraryContext';
|
||||
import { changeDatasource } from './state/datasource';
|
||||
import { updateQueryLibraryRefAction } from './state/explorePane';
|
||||
import { changeQueries, runQueries } from './state/query';
|
||||
import { getExploreItemSelector } from './state/selectors';
|
||||
|
||||
@@ -32,21 +34,28 @@ const makeSelectors = (exploreId: string) => {
|
||||
exploreItemSelector,
|
||||
(s: ExploreItemState | undefined) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.uid)!
|
||||
),
|
||||
getQueryLibraryRef: createSelector(exploreItemSelector, (s) => s!.queryLibraryRef),
|
||||
};
|
||||
};
|
||||
|
||||
export const QueryRows = ({ exploreId, isOpen, changeCompactMode }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const { getQueries, getDatasourceInstanceSettings, getQueryResponse, getHistory, getEventBridge } = useMemo(
|
||||
() => makeSelectors(exploreId),
|
||||
[exploreId]
|
||||
);
|
||||
const { openDrawer } = useQueryLibraryContext();
|
||||
const {
|
||||
getQueries,
|
||||
getDatasourceInstanceSettings,
|
||||
getQueryResponse,
|
||||
getHistory,
|
||||
getEventBridge,
|
||||
getQueryLibraryRef,
|
||||
} = useMemo(() => makeSelectors(exploreId), [exploreId]);
|
||||
|
||||
const queries = useSelector(getQueries);
|
||||
const dsSettings = useSelector(getDatasourceInstanceSettings);
|
||||
const queryResponse = useSelector(getQueryResponse);
|
||||
const history = useSelector(getHistory);
|
||||
const eventBridge = useSelector(getEventBridge);
|
||||
const queryLibraryRef = useSelector(getQueryLibraryRef);
|
||||
|
||||
const onRunQueries = useCallback(() => {
|
||||
dispatch(runQueries({ exploreId }));
|
||||
@@ -89,6 +98,22 @@ export const QueryRows = ({ exploreId, isOpen, changeCompactMode }: Props) => {
|
||||
reportInteraction('grafana_query_row_toggle', queryStatus === undefined ? {} : { queryEnabled: queryStatus });
|
||||
};
|
||||
|
||||
const onCancelQueryLibraryEdit = () => {
|
||||
// Store the current queryLibraryRef before clearing it
|
||||
const originalQueryRef = queryLibraryRef;
|
||||
|
||||
// Clear the queryLibraryRef to exit editing mode
|
||||
dispatch(updateQueryLibraryRefAction({ exploreId, queryLibraryRef: undefined }));
|
||||
|
||||
// Open drawer with the original query highlighted
|
||||
if (originalQueryRef) {
|
||||
openDrawer([], () => {}, {
|
||||
context: 'explore',
|
||||
highlightQuery: originalQueryRef,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onQueryOpenChanged = () => {
|
||||
// Disables compact mode when query is opened.
|
||||
// Compact mode can also be disabled by opening Content Outline.
|
||||
@@ -112,6 +137,8 @@ export const QueryRows = ({ exploreId, isOpen, changeCompactMode }: Props) => {
|
||||
app={CoreApp.Explore}
|
||||
history={history}
|
||||
eventBus={eventBridge}
|
||||
queryLibraryRef={queryLibraryRef}
|
||||
onCancelQueryLibraryEdit={onCancelQueryLibraryEdit}
|
||||
isOpen={isOpen}
|
||||
queryRowWrapper={(children, refId) => (
|
||||
<ContentOutlineItem
|
||||
|
||||
@@ -64,6 +64,23 @@ describe('SecondaryActions', () => {
|
||||
expect(screen.getByRole('button', { name: /Query inspector/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable both add query buttons when addQueryRowButtonDisabled=true and query library is enabled', () => {
|
||||
render(
|
||||
<QueryLibraryContextProviderMock queryLibraryEnabled={true}>
|
||||
<SecondaryActions
|
||||
addQueryRowButtonDisabled={true}
|
||||
onClickAddQueryRowButton={noop}
|
||||
onClickQueryInspectorButton={noop}
|
||||
onSelectQueryFromLibrary={noop}
|
||||
/>
|
||||
</QueryLibraryContextProviderMock>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: /Add query$/i })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /Add query from library/i })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /Query inspector/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should map click handlers correctly', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ export function SecondaryActions({
|
||||
})
|
||||
}
|
||||
icon="plus"
|
||||
disabled={addQueryRowButtonDisabled}
|
||||
>
|
||||
<Trans i18nKey="explore.secondary-actions.add-from-query-library">Add query from library</Trans>
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { makeDatasourceSetup } from '../../spec/helper/setup';
|
||||
import { updateQueryLibraryRefAction } from '../../state/explorePane';
|
||||
import { splitClose, splitOpen } from '../../state/main';
|
||||
|
||||
import { useStateSync } from './';
|
||||
@@ -584,4 +585,34 @@ describe('useStateSync', () => {
|
||||
expect(store.getState().explore.panes['one']?.queries[0]).toMatchObject({ expr: 'b', refId: 'B' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep queryLibraryRef in state but not in URL', async () => {
|
||||
const { store, location } = setup({
|
||||
queryParams: {
|
||||
panes: JSON.stringify({
|
||||
one: {
|
||||
datasource: 'loki-uid',
|
||||
queries: [{ expr: 'test', refId: 'A' }],
|
||||
},
|
||||
}),
|
||||
schemaVersion: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().explore.panes['one']).toBeDefined();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
store.dispatch(updateQueryLibraryRefAction({ exploreId: 'one', queryLibraryRef: 'library-query-456' }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().explore.panes['one']?.queryLibraryRef).toBe('library-query-456');
|
||||
|
||||
const search = location.getSearchObject();
|
||||
const panes = search.panes && typeof search.panes === 'string' ? JSON.parse(search.panes) : {};
|
||||
expect(panes.one?.queryLibraryRef).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,6 +97,13 @@ export const changeCorrelationHelperData = createAction<ChangeCorrelationHelperD
|
||||
'explore/changeCorrelationHelperData'
|
||||
);
|
||||
|
||||
export interface UpdateQueryLibraryRefPayload {
|
||||
exploreId: string;
|
||||
queryLibraryRef?: string;
|
||||
}
|
||||
|
||||
export const updateQueryLibraryRefAction = createAction<UpdateQueryLibraryRefPayload>('explore/updateQueryLibraryRef');
|
||||
|
||||
/**
|
||||
* Initialize Explore state with state from the URL and the React component.
|
||||
* Call this only on components for with the Explore state has not been initialized.
|
||||
@@ -109,6 +116,7 @@ interface InitializeExplorePayload {
|
||||
datasourceInstance?: DataSourceApi;
|
||||
compact: boolean;
|
||||
eventBridge: EventBusExtended;
|
||||
queryLibraryRef?: string;
|
||||
}
|
||||
|
||||
const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExploreAction');
|
||||
@@ -147,6 +155,7 @@ export interface InitializeExploreOptions {
|
||||
correlationHelperData?: ExploreCorrelationHelperData;
|
||||
position?: number;
|
||||
eventBridge: EventBusExtended;
|
||||
queryLibraryRef?: string;
|
||||
compact: boolean;
|
||||
}
|
||||
|
||||
@@ -170,6 +179,7 @@ export const initializeExplore = createAsyncThunk(
|
||||
compact,
|
||||
correlationHelperData,
|
||||
eventBridge,
|
||||
queryLibraryRef,
|
||||
}: InitializeExploreOptions,
|
||||
{ dispatch, getState, fulfillWithValue }
|
||||
) => {
|
||||
@@ -192,6 +202,7 @@ export const initializeExplore = createAsyncThunk(
|
||||
history,
|
||||
compact,
|
||||
eventBridge,
|
||||
queryLibraryRef,
|
||||
})
|
||||
);
|
||||
if (panelsState !== undefined) {
|
||||
@@ -262,8 +273,15 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
||||
};
|
||||
}
|
||||
|
||||
if (updateQueryLibraryRefAction.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
queryLibraryRef: action.payload.queryLibraryRef,
|
||||
};
|
||||
}
|
||||
|
||||
if (initializeExploreAction.match(action)) {
|
||||
const { queries, range, datasourceInstance, history, eventBridge, compact } = action.payload;
|
||||
const { queries, range, datasourceInstance, history, eventBridge, compact, queryLibraryRef } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -277,6 +295,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
||||
queryResponse: createEmptyQueryResponse(),
|
||||
cache: [],
|
||||
correlations: [],
|
||||
queryLibraryRef,
|
||||
compact,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,15 +5,28 @@ import { DataQueryRequest, dateTime, LoadingState, PanelData, toDataFrame } from
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
|
||||
import { DataSourceType } from '../../alerting/unified/utils/datasource';
|
||||
|
||||
import { filterPanelDataToQuery, Props, QueryEditorRow } from './QueryEditorRow';
|
||||
import { filterPanelDataToQuery, Props, QueryEditorRow, QueryLibraryEditingBadge } from './QueryEditorRow';
|
||||
|
||||
const mockDS = mockDataSource({
|
||||
name: 'test',
|
||||
type: DataSourceType.Alertmanager,
|
||||
type: 'testdata',
|
||||
});
|
||||
|
||||
// Mock the QueryLibraryContext
|
||||
const mockQueryLibraryContext = {
|
||||
queryLibraryEnabled: true,
|
||||
};
|
||||
|
||||
jest.mock('app/features/explore/QueryLibrary/QueryLibraryContext', () => ({
|
||||
useQueryLibraryContext: () => mockQueryLibraryContext,
|
||||
}));
|
||||
|
||||
// Mock the internationalization function
|
||||
jest.mock('@grafana/i18n', () => ({
|
||||
...jest.requireActual('@grafana/i18n'),
|
||||
t: (key: string, defaultValue: string) => defaultValue,
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => ({
|
||||
@@ -392,3 +405,30 @@ describe('QueryEditorRow', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('QueryLibraryBadge', () => {
|
||||
beforeEach(() => {
|
||||
mockQueryLibraryContext.queryLibraryEnabled = true;
|
||||
});
|
||||
|
||||
it('should display badge when queryLibraryEnabled is true and queryLibraryRef is provided', () => {
|
||||
render(<QueryLibraryEditingBadge queryLibraryRef="library-query-123" />);
|
||||
expect(screen.getByText('Updating query from library')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display badge when queryLibraryEnabled is false', () => {
|
||||
mockQueryLibraryContext.queryLibraryEnabled = false;
|
||||
render(<QueryLibraryEditingBadge queryLibraryRef="library-query-123" />);
|
||||
expect(screen.queryByText('Updating query from library')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display badge when queryLibraryRef is not provided', () => {
|
||||
render(<QueryLibraryEditingBadge />);
|
||||
expect(screen.queryByText('Updating query from library')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display badge when queryLibraryRef is empty string', () => {
|
||||
render(<QueryLibraryEditingBadge queryLibraryRef="" />);
|
||||
expect(screen.queryByText('Updating query from library')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,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, Divider, ErrorBoundaryAlert, List } from '@grafana/ui';
|
||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||
import {
|
||||
QueryOperationAction,
|
||||
@@ -68,6 +68,8 @@ export interface Props<TQuery extends DataQuery> {
|
||||
onQueryReplacedFromLibrary?: () => void;
|
||||
collapsable?: boolean;
|
||||
hideRefId?: boolean;
|
||||
queryLibraryRef?: string;
|
||||
onCancelQueryLibraryEdit?: () => void;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
@@ -233,6 +235,19 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
}
|
||||
};
|
||||
|
||||
onCancelQueryLibraryEdit = () => {
|
||||
const { query } = this.props;
|
||||
reportInteraction('query_library-update_query_from_explore_cancelled', {
|
||||
datasourceType: query.datasource?.type,
|
||||
});
|
||||
this.props.onCancelQueryLibraryEdit?.();
|
||||
};
|
||||
|
||||
onExitQueryLibraryEditingMode = () => {
|
||||
// Exit query library editing mode after successful update
|
||||
this.props.onCancelQueryLibraryEdit?.();
|
||||
};
|
||||
|
||||
onCopyQuery = () => {
|
||||
const { query, onAddQuery, onQueryCopied } = this.props;
|
||||
const copy = cloneDeep(query);
|
||||
@@ -326,6 +341,11 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
);
|
||||
};
|
||||
|
||||
renderQueryLibraryEditingBadge = () => {
|
||||
const { queryLibraryRef } = this.props;
|
||||
return <QueryLibraryEditingBadge key="query-library-editing-badge" queryLibraryRef={queryLibraryRef} />;
|
||||
};
|
||||
|
||||
renderExtraActions = () => {
|
||||
const { query, queries, data, onAddQuery, dataSource, app } = this.props;
|
||||
|
||||
@@ -363,24 +383,46 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
hideHideQueryButton: hideHideQueryButton = false,
|
||||
onReplace,
|
||||
onQueryReplacedFromLibrary,
|
||||
queryLibraryRef,
|
||||
} = this.props;
|
||||
const { datasource, showingHelp } = this.state;
|
||||
const isHidden = !!query.hide;
|
||||
|
||||
const hasEditorHelp = datasource?.components?.QueryEditorHelp;
|
||||
const isEditingQueryLibrary = queryLibraryRef !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MaybeQueryLibrarySaveButton query={query} app={this.props.app} />
|
||||
<ReplaceQueryFromLibrary
|
||||
datasourceFilters={datasource?.name ? [datasource.name] : []}
|
||||
onSelectQuery={(query) => {
|
||||
onQueryReplacedFromLibrary?.();
|
||||
onReplace?.(query);
|
||||
}}
|
||||
{isEditingQueryLibrary && this.renderQueryLibraryEditingBadge()}
|
||||
<MaybeQueryLibrarySaveButton
|
||||
query={query}
|
||||
queryLibraryRef={queryLibraryRef}
|
||||
app={this.props.app}
|
||||
onUpdateSuccess={this.onExitQueryLibraryEditingMode}
|
||||
/>
|
||||
|
||||
{!isEditingQueryLibrary && (
|
||||
<ReplaceQueryFromLibrary
|
||||
datasourceFilters={datasource?.name ? [datasource.name] : []}
|
||||
onSelectQuery={(query) => {
|
||||
onQueryReplacedFromLibrary?.();
|
||||
onReplace?.(query);
|
||||
}}
|
||||
app={this.props.app}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEditingQueryLibrary && (
|
||||
<>
|
||||
<QueryOperationAction
|
||||
title={t('query-operation.header.cancel-query-library-edit', 'Discard changes')}
|
||||
icon="times"
|
||||
onClick={this.onCancelQueryLibraryEdit}
|
||||
/>
|
||||
<Divider direction="vertical" spacing={0} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasEditorHelp && (
|
||||
<QueryOperationToggleAction
|
||||
title={t('query-operation.header.datasource-help', 'Show data source help')}
|
||||
@@ -390,11 +432,14 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
/>
|
||||
)}
|
||||
{this.renderExtraActions()}
|
||||
<QueryOperationAction
|
||||
title={t('query-operation.header.duplicate-query', 'Duplicate query')}
|
||||
icon="copy"
|
||||
onClick={this.onCopyQuery}
|
||||
/>
|
||||
{!isEditingQueryLibrary && (
|
||||
<QueryOperationAction
|
||||
title={t('query-operation.header.duplicate-query', 'Duplicate query')}
|
||||
icon="copy"
|
||||
onClick={this.onCopyQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hideHideQueryButton ? (
|
||||
<QueryOperationToggleAction
|
||||
dataTestId={selectors.components.QueryEditorRow.actionButton('Hide response')}
|
||||
@@ -408,11 +453,13 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
onClick={this.onHideQuery}
|
||||
/>
|
||||
) : null}
|
||||
<QueryOperationAction
|
||||
title={t('query-operation.header.remove-query', 'Remove query')}
|
||||
icon="trash-alt"
|
||||
onClick={this.onRemoveQuery}
|
||||
/>
|
||||
{!isEditingQueryLibrary && (
|
||||
<QueryOperationAction
|
||||
title={t('query-operation.header.remove-query', 'Remove query')}
|
||||
icon="trash-alt"
|
||||
onClick={this.onRemoveQuery}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -488,6 +535,30 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
}
|
||||
}
|
||||
|
||||
export function QueryLibraryEditingBadge(props: { queryLibraryRef?: string }) {
|
||||
const { queryLibraryEnabled } = useQueryLibraryContext();
|
||||
const { queryLibraryRef } = props;
|
||||
|
||||
if (!queryLibraryEnabled || !queryLibraryRef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
color="blue"
|
||||
icon="book"
|
||||
text={t('query-operation.query-library.from-library', 'Updating query from library')}
|
||||
tooltip={t(
|
||||
'query-operation.query-library.editing-tooltip',
|
||||
'Updating query from library\nUID: {{queryLibraryRef}}',
|
||||
{
|
||||
queryLibraryRef,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a version of the PanelData limited to the query we are looking at
|
||||
*/
|
||||
@@ -530,9 +601,14 @@ export function filterPanelDataToQuery(data: PanelData, refId: string): PanelDat
|
||||
}
|
||||
|
||||
// Will render anything only if query library is enabled
|
||||
function MaybeQueryLibrarySaveButton(props: { query: DataQuery; app?: CoreApp }) {
|
||||
function MaybeQueryLibrarySaveButton(props: {
|
||||
query: DataQuery;
|
||||
app?: CoreApp;
|
||||
queryLibraryRef?: string;
|
||||
onUpdateSuccess?: () => void;
|
||||
}) {
|
||||
const { renderSaveQueryButton } = useQueryLibraryContext();
|
||||
return renderSaveQueryButton(props.query, props.app);
|
||||
return renderSaveQueryButton(props.query, props.app, props.queryLibraryRef, props.onUpdateSuccess);
|
||||
}
|
||||
|
||||
interface ReplaceQueryFromLibraryProps<TQuery extends DataQuery> {
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface Props {
|
||||
onUpdateDatasources?: (datasource: DataSourceRef) => void;
|
||||
onQueryReplacedFromLibrary?: () => void;
|
||||
queryRowWrapper?: (children: ReactNode, refId: string) => ReactNode;
|
||||
queryLibraryRef?: string;
|
||||
onCancelQueryLibraryEdit?: () => void;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
@@ -178,6 +180,8 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
onQueryOpenChanged,
|
||||
onQueryReplacedFromLibrary,
|
||||
queryRowWrapper,
|
||||
queryLibraryRef,
|
||||
onCancelQueryLibraryEdit,
|
||||
isOpen,
|
||||
} = this.props;
|
||||
|
||||
@@ -217,6 +221,8 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
range={getTimeSrv().timeRange()}
|
||||
history={history}
|
||||
eventBus={eventBus}
|
||||
queryLibraryRef={queryLibraryRef}
|
||||
onCancelQueryLibraryEdit={onCancelQueryLibraryEdit}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -140,6 +140,11 @@ export interface ExploreItemState {
|
||||
* Used to distinguish URL state injection versus split view state injection.
|
||||
*/
|
||||
initialized: boolean;
|
||||
/**
|
||||
* Query library reference identifier when editing a query from the query library
|
||||
*
|
||||
*/
|
||||
queryLibraryRef?: string;
|
||||
/**
|
||||
* Log query result to be displayed in the logs result viewer.
|
||||
*/
|
||||
|
||||
@@ -11850,6 +11850,7 @@
|
||||
},
|
||||
"query-operation": {
|
||||
"header": {
|
||||
"cancel-query-library-edit": "Discard changes",
|
||||
"collapse-row": "Collapse query row",
|
||||
"datasource-help": "Show data source help",
|
||||
"drag-and-drop": "Drag and drop to reorder",
|
||||
@@ -11860,7 +11861,11 @@
|
||||
"replace-query-from-library": "Replace with query from library",
|
||||
"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",
|
||||
"query-library": {
|
||||
"editing-tooltip": "Updating query from library\nUID: {{queryLibraryRef}}",
|
||||
"from-library": "Updating query from library"
|
||||
}
|
||||
},
|
||||
"recently-deleted": {
|
||||
"buttons": {
|
||||
|
||||
Reference in New Issue
Block a user