mirror of https://github.com/grafana/grafana
Explore: Reuse Dashboard's QueryRows component (#38942)
* WIP * Functional without custom wrapper component, needs highlight * Remove latency from explore * Sync eventbus * Some cleanup & removal of unused code * Avoid clearing queries when running all empty queries * Run remaining queries when removing one * Update snapshots * fix failing tests * type cleanup * Refactor QueryRows * update snapshot * Remove highlighter expressions * minor fixes in queryrows * remove unwanted change * fix failing e2e test * Persist refId in explore url state * make traces test slightly more robust * add test for query duplicationpull/39254/head
parent
e251863085
commit
f79173c99d
@ -1,104 +0,0 @@ |
||||
// Libraries
|
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
// Services
|
||||
import { getAngularLoader, AngularComponent } from '@grafana/runtime'; |
||||
|
||||
// Types
|
||||
import { DataQuery, TimeRange, EventBusExtended } from '@grafana/data'; |
||||
import 'app/features/plugins/plugin_loader'; |
||||
|
||||
interface QueryEditorProps { |
||||
error?: any; |
||||
datasource: any; |
||||
onExecuteQuery?: () => void; |
||||
onQueryChange?: (value: DataQuery) => void; |
||||
initialQuery: DataQuery; |
||||
exploreEvents: EventBusExtended; |
||||
range: TimeRange; |
||||
textEditModeEnabled?: boolean; |
||||
} |
||||
|
||||
export default class QueryEditor extends PureComponent<QueryEditorProps, any> { |
||||
element: any; |
||||
component?: AngularComponent; |
||||
angularScope: any; |
||||
|
||||
async componentDidMount() { |
||||
if (!this.element) { |
||||
return; |
||||
} |
||||
|
||||
const { datasource, initialQuery, exploreEvents, range } = this.props; |
||||
|
||||
const loader = getAngularLoader(); |
||||
const template = '<plugin-component type="query-ctrl"> </plugin-component>'; |
||||
const target = { datasource: datasource.name, ...initialQuery }; |
||||
const scopeProps = { |
||||
ctrl: { |
||||
datasource, |
||||
target, |
||||
range, |
||||
refresh: () => { |
||||
setTimeout(() => { |
||||
// the "hide" attribute of the quries can be changed from the "outside",
|
||||
// it will be applied to "this.props.initialQuery.hide", but not to "target.hide".
|
||||
// so we have to apply it.
|
||||
if (target.hide !== this.props.initialQuery.hide) { |
||||
target.hide = this.props.initialQuery.hide; |
||||
} |
||||
this.props.onQueryChange?.(target); |
||||
this.props.onExecuteQuery?.(); |
||||
}, 1); |
||||
}, |
||||
onQueryChange: () => { |
||||
setTimeout(() => { |
||||
this.props.onQueryChange?.(target); |
||||
}, 1); |
||||
}, |
||||
events: exploreEvents, |
||||
panel: { datasource, targets: [target] }, |
||||
dashboard: {}, |
||||
}, |
||||
}; |
||||
|
||||
this.component = loader.load(this.element, scopeProps, template); |
||||
this.angularScope = scopeProps.ctrl; |
||||
|
||||
setTimeout(() => { |
||||
this.props.onQueryChange?.(target); |
||||
this.props.onExecuteQuery?.(); |
||||
}, 1); |
||||
} |
||||
|
||||
componentDidUpdate(prevProps: QueryEditorProps) { |
||||
const hasToggledEditorMode = prevProps.textEditModeEnabled !== this.props.textEditModeEnabled; |
||||
const hasNewError = prevProps.error !== this.props.error; |
||||
|
||||
if (this.component) { |
||||
if (hasToggledEditorMode && this.angularScope && this.angularScope.toggleEditorMode) { |
||||
this.angularScope.toggleEditorMode(); |
||||
} |
||||
|
||||
if (this.angularScope) { |
||||
this.angularScope.range = this.props.range; |
||||
} |
||||
|
||||
if (hasNewError || hasToggledEditorMode) { |
||||
// Some query controllers listen to data error events and need a digest
|
||||
// for some reason this needs to be done in next tick
|
||||
setTimeout(this.component.digest); |
||||
} |
||||
} |
||||
} |
||||
|
||||
componentWillUnmount() { |
||||
if (this.component) { |
||||
this.component.destroy(); |
||||
} |
||||
} |
||||
|
||||
render() { |
||||
return <div className="gf-form-query" ref={(element) => (this.element = element)} style={{ width: '100%' }} />; |
||||
} |
||||
} |
||||
@ -1,47 +0,0 @@ |
||||
import React, { ComponentProps } from 'react'; |
||||
import { QueryRow } from './QueryRow'; |
||||
import { shallow } from 'enzyme'; |
||||
import { ExploreId } from 'app/types/explore'; |
||||
import { DataSourceApi, TimeRange, AbsoluteTimeRange, PanelData, EventBusExtended } from '@grafana/data'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: ComponentProps<typeof QueryRow> = { |
||||
exploreId: ExploreId.left, |
||||
index: 1, |
||||
exploreEvents: {} as EventBusExtended, |
||||
changeQuery: jest.fn(), |
||||
datasourceInstance: {} as DataSourceApi, |
||||
highlightLogsExpressionAction: jest.fn() as any, |
||||
history: [], |
||||
query: { |
||||
refId: 'A', |
||||
}, |
||||
modifyQueries: jest.fn(), |
||||
range: {} as TimeRange, |
||||
absoluteRange: {} as AbsoluteTimeRange, |
||||
removeQueryRowAction: jest.fn() as any, |
||||
runQueries: jest.fn(), |
||||
queryResponse: {} as PanelData, |
||||
latency: 1, |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
const wrapper = shallow(<QueryRow {...props} />); |
||||
return wrapper; |
||||
}; |
||||
|
||||
const QueryEditor = () => <div />; |
||||
|
||||
describe('QueryRow', () => { |
||||
describe('if datasource does not have Explore query fields ', () => { |
||||
it('it should render QueryEditor if datasource has it', () => { |
||||
const wrapper = setup({ datasourceInstance: { components: { QueryEditor } } }); |
||||
expect(wrapper.find(QueryEditor)).toHaveLength(1); |
||||
}); |
||||
it('it should not render QueryEditor if datasource does not have it', () => { |
||||
const wrapper = setup({ datasourceInstance: { components: {} } }); |
||||
expect(wrapper.find(QueryEditor)).toHaveLength(0); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,203 +0,0 @@ |
||||
// Libraries
|
||||
import React, { PureComponent } from 'react'; |
||||
import { debounce, has } from 'lodash'; |
||||
import { connect, ConnectedProps } from 'react-redux'; |
||||
import AngularQueryEditor from './QueryEditor'; |
||||
import { QueryRowActions } from './QueryRowActions'; |
||||
import { StoreState } from 'app/types'; |
||||
import { DataQuery, LoadingState, DataSourceApi } from '@grafana/data'; |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
import { ExploreItemState, ExploreId } from 'app/types/explore'; |
||||
import { highlightLogsExpressionAction } from './state/explorePane'; |
||||
import { ErrorContainer } from './ErrorContainer'; |
||||
import { changeQuery, modifyQueries, removeQueryRowAction, runQueries } from './state/query'; |
||||
import { HelpToggle } from '../query/components/HelpToggle'; |
||||
|
||||
interface OwnProps { |
||||
exploreId: ExploreId; |
||||
index: number; |
||||
} |
||||
|
||||
type QueryRowProps = OwnProps & ConnectedProps<typeof connector>; |
||||
|
||||
interface QueryRowState { |
||||
textEditModeEnabled: boolean; |
||||
} |
||||
|
||||
// Empty function to override blur execution on query field
|
||||
const noopOnBlur = () => {}; |
||||
|
||||
export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> { |
||||
state: QueryRowState = { |
||||
textEditModeEnabled: false, |
||||
}; |
||||
|
||||
onRunQuery = () => { |
||||
const { exploreId } = this.props; |
||||
this.props.runQueries(exploreId); |
||||
}; |
||||
|
||||
onChange = (query: DataQuery, override?: boolean) => { |
||||
const { datasourceInstance, exploreId, index } = this.props; |
||||
this.props.changeQuery(exploreId, query, index, override); |
||||
if (query && !override && datasourceInstance?.getHighlighterExpression && index === 0) { |
||||
// Live preview of log search matches. Only use on first row for now
|
||||
this.updateLogsHighlights(query); |
||||
} |
||||
}; |
||||
|
||||
onClickToggleDisabled = () => { |
||||
const { exploreId, index, query } = this.props; |
||||
const newQuery = { |
||||
...query, |
||||
hide: !query.hide, |
||||
}; |
||||
this.props.changeQuery(exploreId, newQuery, index, true); |
||||
}; |
||||
|
||||
onClickRemoveButton = () => { |
||||
const { exploreId, index } = this.props; |
||||
this.props.removeQueryRowAction({ exploreId, index }); |
||||
this.props.runQueries(exploreId); |
||||
}; |
||||
|
||||
onClickToggleEditorMode = () => { |
||||
this.setState({ textEditModeEnabled: !this.state.textEditModeEnabled }); |
||||
}; |
||||
|
||||
setReactQueryEditor = (datasourceInstance: DataSourceApi) => { |
||||
let QueryEditor; |
||||
// TODO:unification
|
||||
if (datasourceInstance.components?.ExploreMetricsQueryField) { |
||||
QueryEditor = datasourceInstance.components.ExploreMetricsQueryField; |
||||
} else if (datasourceInstance.components?.ExploreLogsQueryField) { |
||||
QueryEditor = datasourceInstance.components.ExploreLogsQueryField; |
||||
} else if (datasourceInstance.components?.ExploreQueryField) { |
||||
QueryEditor = datasourceInstance.components.ExploreQueryField; |
||||
} else { |
||||
QueryEditor = datasourceInstance.components?.QueryEditor; |
||||
} |
||||
return QueryEditor; |
||||
}; |
||||
|
||||
renderQueryEditor = (datasourceInstance: DataSourceApi) => { |
||||
const { history, query, exploreEvents, range, queryResponse, exploreId } = this.props; |
||||
|
||||
const queryErrors = queryResponse.error && queryResponse.error.refId === query.refId ? [queryResponse.error] : []; |
||||
|
||||
const ReactQueryEditor = this.setReactQueryEditor(datasourceInstance); |
||||
|
||||
let QueryEditor: JSX.Element; |
||||
if (ReactQueryEditor) { |
||||
QueryEditor = ( |
||||
<ReactQueryEditor |
||||
datasource={datasourceInstance} |
||||
query={query} |
||||
history={history} |
||||
onRunQuery={this.onRunQuery} |
||||
onBlur={noopOnBlur} |
||||
onChange={this.onChange} |
||||
data={queryResponse} |
||||
range={range} |
||||
exploreId={exploreId} |
||||
/> |
||||
); |
||||
} else { |
||||
QueryEditor = ( |
||||
<AngularQueryEditor |
||||
error={queryErrors} |
||||
datasource={datasourceInstance} |
||||
onQueryChange={this.onChange} |
||||
onExecuteQuery={this.onRunQuery} |
||||
initialQuery={query} |
||||
exploreEvents={exploreEvents} |
||||
range={range} |
||||
textEditModeEnabled={this.state.textEditModeEnabled} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
const DatasourceCheatsheet = datasourceInstance.components?.QueryEditorHelp; |
||||
return ( |
||||
<> |
||||
{QueryEditor} |
||||
{DatasourceCheatsheet && ( |
||||
<HelpToggle> |
||||
<DatasourceCheatsheet onClickExample={(query) => this.onChange(query)} datasource={datasourceInstance!} /> |
||||
</HelpToggle> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
updateLogsHighlights = debounce((value: DataQuery) => { |
||||
const { datasourceInstance } = this.props; |
||||
if (datasourceInstance?.getHighlighterExpression) { |
||||
const { exploreId } = this.props; |
||||
const expressions = datasourceInstance.getHighlighterExpression(value); |
||||
this.props.highlightLogsExpressionAction({ exploreId, expressions }); |
||||
} |
||||
}, 500); |
||||
|
||||
render() { |
||||
const { datasourceInstance, query, queryResponse, latency } = this.props; |
||||
|
||||
if (!datasourceInstance) { |
||||
return <>Loading data source</>; |
||||
} |
||||
|
||||
const canToggleEditorModes = has(datasourceInstance, 'components.QueryCtrl.prototype.toggleEditorMode'); |
||||
const isNotStarted = queryResponse.state === LoadingState.NotStarted; |
||||
|
||||
// We show error without refId in ResponseErrorContainer so this condition needs to match se we don't loose errors.
|
||||
const queryErrors = queryResponse.error && queryResponse.error.refId === query.refId ? [queryResponse.error] : []; |
||||
|
||||
return ( |
||||
<> |
||||
<div className="query-row" aria-label={selectors.components.QueryEditorRows.rows}> |
||||
<div className="query-row-field flex-shrink-1">{this.renderQueryEditor(datasourceInstance)}</div> |
||||
<QueryRowActions |
||||
canToggleEditorModes={canToggleEditorModes} |
||||
isDisabled={query.hide} |
||||
isNotStarted={isNotStarted} |
||||
latency={latency} |
||||
onClickToggleEditorMode={this.onClickToggleEditorMode} |
||||
onClickToggleDisabled={this.onClickToggleDisabled} |
||||
onClickRemoveButton={this.onClickRemoveButton} |
||||
/> |
||||
</div> |
||||
{queryErrors.length > 0 && <ErrorContainer queryError={queryErrors[0]} />} |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function mapStateToProps(state: StoreState, { exploreId, index }: OwnProps) { |
||||
const explore = state.explore; |
||||
const item: ExploreItemState = explore[exploreId]!; |
||||
const { datasourceInstance, history, queries, range, absoluteRange, queryResponse, latency, eventBridge } = item; |
||||
const query = queries[index]; |
||||
|
||||
return { |
||||
datasourceInstance, |
||||
history, |
||||
query, |
||||
range, |
||||
absoluteRange, |
||||
queryResponse, |
||||
latency, |
||||
exploreEvents: eventBridge, |
||||
}; |
||||
} |
||||
|
||||
const mapDispatchToProps = { |
||||
changeQuery, |
||||
highlightLogsExpressionAction, |
||||
modifyQueries, |
||||
removeQueryRowAction, |
||||
runQueries, |
||||
}; |
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps); |
||||
|
||||
export default connector(QueryRow); |
||||
@ -1,39 +0,0 @@ |
||||
import React from 'react'; |
||||
import { QueryRowActions, Props } from './QueryRowActions'; |
||||
import { shallow } from 'enzyme'; |
||||
|
||||
const setup = (propOverrides?: object) => { |
||||
const props: Props = { |
||||
isDisabled: false, |
||||
isNotStarted: true, |
||||
canToggleEditorModes: true, |
||||
onClickToggleEditorMode: () => {}, |
||||
onClickToggleDisabled: () => {}, |
||||
onClickRemoveButton: () => {}, |
||||
latency: 0, |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
const wrapper = shallow(<QueryRowActions {...props} />); |
||||
return wrapper; |
||||
}; |
||||
|
||||
describe('QueryRowActions', () => { |
||||
it('should render component', () => { |
||||
const wrapper = setup(); |
||||
expect(wrapper).toMatchSnapshot(); |
||||
}); |
||||
it('should render component without editor mode', () => { |
||||
const wrapper = setup({ canToggleEditorModes: false }); |
||||
expect(wrapper.find({ 'aria-label': 'Edit mode button' })).toHaveLength(0); |
||||
}); |
||||
it('should change icon to eye-slash when query row result is hidden', () => { |
||||
const wrapper = setup({ isDisabled: true }); |
||||
expect(wrapper.find({ title: 'Enable query' })).toHaveLength(1); |
||||
}); |
||||
it('should change icon to eye when query row result is not hidden', () => { |
||||
const wrapper = setup({ isDisabled: false }); |
||||
expect(wrapper.find({ title: 'Disable query' })).toHaveLength(1); |
||||
}); |
||||
}); |
||||
@ -1,64 +0,0 @@ |
||||
import React from 'react'; |
||||
import { Icon } from '@grafana/ui'; |
||||
|
||||
function formatLatency(value: number) { |
||||
return `${(value / 1000).toFixed(1)}s`; |
||||
} |
||||
|
||||
export type Props = { |
||||
canToggleEditorModes: boolean; |
||||
isDisabled?: boolean; |
||||
isNotStarted: boolean; |
||||
latency: number; |
||||
onClickToggleEditorMode: () => void; |
||||
onClickToggleDisabled: () => void; |
||||
onClickRemoveButton: () => void; |
||||
}; |
||||
|
||||
export function QueryRowActions(props: Props) { |
||||
const { |
||||
canToggleEditorModes, |
||||
onClickToggleEditorMode, |
||||
onClickToggleDisabled, |
||||
onClickRemoveButton, |
||||
isDisabled, |
||||
isNotStarted, |
||||
latency, |
||||
} = props; |
||||
|
||||
return ( |
||||
<div className="gf-form-inline flex-shrink-0"> |
||||
{canToggleEditorModes && ( |
||||
<div className="gf-form"> |
||||
<button |
||||
aria-label="Edit mode button" |
||||
className="gf-form-label gf-form-label--btn" |
||||
onClick={onClickToggleEditorMode} |
||||
> |
||||
<Icon name="pen" /> |
||||
</button> |
||||
</div> |
||||
)} |
||||
<div className="gf-form"> |
||||
<button disabled className="gf-form-label" title="Query row latency"> |
||||
{formatLatency(latency)} |
||||
</button> |
||||
</div> |
||||
<div className="gf-form"> |
||||
<button |
||||
disabled={isNotStarted} |
||||
className="gf-form-label gf-form-label--btn" |
||||
onClick={onClickToggleDisabled} |
||||
title={isDisabled ? 'Enable query' : 'Disable query'} |
||||
> |
||||
<Icon name={isDisabled ? 'eye-slash' : 'eye'} /> |
||||
</button> |
||||
</div> |
||||
<div className="gf-form"> |
||||
<button className="gf-form-label gf-form-label--btn" onClick={onClickRemoveButton} title="Remove query"> |
||||
<Icon name="minus" /> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
@ -0,0 +1,80 @@ |
||||
import React from 'react'; |
||||
import { fireEvent, render, screen } from '@testing-library/react'; |
||||
import { configureStore } from 'app/store/configureStore'; |
||||
import { Provider } from 'react-redux'; |
||||
import { QueryRows } from './QueryRows'; |
||||
import { ExploreId, ExploreState } from 'app/types'; |
||||
import { makeExplorePaneState } from './state/utils'; |
||||
import { setDataSourceSrv } from '@grafana/runtime'; |
||||
import { UserState } from '../profile/state/reducers'; |
||||
import { DataQuery } from '../../../../packages/grafana-data/src'; |
||||
|
||||
function setup(queries: DataQuery[]) { |
||||
const defaultDs = { |
||||
name: 'newDs', |
||||
meta: { id: 'newDs' }, |
||||
}; |
||||
|
||||
const datasources: Record<string, any> = { |
||||
newDs: defaultDs, |
||||
someDs: { |
||||
name: 'someDs', |
||||
meta: { id: 'someDs' }, |
||||
components: { |
||||
QueryEditor: () => 'someDs query editor', |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
setDataSourceSrv({ |
||||
getList() { |
||||
return Object.values(datasources).map((d) => ({ name: d.name })); |
||||
}, |
||||
getInstanceSettings(name: string) { |
||||
return datasources[name] || defaultDs; |
||||
}, |
||||
get(name?: string) { |
||||
return Promise.resolve(name ? datasources[name] || defaultDs : defaultDs); |
||||
}, |
||||
} as any); |
||||
|
||||
const leftState = makeExplorePaneState(); |
||||
const initialState: ExploreState = { |
||||
left: { |
||||
...leftState, |
||||
datasourceInstance: datasources.someDs, |
||||
queries, |
||||
}, |
||||
syncedTimes: false, |
||||
right: undefined, |
||||
richHistory: [], |
||||
}; |
||||
const store = configureStore({ explore: initialState, user: { orgId: 1 } as UserState }); |
||||
|
||||
return { |
||||
store, |
||||
datasources, |
||||
}; |
||||
} |
||||
|
||||
describe('Explore QueryRows', () => { |
||||
it('Should duplicate a query and generate a valid refId', async () => { |
||||
const { store } = setup([{ refId: 'A' }]); |
||||
|
||||
render( |
||||
<Provider store={store}> |
||||
<QueryRows exploreId={ExploreId.left} /> |
||||
</Provider> |
||||
); |
||||
|
||||
// waiting for the d&d component to fully render.
|
||||
await screen.findAllByText('someDs query editor'); |
||||
|
||||
let duplicateButton = screen.getByTitle('Duplicate query'); |
||||
|
||||
fireEvent.click(duplicateButton); |
||||
|
||||
// We should have another row with refId B
|
||||
expect(await screen.findByLabelText('Query editor row title B')).toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
@ -1,27 +1,79 @@ |
||||
// Libraries
|
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
// Components
|
||||
import QueryRow from './QueryRow'; |
||||
|
||||
// Types
|
||||
import React, { useCallback, useMemo } from 'react'; |
||||
import { ExploreId } from 'app/types/explore'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
import { getDatasourceSrv } from '../plugins/datasource_srv'; |
||||
import { runQueries, changeQueriesAction } from './state/query'; |
||||
import { CoreApp, DataQuery } from '@grafana/data'; |
||||
import { getNextRefIdChar } from 'app/core/utils/query'; |
||||
import { QueryEditorRows } from '../query/components/QueryEditorRows'; |
||||
import { createSelector } from '@reduxjs/toolkit'; |
||||
import { getExploreItemSelector } from './state/selectors'; |
||||
|
||||
interface QueryRowsProps { |
||||
className?: string; |
||||
interface Props { |
||||
exploreId: ExploreId; |
||||
queryKeys: string[]; |
||||
} |
||||
|
||||
export default class QueryRows extends PureComponent<QueryRowsProps> { |
||||
render() { |
||||
const { className = '', exploreId, queryKeys } = this.props; |
||||
return ( |
||||
<div className={className}> |
||||
{queryKeys.map((key, index) => { |
||||
return <QueryRow key={key} exploreId={exploreId} index={index} />; |
||||
})} |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
const makeSelectors = (exploreId: ExploreId) => { |
||||
const exploreItemSelector = getExploreItemSelector(exploreId); |
||||
return { |
||||
getQueries: createSelector(exploreItemSelector, (s) => s!.queries), |
||||
getQueryResponse: createSelector(exploreItemSelector, (s) => s!.queryResponse), |
||||
getHistory: createSelector(exploreItemSelector, (s) => s!.history), |
||||
getEventBridge: createSelector(exploreItemSelector, (s) => s!.eventBridge), |
||||
getDatasourceInstanceSettings: createSelector( |
||||
exploreItemSelector, |
||||
(s) => getDatasourceSrv().getInstanceSettings(s!.datasourceInstance?.name)! |
||||
), |
||||
}; |
||||
}; |
||||
|
||||
export const QueryRows = ({ exploreId }: Props) => { |
||||
const dispatch = useDispatch(); |
||||
const { getQueries, getDatasourceInstanceSettings, getQueryResponse, getHistory, getEventBridge } = 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 onRunQueries = useCallback(() => { |
||||
dispatch(runQueries(exploreId)); |
||||
}, [dispatch, exploreId]); |
||||
|
||||
const onChange = useCallback( |
||||
(newQueries: DataQuery[]) => { |
||||
dispatch(changeQueriesAction({ queries: newQueries, exploreId })); |
||||
|
||||
// if we are removing a query we want to run the remaining ones
|
||||
if (newQueries.length < queries.length) { |
||||
onRunQueries(); |
||||
} |
||||
}, |
||||
[dispatch, exploreId, onRunQueries, queries] |
||||
); |
||||
|
||||
const onAddQuery = useCallback( |
||||
(query: DataQuery) => { |
||||
onChange([...queries, { ...query, refId: getNextRefIdChar(queries) }]); |
||||
}, |
||||
[onChange, queries] |
||||
); |
||||
|
||||
return ( |
||||
<QueryEditorRows |
||||
dsSettings={dsSettings} |
||||
queries={queries} |
||||
onQueriesChange={onChange} |
||||
onAddQuery={onAddQuery} |
||||
onRunQueries={onRunQueries} |
||||
data={queryResponse} |
||||
app={CoreApp.Explore} |
||||
history={history} |
||||
eventBus={eventBridge} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
@ -1,19 +0,0 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
|
||||
import { LoadingState, TimeRange, PanelData } from '@grafana/data'; |
||||
|
||||
import QueryStatus from './QueryStatus'; |
||||
|
||||
describe('<QueryStatus />', () => { |
||||
it('should render with a latency', () => { |
||||
const res: PanelData = { series: [], state: LoadingState.Done, timeRange: {} as TimeRange }; |
||||
const wrapper = shallow(<QueryStatus latency={0} queryResponse={res} />); |
||||
expect(wrapper.find('div').exists()).toBeTruthy(); |
||||
}); |
||||
it('should not render when query has not started', () => { |
||||
const res: PanelData = { series: [], state: LoadingState.NotStarted, timeRange: {} as TimeRange }; |
||||
const wrapper = shallow(<QueryStatus latency={0} queryResponse={res} />); |
||||
expect(wrapper.getElement()).toBe(null); |
||||
}); |
||||
}); |
||||
@ -1,52 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
import { ElapsedTime } from './ElapsedTime'; |
||||
import { PanelData, LoadingState } from '@grafana/data'; |
||||
|
||||
function formatLatency(value: number) { |
||||
return `${(value / 1000).toFixed(1)}s`; |
||||
} |
||||
|
||||
interface QueryStatusItemProps { |
||||
queryResponse: PanelData; |
||||
latency: number; |
||||
} |
||||
|
||||
class QueryStatusItem extends PureComponent<QueryStatusItemProps> { |
||||
render() { |
||||
const { queryResponse, latency } = this.props; |
||||
const className = |
||||
queryResponse.state === LoadingState.Done || LoadingState.Error |
||||
? 'query-transaction' |
||||
: 'query-transaction query-transaction--loading'; |
||||
return ( |
||||
<div className={className}> |
||||
{/* <div className="query-transaction__type">{transaction.resultType}:</div> */} |
||||
<div className="query-transaction__duration"> |
||||
{queryResponse.state === LoadingState.Done || LoadingState.Error ? formatLatency(latency) : <ElapsedTime />} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
interface QueryStatusProps { |
||||
queryResponse: PanelData; |
||||
latency: number; |
||||
} |
||||
|
||||
export default class QueryStatus extends PureComponent<QueryStatusProps> { |
||||
render() { |
||||
const { queryResponse, latency } = this.props; |
||||
|
||||
if (queryResponse.state === LoadingState.NotStarted) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div className="query-transactions"> |
||||
<QueryStatusItem queryResponse={queryResponse} latency={latency} /> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
@ -1,59 +0,0 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`QueryRowActions should render component 1`] = ` |
||||
<div |
||||
className="gf-form-inline flex-shrink-0" |
||||
> |
||||
<div |
||||
className="gf-form" |
||||
> |
||||
<button |
||||
aria-label="Edit mode button" |
||||
className="gf-form-label gf-form-label--btn" |
||||
onClick={[Function]} |
||||
> |
||||
<Icon |
||||
name="pen" |
||||
/> |
||||
</button> |
||||
</div> |
||||
<div |
||||
className="gf-form" |
||||
> |
||||
<button |
||||
className="gf-form-label" |
||||
disabled={true} |
||||
title="Query row latency" |
||||
> |
||||
0.0s |
||||
</button> |
||||
</div> |
||||
<div |
||||
className="gf-form" |
||||
> |
||||
<button |
||||
className="gf-form-label gf-form-label--btn" |
||||
disabled={true} |
||||
onClick={[Function]} |
||||
title="Disable query" |
||||
> |
||||
<Icon |
||||
name="eye" |
||||
/> |
||||
</button> |
||||
</div> |
||||
<div |
||||
className="gf-form" |
||||
> |
||||
<button |
||||
className="gf-form-label gf-form-label--btn" |
||||
onClick={[Function]} |
||||
title="Remove query" |
||||
> |
||||
<Icon |
||||
name="minus" |
||||
/> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
`; |
||||
@ -1,3 +1,5 @@ |
||||
import { ExploreId, StoreState } from 'app/types'; |
||||
|
||||
export const isSplit = (state: StoreState) => Boolean(state.explore[ExploreId.left] && state.explore[ExploreId.right]); |
||||
|
||||
export const getExploreItemSelector = (exploreId: ExploreId) => (state: StoreState) => state.explore[exploreId]; |
||||
|
||||
Loading…
Reference in new issue