mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
1716b706da
commit
2ca6df814e
@ -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
|
||||
|
||||

|
||||
|
@ -1,9 +1,12 @@
|
||||
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];
|
||||
}
|
||||
|
@ -1,7 +1,17 @@
|
||||
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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user