mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore / Query Library: Enable run button (#87882)
* Enable run button * First pass of shared component, tests half-implemented * cleanup
This commit is contained in:
parent
3800b97a5b
commit
c8d237dd56
157
public/app/features/explore/ExploreRunQueryButton.test.tsx
Normal file
157
public/app/features/explore/ExploreRunQueryButton.test.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { DatasourceSrvMock, MockDataSourceApi } from 'test/mocks/datasource_srv';
|
||||||
|
|
||||||
|
import { DataSourceApi } from '@grafana/data';
|
||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
import { ExploreItemState, ExploreState } from 'app/types';
|
||||||
|
|
||||||
|
import { Props, ExploreRunQueryButton } from './ExploreRunQueryButton';
|
||||||
|
import { makeExplorePaneState } from './state/utils';
|
||||||
|
|
||||||
|
interface MockQuery extends DataQuery {
|
||||||
|
query: string;
|
||||||
|
queryText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lokiDs = {
|
||||||
|
uid: 'loki',
|
||||||
|
name: 'testDs',
|
||||||
|
type: 'loki',
|
||||||
|
meta: { mixed: false },
|
||||||
|
getRef: () => {
|
||||||
|
return { type: 'loki', uid: 'loki' };
|
||||||
|
},
|
||||||
|
} as unknown as DataSourceApi;
|
||||||
|
|
||||||
|
const promDs = {
|
||||||
|
uid: 'prom',
|
||||||
|
name: 'testDs2',
|
||||||
|
type: 'prom',
|
||||||
|
meta: { mixed: false },
|
||||||
|
getRef: () => {
|
||||||
|
return { type: 'prom', uid: 'prom' };
|
||||||
|
},
|
||||||
|
} as unknown as DataSourceApi;
|
||||||
|
|
||||||
|
const datasourceSrv = new DatasourceSrvMock(lokiDs, {
|
||||||
|
prom: promDs,
|
||||||
|
mixed: {
|
||||||
|
uid: 'mixed',
|
||||||
|
name: 'testDSMixed',
|
||||||
|
type: 'mixed',
|
||||||
|
meta: { mixed: true },
|
||||||
|
} as MockDataSourceApi,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDataSourceSrvMock = jest.fn().mockReturnValue(datasourceSrv);
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getDataSourceSrv: () => getDataSourceSrvMock(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setup = (propOverrides?: Partial<Props>, paneCount = 1) => {
|
||||||
|
const props: Props = {
|
||||||
|
queries: [],
|
||||||
|
rootDatasourceUid: 'loki',
|
||||||
|
setQueries: jest.fn(),
|
||||||
|
changeDatasource: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const panes: Record<string, ExploreItemState | undefined> = {};
|
||||||
|
|
||||||
|
if (paneCount > 0) {
|
||||||
|
panes.left = makeExplorePaneState({ datasourceInstance: lokiDs });
|
||||||
|
}
|
||||||
|
if (paneCount === 2) {
|
||||||
|
panes.right = makeExplorePaneState({ datasourceInstance: lokiDs });
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
explore: {
|
||||||
|
panes,
|
||||||
|
} as unknown as ExploreState,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ExploreRunQueryButton {...props} />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ExploreRunQueryButton', () => {
|
||||||
|
it('should disable run query button if there are no explore IDs', async () => {
|
||||||
|
setup({}, 0);
|
||||||
|
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
||||||
|
expect(runQueryButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled if the root datasource is undefined (invalid datasource)', async () => {
|
||||||
|
setup({
|
||||||
|
rootDatasourceUid: undefined,
|
||||||
|
});
|
||||||
|
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
||||||
|
expect(runQueryButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled if property is set', async () => {
|
||||||
|
setup({
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
||||||
|
expect(runQueryButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set new queries without changing DS when running queries from the same datasource', async () => {
|
||||||
|
const setQueries = jest.fn();
|
||||||
|
const changeDatasource = jest.fn();
|
||||||
|
const queries: MockQuery[] = [
|
||||||
|
{ query: 'query1', refId: 'A', datasource: { uid: 'loki' } },
|
||||||
|
{ query: 'query2', refId: 'B', datasource: { uid: 'loki' } },
|
||||||
|
];
|
||||||
|
setup({
|
||||||
|
setQueries,
|
||||||
|
changeDatasource,
|
||||||
|
rootDatasourceUid: 'loki',
|
||||||
|
queries,
|
||||||
|
});
|
||||||
|
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
||||||
|
await userEvent.click(runQueryButton);
|
||||||
|
|
||||||
|
expect(setQueries).toHaveBeenCalledWith(expect.any(String), queries);
|
||||||
|
expect(changeDatasource).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change datasource to mixed and set new queries when running queries from mixed datasource', async () => {
|
||||||
|
const setQueries = jest.fn();
|
||||||
|
const changeDatasource = jest.fn();
|
||||||
|
const queries: MockQuery[] = [
|
||||||
|
{ query: 'query1', refId: 'A', datasource: { type: 'loki', uid: 'loki' } },
|
||||||
|
{ query: 'query2', refId: 'B', datasource: { type: 'prometheus', uid: 'prometheus' } },
|
||||||
|
];
|
||||||
|
setup({
|
||||||
|
setQueries,
|
||||||
|
changeDatasource,
|
||||||
|
rootDatasourceUid: 'mixed',
|
||||||
|
queries,
|
||||||
|
});
|
||||||
|
|
||||||
|
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
||||||
|
await userEvent.click(runQueryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(setQueries).toHaveBeenCalledWith(expect.any(String), queries);
|
||||||
|
expect(changeDatasource).toHaveBeenCalledWith({ datasource: 'mixed', exploreId: 'left' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
126
public/app/features/explore/ExploreRunQueryButton.tsx
Normal file
126
public/app/features/explore/ExploreRunQueryButton.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ConnectedProps, connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { config, reportInteraction } from '@grafana/runtime';
|
||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
import { Button, Dropdown, Menu, ToolbarButton } from '@grafana/ui';
|
||||||
|
import { t } from '@grafana/ui/src/utils/i18n';
|
||||||
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
|
import { changeDatasource } from './state/datasource';
|
||||||
|
import { setQueries } from './state/query';
|
||||||
|
import { isSplit, selectExploreDSMaps, selectPanesEntries } from './state/selectors';
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setQueries,
|
||||||
|
changeDatasource,
|
||||||
|
};
|
||||||
|
|
||||||
|
const connector = connect(undefined, mapDispatchToProps);
|
||||||
|
|
||||||
|
interface ExploreRunQueryButtonProps {
|
||||||
|
queries: DataQuery[];
|
||||||
|
rootDatasourceUid?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = ConnectedProps<typeof connector> & ExploreRunQueryButtonProps;
|
||||||
|
|
||||||
|
/*
|
||||||
|
This component does not validate datasources before running them. Root datasource validation should happen outside this component and can pass in an undefined if invalid
|
||||||
|
If query level validation is done and a query datasource is invalid, pass in disabled = true
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function ExploreRunQueryButton({
|
||||||
|
rootDatasourceUid,
|
||||||
|
queries,
|
||||||
|
disabled = false,
|
||||||
|
changeDatasource,
|
||||||
|
setQueries,
|
||||||
|
}: Props) {
|
||||||
|
const [openRunQueryButton, setOpenRunQueryButton] = useState(false);
|
||||||
|
const isPaneSplit = useSelector(isSplit);
|
||||||
|
const exploreActiveDS = useSelector(selectExploreDSMaps);
|
||||||
|
const panesEntries = useSelector(selectPanesEntries);
|
||||||
|
|
||||||
|
const isDifferentDatasource = (uid: string, exploreId: string) =>
|
||||||
|
!exploreActiveDS.dsToExplore.find((di) => di.datasource.uid === uid)?.exploreIds.includes(exploreId);
|
||||||
|
|
||||||
|
// exploreId on where the query will be ran, and the datasource ID for the item's DS
|
||||||
|
const runQueryText = (exploreId: string, dsUid?: string) => {
|
||||||
|
// if the datasource or exploreID is undefined, it will be disabled, but give it default query button text
|
||||||
|
return dsUid !== undefined && exploreId !== undefined && isDifferentDatasource(dsUid, exploreId)
|
||||||
|
? {
|
||||||
|
fallbackText: 'Switch data source and run query',
|
||||||
|
translation: t('explore.run-query.switch-datasource-button', 'Switch data source and run query'),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fallbackText: 'Run query',
|
||||||
|
translation: t('explore.run-query.run-query-button', 'Run query'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const runQuery = async (exploreId: string) => {
|
||||||
|
const differentDataSource = isDifferentDatasource(rootDatasourceUid!, exploreId);
|
||||||
|
if (differentDataSource) {
|
||||||
|
await changeDatasource({ exploreId, datasource: rootDatasourceUid! });
|
||||||
|
}
|
||||||
|
setQueries(exploreId, queries);
|
||||||
|
|
||||||
|
reportInteraction('grafana_explore_query_history_run', {
|
||||||
|
queryHistoryEnabled: config.queryHistoryEnabled,
|
||||||
|
differentDataSource,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runButton = () => {
|
||||||
|
const isInvalid = disabled || queries.length === 0 || rootDatasourceUid === undefined;
|
||||||
|
if (!isPaneSplit) {
|
||||||
|
const exploreId = exploreActiveDS.exploreToDS[0]?.exploreId; // may be undefined if explore is refreshed while the pane is up
|
||||||
|
const buttonText = runQueryText(exploreId, rootDatasourceUid);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={buttonText.translation}
|
||||||
|
onClick={() => runQuery(exploreId)}
|
||||||
|
disabled={isInvalid || exploreId === undefined}
|
||||||
|
>
|
||||||
|
{buttonText.translation}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
{panesEntries.map((pane, i) => {
|
||||||
|
const buttonText = runQueryText(pane[0], rootDatasourceUid);
|
||||||
|
const paneLabel =
|
||||||
|
i === 0 ? t('explore.run-query.left-pane', 'Left pane') : t('explore.run-query.right-pane', 'Right pane');
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={i}
|
||||||
|
ariaLabel={buttonText.fallbackText}
|
||||||
|
onClick={() => {
|
||||||
|
runQuery(pane[0]);
|
||||||
|
}}
|
||||||
|
label={`${paneLabel}: ${buttonText.translation}`}
|
||||||
|
disabled={isInvalid || pane[0] === undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown onVisibleChange={(state) => setOpenRunQueryButton(state)} placement="bottom-start" overlay={menu}>
|
||||||
|
<ToolbarButton aria-label="run query options" variant="canvas" isOpen={openRunQueryButton}>
|
||||||
|
{t('explore.run-query.run-query-button', 'Run query')}
|
||||||
|
</ToolbarButton>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{runButton()}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connector(ExploreRunQueryButton);
|
@ -1,13 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Button } from '@grafana/ui';
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
|
||||||
export function ActionsCell() {
|
import ExploreRunQueryButton from '../../ExploreRunQueryButton';
|
||||||
return (
|
|
||||||
<>
|
interface ActionsCellProps {
|
||||||
<Button disabled={true} variant="primary">
|
query?: DataQuery;
|
||||||
Run
|
rootDatasourceUid?: string;
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ActionsCell({ query, rootDatasourceUid }: ActionsCellProps) {
|
||||||
|
return <ExploreRunQueryButton queries={query ? [query] : []} rootDatasourceUid={rootDatasourceUid} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActionsCell;
|
||||||
|
@ -4,7 +4,7 @@ import { SortByFn } from 'react-table';
|
|||||||
|
|
||||||
import { Column, InteractiveTable } from '@grafana/ui';
|
import { Column, InteractiveTable } from '@grafana/ui';
|
||||||
|
|
||||||
import { ActionsCell } from './ActionsCell';
|
import ActionsCell from './ActionsCell';
|
||||||
import { AddedByCell } from './AddedByCell';
|
import { AddedByCell } from './AddedByCell';
|
||||||
import { DatasourceTypeCell } from './DatasourceTypeCell';
|
import { DatasourceTypeCell } from './DatasourceTypeCell';
|
||||||
import { DateAddedCell } from './DateAddedCell';
|
import { DateAddedCell } from './DateAddedCell';
|
||||||
@ -22,7 +22,13 @@ const columns: Array<Column<QueryTemplateRow>> = [
|
|||||||
{ id: 'addedBy', header: 'Added by', cell: AddedByCell },
|
{ id: 'addedBy', header: 'Added by', cell: AddedByCell },
|
||||||
{ id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' },
|
{ id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' },
|
||||||
{ id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort },
|
{ id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort },
|
||||||
{ id: 'actions', header: '', cell: ActionsCell },
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row: { original } }) => (
|
||||||
|
<ActionsCell query={original.query} rootDatasourceUid={original.datasourceRef?.uid} />
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { fireEvent, render, screen, getByText, waitFor } from '@testing-library/react';
|
import { fireEvent, render, screen, getByText } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
@ -213,12 +213,6 @@ describe('RichHistoryCard', () => {
|
|||||||
expect(datasourceName).toHaveTextContent('Data source does not exist anymore');
|
expect(datasourceName).toHaveTextContent('Data source does not exist anymore');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable run query button if there are no explore IDs', async () => {
|
|
||||||
setup({}, true);
|
|
||||||
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
|
||||||
expect(runQueryButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('copy queries to clipboard', () => {
|
describe('copy queries to clipboard', () => {
|
||||||
it('should copy query model to clipboard when copying a query from a non existent datasource', async () => {
|
it('should copy query model to clipboard when copying a query from a non existent datasource', async () => {
|
||||||
setup({
|
setup({
|
||||||
@ -304,117 +298,6 @@ describe('RichHistoryCard', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('run queries', () => {
|
|
||||||
it('should be disabled if at least one query datasource is missing when using mixed', async () => {
|
|
||||||
const setQueries = jest.fn();
|
|
||||||
const changeDatasource = jest.fn();
|
|
||||||
const queries: MockQuery[] = [
|
|
||||||
{ query: 'query1', refId: 'A', datasource: { uid: 'nonexistent-ds' } },
|
|
||||||
{ query: 'query2', refId: 'B', datasource: { uid: 'loki' } },
|
|
||||||
];
|
|
||||||
setup({
|
|
||||||
setQueries,
|
|
||||||
changeDatasource,
|
|
||||||
queryHistoryItem: {
|
|
||||||
id: '2',
|
|
||||||
createdAt: 1,
|
|
||||||
datasourceUid: 'mixed',
|
|
||||||
datasourceName: 'Mixed',
|
|
||||||
starred: false,
|
|
||||||
comment: '',
|
|
||||||
queries,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
|
||||||
|
|
||||||
expect(runQueryButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be disabled if at datasource is missing', async () => {
|
|
||||||
const setQueries = jest.fn();
|
|
||||||
const changeDatasource = jest.fn();
|
|
||||||
const queries: MockQuery[] = [
|
|
||||||
{ query: 'query1', refId: 'A' },
|
|
||||||
{ query: 'query2', refId: 'B' },
|
|
||||||
];
|
|
||||||
setup({
|
|
||||||
setQueries,
|
|
||||||
changeDatasource,
|
|
||||||
queryHistoryItem: {
|
|
||||||
id: '2',
|
|
||||||
createdAt: 1,
|
|
||||||
datasourceUid: 'nonexistent-ds',
|
|
||||||
datasourceName: 'nonexistent-ds',
|
|
||||||
starred: false,
|
|
||||||
comment: '',
|
|
||||||
queries,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
|
||||||
|
|
||||||
expect(runQueryButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only set new queries when running queries from the same datasource', async () => {
|
|
||||||
const setQueries = jest.fn();
|
|
||||||
const changeDatasource = jest.fn();
|
|
||||||
const queries: MockQuery[] = [
|
|
||||||
{ query: 'query1', refId: 'A' },
|
|
||||||
{ query: 'query2', refId: 'B' },
|
|
||||||
];
|
|
||||||
setup({
|
|
||||||
setQueries,
|
|
||||||
changeDatasource,
|
|
||||||
queryHistoryItem: {
|
|
||||||
id: '2',
|
|
||||||
createdAt: 1,
|
|
||||||
datasourceUid: 'loki',
|
|
||||||
datasourceName: 'Loki',
|
|
||||||
starred: false,
|
|
||||||
comment: '',
|
|
||||||
queries,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
|
||||||
await userEvent.click(runQueryButton);
|
|
||||||
|
|
||||||
expect(setQueries).toHaveBeenCalledWith(expect.any(String), queries);
|
|
||||||
expect(changeDatasource).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should change datasource to mixed and set new queries when running queries from mixed datasource', async () => {
|
|
||||||
const setQueries = jest.fn();
|
|
||||||
const changeDatasource = jest.fn();
|
|
||||||
const queries: MockQuery[] = [
|
|
||||||
{ query: 'query1', refId: 'A', datasource: { type: 'loki', uid: 'loki' } },
|
|
||||||
{ query: 'query2', refId: 'B', datasource: { type: 'prometheus', uid: 'prometheus' } },
|
|
||||||
];
|
|
||||||
setup({
|
|
||||||
setQueries,
|
|
||||||
changeDatasource,
|
|
||||||
queryHistoryItem: {
|
|
||||||
id: '2',
|
|
||||||
createdAt: 1,
|
|
||||||
datasourceUid: 'mixed',
|
|
||||||
datasourceName: 'Mixed',
|
|
||||||
starred: false,
|
|
||||||
comment: '',
|
|
||||||
queries,
|
|
||||||
},
|
|
||||||
datasourceInstances: [dsStore.loki, dsStore.prometheus, dsStore.mixed],
|
|
||||||
});
|
|
||||||
|
|
||||||
const runQueryButton = await screen.findByRole('button', { name: /run query/i });
|
|
||||||
await userEvent.click(runQueryButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(setQueries).toHaveBeenCalledWith(expect.any(String), queries);
|
|
||||||
expect(changeDatasource).toHaveBeenCalledWith({ datasource: 'mixed', exploreId: 'left' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('commenting', () => {
|
describe('commenting', () => {
|
||||||
it('should render comment, if comment present', async () => {
|
it('should render comment, if comment present', async () => {
|
||||||
setup({ queryHistoryItem: starredQueryWithComment });
|
setup({ queryHistoryItem: starredQueryWithComment });
|
||||||
|
@ -5,7 +5,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
|||||||
import { GrafanaTheme2, DataSourceApi } from '@grafana/data';
|
import { GrafanaTheme2, DataSourceApi } from '@grafana/data';
|
||||||
import { config, reportInteraction, getAppEvents } from '@grafana/runtime';
|
import { config, reportInteraction, getAppEvents } from '@grafana/runtime';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { TextArea, Button, IconButton, useStyles2, ToolbarButton, Dropdown, Menu } from '@grafana/ui';
|
import { TextArea, Button, IconButton, useStyles2 } from '@grafana/ui';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
import { Trans, t } from 'app/core/internationalization';
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
@ -16,11 +16,10 @@ import { changeDatasource } from 'app/features/explore/state/datasource';
|
|||||||
import { starHistoryItem, commentHistoryItem, deleteHistoryItem } from 'app/features/explore/state/history';
|
import { starHistoryItem, commentHistoryItem, deleteHistoryItem } from 'app/features/explore/state/history';
|
||||||
import { setQueries } from 'app/features/explore/state/query';
|
import { setQueries } from 'app/features/explore/state/query';
|
||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { useSelector } from 'app/types';
|
|
||||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||||
import { RichHistoryQuery } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
|
|
||||||
import { isSplit, selectExploreDSMaps, selectPanesEntries } from '../state/selectors';
|
import ExploreRunQueryButton from '../ExploreRunQueryButton';
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
changeDatasource,
|
changeDatasource,
|
||||||
@ -134,46 +133,16 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function RichHistoryCard(props: Props) {
|
export function RichHistoryCard(props: Props) {
|
||||||
const {
|
const { queryHistoryItem, commentHistoryItem, starHistoryItem, deleteHistoryItem, datasourceInstances } = props;
|
||||||
queryHistoryItem,
|
|
||||||
commentHistoryItem,
|
|
||||||
starHistoryItem,
|
|
||||||
deleteHistoryItem,
|
|
||||||
changeDatasource,
|
|
||||||
setQueries,
|
|
||||||
datasourceInstances,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [activeUpdateComment, setActiveUpdateComment] = useState(false);
|
const [activeUpdateComment, setActiveUpdateComment] = useState(false);
|
||||||
const [openRunQueryButton, setOpenRunQueryButton] = useState(false);
|
|
||||||
const [comment, setComment] = useState<string | undefined>(queryHistoryItem.comment);
|
const [comment, setComment] = useState<string | undefined>(queryHistoryItem.comment);
|
||||||
const panesEntries = useSelector(selectPanesEntries);
|
|
||||||
const exploreActiveDS = useSelector(selectExploreDSMaps);
|
|
||||||
const isPaneSplit = useSelector(isSplit);
|
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const cardRootDatasource = datasourceInstances
|
const cardRootDatasource = datasourceInstances
|
||||||
? datasourceInstances.find((di) => di.uid === queryHistoryItem.datasourceUid)
|
? datasourceInstances.find((di) => di.uid === queryHistoryItem.datasourceUid)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const isDifferentDatasource = (uid: string, exploreId: string) =>
|
|
||||||
!exploreActiveDS.dsToExplore.find((di) => di.datasource.uid === uid)?.exploreIds.includes(exploreId);
|
|
||||||
|
|
||||||
const onRunQuery = async (exploreId: string) => {
|
|
||||||
const queriesToRun = queryHistoryItem.queries;
|
|
||||||
const differentDataSource = isDifferentDatasource(queryHistoryItem.datasourceUid, exploreId);
|
|
||||||
if (differentDataSource) {
|
|
||||||
await changeDatasource({ exploreId, datasource: queryHistoryItem.datasourceUid });
|
|
||||||
}
|
|
||||||
setQueries(exploreId, queriesToRun);
|
|
||||||
|
|
||||||
reportInteraction('grafana_explore_query_history_run', {
|
|
||||||
queryHistoryEnabled: config.queryHistoryEnabled,
|
|
||||||
differentDataSource,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCopyQuery = async () => {
|
const onCopyQuery = async () => {
|
||||||
const datasources = [...queryHistoryItem.queries.map((query) => query.datasource?.type || 'unknown')];
|
const datasources = [...queryHistoryItem.queries.map((query) => query.datasource?.type || 'unknown')];
|
||||||
reportInteraction('grafana_explore_query_history_copy_query', {
|
reportInteraction('grafana_explore_query_history_copy_query', {
|
||||||
@ -344,68 +313,6 @@ export function RichHistoryCard(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// exploreId on where the query will be ran, and the datasource ID for the item's DS
|
|
||||||
const runQueryText = (exploreId: string, dsUid: string) => {
|
|
||||||
return dsUid !== undefined && exploreId !== undefined && isDifferentDatasource(dsUid, exploreId)
|
|
||||||
? {
|
|
||||||
fallbackText: 'Switch data source and run query',
|
|
||||||
translation: t('explore.rich-history-card.switch-datasource-button', 'Switch data source and run query'),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
fallbackText: 'Run query',
|
|
||||||
translation: t('explore.rich-history-card.run-query-button', 'Run query'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const runButton = () => {
|
|
||||||
const disabled = cardRootDatasource?.uid === undefined;
|
|
||||||
if (!isPaneSplit) {
|
|
||||||
const exploreId = exploreActiveDS.exploreToDS[0]?.exploreId; // may be undefined if explore is refreshed while the pane is up
|
|
||||||
const buttonText = runQueryText(exploreId, props.queryHistoryItem.datasourceUid);
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
aria-label={buttonText.translation}
|
|
||||||
onClick={() => onRunQuery(exploreId)}
|
|
||||||
disabled={disabled || exploreId === undefined}
|
|
||||||
>
|
|
||||||
{buttonText.translation}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const menu = (
|
|
||||||
<Menu>
|
|
||||||
{panesEntries.map((pane, i) => {
|
|
||||||
const buttonText = runQueryText(pane[0], props.queryHistoryItem.datasourceUid);
|
|
||||||
const paneLabel =
|
|
||||||
i === 0
|
|
||||||
? t('explore.rich-history-card.left-pane', 'Left pane')
|
|
||||||
: t('explore.rich-history-card.right-pane', 'Right pane');
|
|
||||||
return (
|
|
||||||
<Menu.Item
|
|
||||||
key={i}
|
|
||||||
ariaLabel={buttonText.fallbackText}
|
|
||||||
onClick={() => {
|
|
||||||
onRunQuery(pane[0]);
|
|
||||||
}}
|
|
||||||
label={`${paneLabel}: ${buttonText.translation}`}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown onVisibleChange={(state) => setOpenRunQueryButton(state)} placement="bottom-start" overlay={menu}>
|
|
||||||
<ToolbarButton aria-label="run query options" variant="canvas" isOpen={openRunQueryButton}>
|
|
||||||
{t('explore.rich-history-card.run-query-button', 'Run query')}
|
|
||||||
</ToolbarButton>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.queryCard}>
|
<div className={styles.queryCard}>
|
||||||
<div className={styles.cardRow}>
|
<div className={styles.cardRow}>
|
||||||
@ -435,7 +342,11 @@ export function RichHistoryCard(props: Props) {
|
|||||||
)}
|
)}
|
||||||
{activeUpdateComment && updateComment}
|
{activeUpdateComment && updateComment}
|
||||||
</div>
|
</div>
|
||||||
{!activeUpdateComment && <div className={styles.runButton}>{runButton()}</div>}
|
{!activeUpdateComment && (
|
||||||
|
<div className={styles.runButton}>
|
||||||
|
<ExploreRunQueryButton queries={queryHistoryItem.queries} rootDatasourceUid={cardRootDatasource?.uid} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -491,15 +491,11 @@
|
|||||||
"delete-query-tooltip": "Delete query",
|
"delete-query-tooltip": "Delete query",
|
||||||
"delete-starred-query-confirmation-text": "Are you sure you want to permanently delete your starred query?",
|
"delete-starred-query-confirmation-text": "Are you sure you want to permanently delete your starred query?",
|
||||||
"edit-comment-tooltip": "Edit comment",
|
"edit-comment-tooltip": "Edit comment",
|
||||||
"left-pane": "Left pane",
|
|
||||||
"optional-description": "An optional description of what the query does.",
|
"optional-description": "An optional description of what the query does.",
|
||||||
"query-comment-label": "Query comment",
|
"query-comment-label": "Query comment",
|
||||||
"query-text-label": "Query text",
|
"query-text-label": "Query text",
|
||||||
"right-pane": "Right pane",
|
|
||||||
"run-query-button": "Run query",
|
|
||||||
"save-comment": "Save comment",
|
"save-comment": "Save comment",
|
||||||
"star-query-tooltip": "Star query",
|
"star-query-tooltip": "Star query",
|
||||||
"switch-datasource-button": "Switch data source and run query",
|
|
||||||
"unstar-query-tooltip": "Unstar query",
|
"unstar-query-tooltip": "Unstar query",
|
||||||
"update-comment-form": "Update comment form"
|
"update-comment-form": "Update comment form"
|
||||||
},
|
},
|
||||||
@ -568,6 +564,12 @@
|
|||||||
"saving-failed": "Saving rich history failed",
|
"saving-failed": "Saving rich history failed",
|
||||||
"update-failed": "Rich History update failed"
|
"update-failed": "Rich History update failed"
|
||||||
},
|
},
|
||||||
|
"run-query": {
|
||||||
|
"left-pane": "Left pane",
|
||||||
|
"right-pane": "Right pane",
|
||||||
|
"run-query-button": "Run query",
|
||||||
|
"switch-datasource-button": "Switch data source and run query"
|
||||||
|
},
|
||||||
"secondary-actions": {
|
"secondary-actions": {
|
||||||
"query-add-button": "Add query",
|
"query-add-button": "Add query",
|
||||||
"query-add-button-aria-label": "Add query",
|
"query-add-button-aria-label": "Add query",
|
||||||
|
@ -491,15 +491,11 @@
|
|||||||
"delete-query-tooltip": "Đęľęŧę qūęřy",
|
"delete-query-tooltip": "Đęľęŧę qūęřy",
|
||||||
"delete-starred-query-confirmation-text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő pęřmäʼnęʼnŧľy đęľęŧę yőūř şŧäřřęđ qūęřy?",
|
"delete-starred-query-confirmation-text": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő pęřmäʼnęʼnŧľy đęľęŧę yőūř şŧäřřęđ qūęřy?",
|
||||||
"edit-comment-tooltip": "Ēđįŧ čőmmęʼnŧ",
|
"edit-comment-tooltip": "Ēđįŧ čőmmęʼnŧ",
|
||||||
"left-pane": "Ŀęƒŧ päʼnę",
|
|
||||||
"optional-description": "Åʼn őpŧįőʼnäľ đęşčřįpŧįőʼn őƒ ŵĥäŧ ŧĥę qūęřy đőęş.",
|
"optional-description": "Åʼn őpŧįőʼnäľ đęşčřįpŧįőʼn őƒ ŵĥäŧ ŧĥę qūęřy đőęş.",
|
||||||
"query-comment-label": "Qūęřy čőmmęʼnŧ",
|
"query-comment-label": "Qūęřy čőmmęʼnŧ",
|
||||||
"query-text-label": "Qūęřy ŧęχŧ",
|
"query-text-label": "Qūęřy ŧęχŧ",
|
||||||
"right-pane": "Ŗįģĥŧ päʼnę",
|
|
||||||
"run-query-button": "Ŗūʼn qūęřy",
|
|
||||||
"save-comment": "Ŝävę čőmmęʼnŧ",
|
"save-comment": "Ŝävę čőmmęʼnŧ",
|
||||||
"star-query-tooltip": "Ŝŧäř qūęřy",
|
"star-query-tooltip": "Ŝŧäř qūęřy",
|
||||||
"switch-datasource-button": "Ŝŵįŧčĥ đäŧä şőūřčę äʼnđ řūʼn qūęřy",
|
|
||||||
"unstar-query-tooltip": "Ůʼnşŧäř qūęřy",
|
"unstar-query-tooltip": "Ůʼnşŧäř qūęřy",
|
||||||
"update-comment-form": "Ůpđäŧę čőmmęʼnŧ ƒőřm"
|
"update-comment-form": "Ůpđäŧę čőmmęʼnŧ ƒőřm"
|
||||||
},
|
},
|
||||||
@ -568,6 +564,12 @@
|
|||||||
"saving-failed": "Ŝävįʼnģ řįčĥ ĥįşŧőřy ƒäįľęđ",
|
"saving-failed": "Ŝävįʼnģ řįčĥ ĥįşŧőřy ƒäįľęđ",
|
||||||
"update-failed": "Ŗįčĥ Ħįşŧőřy ūpđäŧę ƒäįľęđ"
|
"update-failed": "Ŗįčĥ Ħįşŧőřy ūpđäŧę ƒäįľęđ"
|
||||||
},
|
},
|
||||||
|
"run-query": {
|
||||||
|
"left-pane": "Ŀęƒŧ päʼnę",
|
||||||
|
"right-pane": "Ŗįģĥŧ päʼnę",
|
||||||
|
"run-query-button": "Ŗūʼn qūęřy",
|
||||||
|
"switch-datasource-button": "Ŝŵįŧčĥ đäŧä şőūřčę äʼnđ řūʼn qūęřy"
|
||||||
|
},
|
||||||
"secondary-actions": {
|
"secondary-actions": {
|
||||||
"query-add-button": "Åđđ qūęřy",
|
"query-add-button": "Åđđ qūęřy",
|
||||||
"query-add-button-aria-label": "Åđđ qūęřy",
|
"query-add-button-aria-label": "Åđđ qūęřy",
|
||||||
|
Loading…
Reference in New Issue
Block a user