Commit 2ca6df81 by Andrej Ocenas Committed by GitHub

Elastic: Internal data links (#25942)

* Allow internal datalinks for elastic

* Add docs

* Update docs/sources/features/datasources/elasticsearch.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
parent 1716b706
......@@ -91,6 +91,15 @@ For example, if you're using a default setup of Filebeat for shipping logs to El
- **Message field name:** message
- **Level field name:** fields.level
### Data links
Data links create a link from a specified field that can be accessed in logs view in Explore.
Each data link configuration consists of:
- **Field -** Name of the field used by the data link.
- **URL/query -** If the link is external, then enter the full link URL. If the link is internal link, then this input serves as query for the target data source. In both cases, you can interpolate the value from the field with `${__value.raw }` macro.
- **Internal link -** Select if the link is internal or external. In case of internal link, a data source selectorallows you to select the target data source. Only tracing data sources are supported.
## Metric Query editor
![Elasticsearch Query Editor](/img/docs/elasticsearch/query_editor.png)
......
import React from 'react';
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { css } from 'emotion';
import { VariableSuggestion } from '@grafana/data';
import { DataSourceSelectItem, VariableSuggestion } from '@grafana/data';
import { Button, LegacyForms, DataLinkInput, stylesFactory } from '@grafana/ui';
const { FormField } = LegacyForms;
const { FormField, Switch } = LegacyForms;
import { DataLinkConfig } from '../types';
import { usePrevious } from 'react-use';
import { getDatasourceSrv } from '../../../../features/plugins/datasource_srv';
import DataSourcePicker from '../../../../core/components/Select/DataSourcePicker';
const getStyles = stylesFactory(() => ({
firstRow: css`
......@@ -15,6 +18,10 @@ const getStyles = stylesFactory(() => ({
regexField: css`
flex: 3;
`,
row: css`
display: flex;
align-items: baseline;
`,
}));
type Props = {
......@@ -27,6 +34,7 @@ type Props = {
export const DataLink = (props: Props) => {
const { value, onChange, onDelete, suggestions, className } = props;
const styles = getStyles();
const [showInternalLink, setShowInternalLink] = useInternalLink(value.datasourceUid);
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
onChange({
......@@ -61,11 +69,11 @@ export const DataLink = (props: Props) => {
</div>
<div className="gf-form">
<FormField
label="URL"
label={showInternalLink ? 'Query' : 'URL'}
labelWidth={6}
inputEl={
<DataLinkInput
placeholder={'http://example.com/${__value.raw}'}
placeholder={showInternalLink ? '${__value.raw}' : 'http://example.com/${__value.raw}'}
value={value.url || ''}
onChange={newValue =>
onChange({
......@@ -81,6 +89,82 @@ export const DataLink = (props: Props) => {
`}
/>
</div>
<div className={styles.row}>
<Switch
label="Internal link"
checked={showInternalLink}
onChange={() => {
if (showInternalLink) {
onChange({
...value,
datasourceUid: undefined,
});
}
setShowInternalLink(!showInternalLink);
}}
/>
{showInternalLink && (
<DataSourceSection
onChange={datasourceUid => {
onChange({
...value,
datasourceUid,
});
}}
datasourceUid={value.datasourceUid}
/>
)}
</div>
</div>
);
};
type DataSourceSectionProps = {
datasourceUid?: string;
onChange: (uid: string) => void;
};
const DataSourceSection = (props: DataSourceSectionProps) => {
const { datasourceUid, onChange } = props;
const datasources: DataSourceSelectItem[] = getDatasourceSrv()
.getExternal()
// At this moment only Jaeger and Zipkin datasource is supported as the link target.
.filter(ds => ds.meta.tracing)
.map(
ds =>
({
value: ds.uid,
name: ds.name,
meta: ds.meta,
} as DataSourceSelectItem)
);
let selectedDatasource = datasourceUid && datasources.find(d => d.value === datasourceUid);
return (
<DataSourcePicker
// Uid and value should be always set in the db and so in the items.
onChange={ds => onChange(ds.value!)}
datasources={datasources}
current={selectedDatasource || undefined}
/>
);
};
function useInternalLink(datasourceUid: string): [boolean, Dispatch<SetStateAction<boolean>>] {
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
const previousUid = usePrevious(datasourceUid);
// Force internal link visibility change if uid changed outside of this component.
useEffect(() => {
if (!previousUid && datasourceUid && !showInternalLink) {
setShowInternalLink(true);
}
if (previousUid && !datasourceUid && showInternalLink) {
setShowInternalLink(false);
}
}, [previousUid, datasourceUid, showInternalLink]);
return [showInternalLink, setShowInternalLink];
}
import angular from 'angular';
import { CoreApp, DataQueryRequest, DataSourceInstanceSettings, dateMath, dateTime, Field, toUtc } from '@grafana/data';
import {
ArrayVector,
CoreApp,
DataQueryRequest,
DataSourceInstanceSettings,
dateMath,
dateTime,
Field,
MutableDataFrame,
toUtc,
} from '@grafana/data';
import _ from 'lodash';
import { ElasticDatasource } from './datasource';
import { ElasticDatasource, enhanceDataFrame } from './datasource';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
......@@ -849,6 +859,49 @@ describe('ElasticDatasource', function(this: any) {
});
});
describe('enhanceDataFrame', () => {
it('adds links to dataframe', () => {
const df = new MutableDataFrame({
fields: [
{
name: 'urlField',
values: new ArrayVector([]),
},
{
name: 'traceField',
values: new ArrayVector([]),
},
],
});
enhanceDataFrame(df, [
{
field: 'urlField',
url: 'someUrl',
},
{
field: 'traceField',
url: 'query',
datasourceUid: 'dsUid',
},
]);
expect(df.fields[0].config.links.length).toBe(1);
expect(df.fields[0].config.links[0]).toEqual({
title: '',
url: 'someUrl',
});
expect(df.fields[1].config.links.length).toBe(1);
expect(df.fields[1].config.links[0]).toEqual({
title: '',
url: '',
internal: {
query: { query: 'query' },
datasourceUid: 'dsUid',
},
});
});
});
const createElasticQuery = (): DataQueryRequest<ElasticsearchQuery> => {
return {
requestId: '',
......
......@@ -7,6 +7,7 @@ import {
DataQueryResponse,
DataFrame,
ScopedVars,
DataLink,
} from '@grafana/data';
import { ElasticResponse } from './elastic_response';
import { IndexPattern } from './index_pattern';
......@@ -404,7 +405,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
if (sentTargets.some(target => target.isLogsQuery)) {
const response = er.getLogs(this.logMessageField, this.logLevelField);
for (const dataFrame of response.data) {
this.enhanceDataFrame(dataFrame);
enhanceDataFrame(dataFrame, this.dataLinks);
}
return response;
}
......@@ -584,24 +585,6 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
return false;
}
enhanceDataFrame(dataFrame: DataFrame) {
if (this.dataLinks.length) {
for (const field of dataFrame.fields) {
const dataLink = this.dataLinks.find(dataLink => field.name && field.name.match(dataLink.field));
if (dataLink) {
field.config = field.config || {};
field.config.links = [
...(field.config.links || []),
{
url: dataLink.url,
title: '',
},
];
}
}
}
}
private isPrimitive(obj: any) {
if (obj === null || obj === undefined) {
return true;
......@@ -639,3 +622,35 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
return false;
}
}
/**
* Modifies dataframe and adds dataLinks from the config.
* Exported for tests.
*/
export function enhanceDataFrame(dataFrame: DataFrame, dataLinks: DataLinkConfig[]) {
if (dataLinks.length) {
for (const field of dataFrame.fields) {
const dataLinkConfig = dataLinks.find(dataLink => field.name && field.name.match(dataLink.field));
if (dataLinkConfig) {
let link: DataLink;
if (dataLinkConfig.datasourceUid) {
link = {
title: '',
url: '',
internal: {
query: { query: dataLinkConfig.url },
datasourceUid: dataLinkConfig.datasourceUid,
},
};
} else {
link = {
title: '',
url: dataLinkConfig.url,
};
}
field.config = field.config || {};
field.config.links = [...(field.config.links || []), link];
}
}
}
}
......@@ -29,4 +29,5 @@ export interface ElasticsearchQuery extends DataQuery {
export type DataLinkConfig = {
field: string;
url: string;
datasourceUid?: string;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment