mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
* Replace AppEvents with notifyApp in Explore * Replace AppEvents with notifyApp in Explore-related components
242 lines
7.1 KiB
TypeScript
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;
|