grafana/public/app/plugins/datasource/jaeger/QueryField.tsx
Piotr Jamróz f97384c2f9
Replace AppEvents with notifyApp in Explore (#31864)
* Replace AppEvents with notifyApp in Explore

* Replace AppEvents with notifyApp in Explore-related components
2021-03-15 15:11:52 +01:00

242 lines
7.1 KiB
TypeScript

import { ExploreQueryFieldProps } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { ButtonCascader, CascaderOption } from '@grafana/ui';
import React from 'react';
import { JaegerDatasource, JaegerQuery } from './datasource';
import { Span, TraceResponse } from './types';
import { dispatch } from 'app/store/store';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
const ALL_OPERATIONS_KEY = '__ALL__';
const NO_TRACES_KEY = '__NO_TRACES__';
type Props = ExploreQueryFieldProps<JaegerDatasource, JaegerQuery>;
interface State {
serviceOptions: CascaderOption[];
}
function findRootSpan(spans: Span[]): Span | undefined {
return spans.find((s) => !s.references?.length);
}
function getLabelFromTrace(trace: TraceResponse): string {
const rootSpan = findRootSpan(trace.spans);
if (rootSpan) {
return `${rootSpan.operationName} [${rootSpan.duration / 1000} ms]`;
}
return trace.traceID;
}
export class JaegerQueryField extends React.PureComponent<Props, State> {
private _isMounted: boolean;
constructor(props: Props, context: React.Context<any>) {
super(props, context);
this.state = {
serviceOptions: [],
};
}
componentDidMount() {
this._isMounted = true;
// We should probably call this periodically to get new services after mount.
this.getServices();
}
componentWillUnmount(): void {
this._isMounted = false;
}
async getServices() {
const url = '/api/services';
const { datasource } = this.props;
try {
const services: string[] | null = await datasource.metadataRequest(url);
if (!this._isMounted) {
return;
}
if (services) {
const serviceOptions: CascaderOption[] = services.sort().map((service) => ({
label: service,
value: service,
isLeaf: false,
}));
this.setState({ serviceOptions });
}
} catch (error) {
dispatch(notifyApp(createErrorNotification('Failed to load services from Jaeger', error)));
}
}
onLoadOptions = async (selectedOptions: CascaderOption[]) => {
const service = selectedOptions[0].value;
if (selectedOptions.length === 1) {
// Load operations
const operations: string[] = await this.findOperations(service);
if (!this._isMounted) {
return;
}
const allOperationsOption: CascaderOption = {
label: '[ALL]',
value: ALL_OPERATIONS_KEY,
};
const operationOptions: CascaderOption[] = [
allOperationsOption,
...operations.sort().map((operation) => ({
label: operation,
value: operation,
isLeaf: false,
})),
];
this.setState((state) => {
const serviceOptions = state.serviceOptions.map((serviceOption) => {
if (serviceOption.value === service) {
return {
...serviceOption,
children: operationOptions,
};
}
return serviceOption;
});
return { serviceOptions };
});
} else if (selectedOptions.length === 2) {
// Load traces
const operationValue = selectedOptions[1].value;
const operation = operationValue === ALL_OPERATIONS_KEY ? '' : operationValue;
const traces: any[] = await this.findTraces(service, operation);
if (!this._isMounted) {
return;
}
let traceOptions: CascaderOption[] = traces.map((trace) => ({
label: getLabelFromTrace(trace),
value: trace.traceID,
}));
if (traceOptions.length === 0) {
traceOptions = [
{
label: '[No traces in time range]',
value: NO_TRACES_KEY,
},
];
}
this.setState((state) => {
// Place new traces into the correct service/operation sub-tree
const serviceOptions = state.serviceOptions.map((serviceOption) => {
if (serviceOption.value === service && serviceOption.children) {
const operationOptions = serviceOption.children.map((operationOption) => {
if (operationOption.value === operationValue) {
return {
...operationOption,
children: traceOptions,
};
}
return operationOption;
});
return {
...serviceOption,
children: operationOptions,
};
}
return serviceOption;
});
return { serviceOptions };
});
}
};
findOperations = async (service: string) => {
const { datasource } = this.props;
const url = `/api/services/${encodeURIComponent(service)}/operations`;
try {
return await datasource.metadataRequest(url);
} catch (error) {
dispatch(notifyApp(createErrorNotification('Failed to load operations from Jaeger', error)));
}
return [];
};
findTraces = async (service: string, operation?: string) => {
const { datasource } = this.props;
const { start, end } = datasource.getTimeRange();
const traceSearch = {
start,
end,
service,
operation,
limit: 10,
lookback: '1h',
maxDuration: '',
minDuration: '',
};
const url = '/api/traces';
try {
return await datasource.metadataRequest(url, traceSearch);
} catch (error) {
dispatch(notifyApp(createErrorNotification('Failed to load traces from Jaeger', error)));
}
return [];
};
onSelectTrace = (values: string[], selectedOptions: CascaderOption[]) => {
const { query, onChange, onRunQuery } = this.props;
if (selectedOptions.length === 3) {
const traceID = selectedOptions[2].value;
onChange({ ...query, query: traceID });
onRunQuery();
}
};
render() {
const { query, onChange } = this.props;
const { serviceOptions } = this.state;
const cascaderOptions = serviceOptions && serviceOptions.length ? serviceOptions : noTracesFoundOptions;
return (
<>
<div className="gf-form-inline gf-form-inline--nowrap">
<div className="gf-form flex-shrink-0">
<ButtonCascader options={cascaderOptions} onChange={this.onSelectTrace} loadData={this.onLoadOptions}>
Traces
</ButtonCascader>
</div>
<div className="gf-form gf-form--grow flex-shrink-1">
<div className="slate-query-field__wrapper">
<div className="slate-query-field" aria-label={selectors.components.QueryField.container}>
<input
style={{ width: '100%' }}
value={query.query || ''}
onChange={(e) =>
onChange({
...query,
query: e.currentTarget.value,
})
}
/>
</div>
</div>
</div>
</div>
</>
);
}
}
const noTracesFoundOptions = [
{
label: 'No traces found',
value: 'no_traces',
isLeaf: true,
// Cannot be disabled because then cascader shows 'loading' for some reason.
// disabled: true,
},
];
export default JaegerQueryField;