mirror of https://github.com/grafana/grafana
Explore / Query Library: Enable run button (#87882)
* Enable run button * First pass of shared component, tests half-implemented * cleanuppull/87532/head
parent
3800b97a5b
commit
c8d237dd56
@ -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' }); |
||||
}); |
||||
}); |
||||
}); |
@ -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 { Button } from '@grafana/ui'; |
||||
|
||||
export function ActionsCell() { |
||||
return ( |
||||
<> |
||||
<Button disabled={true} variant="primary"> |
||||
Run |
||||
</Button> |
||||
</> |
||||
); |
||||
import { DataQuery } from '@grafana/schema'; |
||||
|
||||
import ExploreRunQueryButton from '../../ExploreRunQueryButton'; |
||||
|
||||
interface ActionsCellProps { |
||||
query?: DataQuery; |
||||
rootDatasourceUid?: string; |
||||
} |
||||
|
||||
function ActionsCell({ query, rootDatasourceUid }: ActionsCellProps) { |
||||
return <ExploreRunQueryButton queries={query ? [query] : []} rootDatasourceUid={rootDatasourceUid} />; |
||||
} |
||||
|
||||
export default ActionsCell; |
||||
|
Loading…
Reference in new issue