Add grafana-o11y-ds-frontend workspace package (#80362)

This commit is contained in:
Fabrizio
2024-01-22 13:47:50 +01:00
committed by GitHub
parent 3a10e480ba
commit 6cbc3df11e
57 changed files with 164 additions and 1463 deletions

View File

@@ -1,47 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
type Props = {
description: string;
suffix: string;
feature: string;
};
export function ConfigDescriptionLink(props: Props) {
const { description, suffix, feature } = props;
const text = `Learn more about ${feature}`;
const styles = useStyles2(getStyles);
return (
<span className={styles.container}>
{description}
<a
aria-label={text}
href={`https://grafana.com/docs/grafana/next/datasources/${suffix}`}
rel="noreferrer"
target="_blank"
>
{text}
</a>
</span>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
color: theme.colors.text.secondary,
a: css({
color: theme.colors.text.link,
textDecoration: 'underline',
marginLeft: '5px',
'&:hover': {
textDecoration: 'none',
},
}),
}),
};
};

View File

@@ -1,76 +0,0 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { useState } from 'react';
import { invalidTimeShiftError } from '../TraceToLogs/TraceToLogsSettings';
import { IntervalInput } from './IntervalInput';
describe('IntervalInput', () => {
const IntervalInputtWithProps = ({ val }: { val: string }) => {
const [value, setValue] = useState(val);
return (
<IntervalInput
label=""
tooltip=""
value={value}
disabled={false}
onChange={(v) => {
setValue(v);
}}
isInvalidError={invalidTimeShiftError}
/>
);
};
describe('validates time shift correctly', () => {
it('for previosuly saved invalid value', async () => {
render(<IntervalInputtWithProps val="77" />);
expect(screen.getByDisplayValue('77')).toBeInTheDocument();
expect(screen.getByText(invalidTimeShiftError)).toBeInTheDocument();
});
it('for previously saved empty value', async () => {
render(<IntervalInputtWithProps val="" />);
expect(screen.getByPlaceholderText('0')).toBeInTheDocument();
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
it('for empty (valid) value', async () => {
render(<IntervalInputtWithProps val="1ms" />);
await userEvent.clear(screen.getByDisplayValue('1ms'));
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
});
it('for valid value', async () => {
render(<IntervalInputtWithProps val="10ms" />);
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
const input = screen.getByDisplayValue('10ms');
await userEvent.clear(input);
await userEvent.type(input, '100s');
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
await userEvent.clear(input);
await userEvent.type(input, '-77ms');
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
});
it('for invalid value', async () => {
render(<IntervalInputtWithProps val="10ms" />);
const input = screen.getByDisplayValue('10ms');
await userEvent.clear(input);
await userEvent.type(input, 'abc');
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).toBeInTheDocument();
});
});
});
});

View File

@@ -1,69 +0,0 @@
import React, { useState } from 'react';
import { useDebounce } from 'react-use';
import { InlineField, Input } from '@grafana/ui';
import { validateInterval, validateIntervalRegex } from './validation';
interface Props {
value: string;
onChange: (val: string) => void;
isInvalidError: string;
placeholder?: string;
width?: number;
ariaLabel?: string;
label?: string;
tooltip?: string;
disabled?: boolean;
validationRegex?: RegExp;
}
interface FieldProps {
labelWidth: number;
disabled: boolean;
invalid: boolean;
error: string;
label?: string;
tooltip?: string;
}
export const IntervalInput = (props: Props) => {
const validationRegex = props.validationRegex || validateIntervalRegex;
const [intervalIsInvalid, setIntervalIsInvalid] = useState(() => {
return props.value ? validateInterval(props.value, validationRegex) : false;
});
useDebounce(
() => {
setIntervalIsInvalid(validateInterval(props.value, validationRegex));
},
500,
[props.value]
);
const fieldProps: FieldProps = {
labelWidth: 26,
disabled: props.disabled ?? false,
invalid: intervalIsInvalid,
error: props.isInvalidError,
};
if (props.label) {
fieldProps.label = props.label;
fieldProps.tooltip = props.tooltip || '';
}
return (
<InlineField {...fieldProps}>
<Input
type="text"
placeholder={props.placeholder || '0'}
width={props.width || 40}
onChange={(e) => {
props.onChange(e.currentTarget.value);
}}
value={props.value}
aria-label={props.ariaLabel || 'interval input'}
/>
</InlineField>
);
};

View File

@@ -1,28 +0,0 @@
import { validateInterval, validateIntervalRegex } from './validation';
describe('Validation', () => {
it('should validate incorrect values correctly', () => {
expect(validateInterval('-', validateIntervalRegex)).toBeTruthy();
expect(validateInterval('1', validateIntervalRegex)).toBeTruthy();
expect(validateInterval('test', validateIntervalRegex)).toBeTruthy();
expect(validateInterval('1ds', validateIntervalRegex)).toBeTruthy();
expect(validateInterval('10Ms', validateIntervalRegex)).toBeTruthy();
expect(validateInterval('-9999999', validateIntervalRegex)).toBeTruthy();
});
it('should validate correct values correctly', () => {
expect(validateInterval('1y', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('1M', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('1w', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('1d', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('2h', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('4m', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('8s', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('80ms', validateIntervalRegex)).toBeFalsy();
expect(validateInterval('-80ms', validateIntervalRegex)).toBeFalsy();
});
it('should not return error if no value provided', () => {
expect(validateInterval('', validateIntervalRegex)).toBeFalsy();
});
});

View File

@@ -1,6 +0,0 @@
export const validateIntervalRegex = /^(-?\d+(?:\.\d+)?)(ms|[Mwdhmsy])$/;
export const validateInterval = (val: string, regex: RegExp) => {
const matches = val.match(regex);
return matches || !val ? false : true;
};

View File

@@ -1,112 +0,0 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { InlineLabel, SegmentInput, ToolbarButton, useStyles2 } from '@grafana/ui';
import { ToolbarButtonVariant } from '@grafana/ui/src/components/ToolbarButton';
import { TraceToLogsTag } from './TraceToLogsSettings';
interface Props {
values: TraceToLogsTag[];
onChange: (values: TraceToLogsTag[]) => void;
id?: string;
}
const VARIANT = 'none' as ToolbarButtonVariant;
export const TagMappingInput = ({ values, onChange, id }: Props) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.wrapper}>
{values.length ? (
values.map((value, idx) => (
<div className={styles.pair} key={idx}>
<SegmentInput
id={`${id}-key-${idx}`}
placeholder={'Tag name'}
value={value.key}
onChange={(e) => {
onChange(
values.map((v, i) => {
if (i === idx) {
return { ...v, key: String(e) };
}
return v;
})
);
}}
/>
<InlineLabel aria-label="equals" className={styles.operator}>
as
</InlineLabel>
<SegmentInput
id={`${id}-value-${idx}`}
placeholder={'New name (optional)'}
value={value.value || ''}
onChange={(e) => {
onChange(
values.map((v, i) => {
if (i === idx) {
return { ...v, value: String(e) };
}
return v;
})
);
}}
/>
<ToolbarButton
onClick={() => onChange([...values.slice(0, idx), ...values.slice(idx + 1)])}
className={cx(styles.removeTag, 'query-part')}
aria-label="Remove tag"
variant={VARIANT}
type="button"
icon="times"
/>
{idx === values.length - 1 ? (
<ToolbarButton
onClick={() => onChange([...values, { key: '', value: '' }])}
className="query-part"
aria-label="Add tag"
type="button"
variant={VARIANT}
icon="plus"
/>
) : null}
</div>
))
) : (
<ToolbarButton
icon="plus"
onClick={() => onChange([...values, { key: '', value: '' }])}
className="query-part"
aria-label="Add tag"
type="button"
variant={VARIANT}
/>
)}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
gap: `${theme.spacing(0.5)} 0`,
}),
pair: css({
display: 'flex',
justifyContent: 'start',
alignItems: 'center',
}),
operator: css({
color: theme.v1.palette.orange,
width: 'auto',
}),
removeTag: css({
marginRight: theme.spacing(0.5),
}),
});

View File

@@ -1,123 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { DataSourceInstanceSettings, DataSourceSettings } from '@grafana/data';
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
import { TraceToLogsData, TraceToLogsSettings } from './TraceToLogsSettings';
const defaultOptionsOldFormat: DataSourceSettings<TraceToLogsData> = {
jsonData: {
tracesToLogs: {
datasourceUid: 'loki1_uid',
tags: ['someTag'],
mapTagNamesEnabled: false,
spanStartTimeShift: '1m',
spanEndTimeShift: '1m',
filterByTraceID: true,
filterBySpanID: true,
},
},
} as unknown as DataSourceSettings<TraceToLogsData>;
const defaultOptionsNewFormat: DataSourceSettings<TraceToLogsData> = {
jsonData: {
tracesToLogsV2: {
datasourceUid: 'loki1_uid',
tags: [{ key: 'someTag', value: 'newName' }],
spanStartTimeShift: '1m',
spanEndTimeShift: '1m',
filterByTraceID: true,
filterBySpanID: true,
customQuery: true,
query: '{${__tags}}',
},
},
} as unknown as DataSourceSettings<TraceToLogsData>;
const lokiSettings = {
uid: 'loki1_uid',
name: 'loki1',
type: 'loki',
meta: { info: { logos: { small: '' } } },
} as unknown as DataSourceInstanceSettings;
describe('TraceToLogsSettings', () => {
beforeAll(() => {
setDataSourceSrv({
getList() {
return [lokiSettings];
},
getInstanceSettings() {
return lokiSettings;
},
} as unknown as DataSourceSrv);
});
it('should render old format without error', () => {
expect(() =>
render(<TraceToLogsSettings options={defaultOptionsOldFormat} onOptionsChange={() => {}} />)
).not.toThrow();
});
it('should render new format without error', () => {
expect(() =>
render(<TraceToLogsSettings options={defaultOptionsNewFormat} onOptionsChange={() => {}} />)
).not.toThrow();
});
it('should render and transform data from old format correctly', () => {
render(<TraceToLogsSettings options={defaultOptionsOldFormat} onOptionsChange={() => {}} />);
expect(screen.getByText('someTag')).toBeInTheDocument();
expect((screen.getByLabelText('Use custom query') as HTMLInputElement).checked).toBeFalsy();
expect((screen.getByLabelText('Filter by trace ID') as HTMLInputElement).checked).toBeTruthy();
expect((screen.getByLabelText('Filter by span ID') as HTMLInputElement).checked).toBeTruthy();
});
it('renders old mapped tags correctly', () => {
const options = {
...defaultOptionsOldFormat,
jsonData: {
...defaultOptionsOldFormat.jsonData,
tracesToLogs: {
...defaultOptionsOldFormat.jsonData.tracesToLogs,
tags: undefined,
mappedTags: [{ key: 'someTag', value: 'withNewName' }],
mapTagNamesEnabled: true,
},
},
};
render(<TraceToLogsSettings options={options} onOptionsChange={() => {}} />);
expect(screen.getByText('someTag')).toBeInTheDocument();
expect(screen.getByText('withNewName')).toBeInTheDocument();
});
it('transforms old format to new on change', async () => {
const changeMock = jest.fn();
render(<TraceToLogsSettings options={defaultOptionsOldFormat} onOptionsChange={changeMock} />);
const checkBox = screen.getByLabelText('Filter by trace ID');
await userEvent.click(checkBox);
expect(changeMock.mock.calls[0]).toEqual([
{
jsonData: {
tracesToLogs: undefined,
tracesToLogsV2: {
customQuery: false,
datasourceUid: 'loki1_uid',
filterBySpanID: true,
filterByTraceID: false,
spanEndTimeShift: '1m',
spanStartTimeShift: '1m',
tags: [
{
key: 'someTag',
},
],
},
},
},
]);
});
});

View File

@@ -1,275 +0,0 @@
import { css } from '@emotion/css';
import React, { useCallback, useMemo } from 'react';
import { DataSourceJsonData, DataSourceInstanceSettings, DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { ConfigSection } from '@grafana/experimental';
import { DataSourcePicker } from '@grafana/runtime';
import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui';
import { ConfigDescriptionLink } from '../ConfigDescriptionLink';
import { IntervalInput } from '../IntervalInput/IntervalInput';
import { TagMappingInput } from './TagMappingInput';
export interface TraceToLogsTag {
key: string;
value?: string;
}
// @deprecated use getTraceToLogsOptions to get the v2 version of this config from jsonData
export interface TraceToLogsOptions {
datasourceUid?: string;
tags?: string[];
mappedTags?: TraceToLogsTag[];
mapTagNamesEnabled?: boolean;
spanStartTimeShift?: string;
spanEndTimeShift?: string;
filterByTraceID?: boolean;
filterBySpanID?: boolean;
lokiSearch?: boolean; // legacy
}
export interface TraceToLogsOptionsV2 {
datasourceUid?: string;
tags?: TraceToLogsTag[];
spanStartTimeShift?: string;
spanEndTimeShift?: string;
filterByTraceID?: boolean;
filterBySpanID?: boolean;
query?: string;
customQuery: boolean;
}
export interface TraceToLogsData extends DataSourceJsonData {
tracesToLogs?: TraceToLogsOptions;
tracesToLogsV2?: TraceToLogsOptionsV2;
}
/**
* Gets new version of the traceToLogs config from the json data either returning directly or transforming the old
* version to new and returning that.
*/
export function getTraceToLogsOptions(data?: TraceToLogsData): TraceToLogsOptionsV2 | undefined {
if (data?.tracesToLogsV2) {
return data.tracesToLogsV2;
}
if (!data?.tracesToLogs) {
return undefined;
}
const traceToLogs: TraceToLogsOptionsV2 = {
customQuery: false,
};
traceToLogs.datasourceUid = data.tracesToLogs.datasourceUid;
traceToLogs.tags = data.tracesToLogs.mapTagNamesEnabled
? data.tracesToLogs.mappedTags
: data.tracesToLogs.tags?.map((tag) => ({ key: tag }));
traceToLogs.filterByTraceID = data.tracesToLogs.filterByTraceID;
traceToLogs.filterBySpanID = data.tracesToLogs.filterBySpanID;
traceToLogs.spanStartTimeShift = data.tracesToLogs.spanStartTimeShift;
traceToLogs.spanEndTimeShift = data.tracesToLogs.spanEndTimeShift;
return traceToLogs;
}
interface Props extends DataSourcePluginOptionsEditorProps<TraceToLogsData> {}
export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
const supportedDataSourceTypes = [
'loki',
'elasticsearch',
'grafana-splunk-datasource', // external
'grafana-opensearch-datasource', // external
'grafana-falconlogscale-datasource', // external
'googlecloud-logging-datasource', // external
];
const traceToLogs = useMemo(
(): TraceToLogsOptionsV2 => getTraceToLogsOptions(options.jsonData) || { customQuery: false },
[options.jsonData]
);
const { query = '', tags, customQuery } = traceToLogs;
const updateTracesToLogs = useCallback(
(value: Partial<TraceToLogsOptionsV2>) => {
// Cannot use updateDatasourcePluginJsonDataOption here as we need to update 2 keys, and they would overwrite each
// other as updateDatasourcePluginJsonDataOption isn't synchronized
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
tracesToLogsV2: {
...traceToLogs,
...value,
},
tracesToLogs: undefined,
},
});
},
[onOptionsChange, options, traceToLogs]
);
return (
<div className={css({ width: '100%' })}>
<InlineFieldRow>
<InlineField
tooltip="The logs data source the trace is going to navigate to"
label="Data source"
labelWidth={26}
>
<DataSourcePicker
inputId="trace-to-logs-data-source-picker"
filter={(ds) => supportedDataSourceTypes.includes(ds.type)}
current={traceToLogs.datasourceUid}
noDefault={true}
width={40}
onChange={(ds: DataSourceInstanceSettings) =>
updateTracesToLogs({
datasourceUid: ds.uid,
})
}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<IntervalInput
label={getTimeShiftLabel('start')}
tooltip={getTimeShiftTooltip('start', '0')}
value={traceToLogs.spanStartTimeShift || ''}
onChange={(val) => {
updateTracesToLogs({ spanStartTimeShift: val });
}}
isInvalidError={invalidTimeShiftError}
/>
</InlineFieldRow>
<InlineFieldRow>
<IntervalInput
label={getTimeShiftLabel('end')}
tooltip={getTimeShiftTooltip('end', '0')}
value={traceToLogs.spanEndTimeShift || ''}
onChange={(val) => {
updateTracesToLogs({ spanEndTimeShift: val });
}}
isInvalidError={invalidTimeShiftError}
/>
</InlineFieldRow>
<InlineFieldRow>
<InlineField
tooltip="Tags that will be used in the query. Default tags: 'cluster', 'hostname', 'namespace', 'pod', 'service.name', 'service.namespace'"
label="Tags"
labelWidth={26}
>
<TagMappingInput values={tags ?? []} onChange={(v) => updateTracesToLogs({ tags: v })} />
</InlineField>
</InlineFieldRow>
<IdFilter
disabled={customQuery}
type={'trace'}
id={'filterByTraceID'}
value={Boolean(traceToLogs.filterByTraceID)}
onChange={(val) => updateTracesToLogs({ filterByTraceID: val })}
/>
<IdFilter
disabled={customQuery}
type={'span'}
id={'filterBySpanID'}
value={Boolean(traceToLogs.filterBySpanID)}
onChange={(val) => updateTracesToLogs({ filterBySpanID: val })}
/>
<InlineFieldRow>
<InlineField
tooltip="Use a custom query with the possibility to interpolate variables from the trace or span"
label="Use custom query"
labelWidth={26}
>
<InlineSwitch
id={'customQuerySwitch'}
value={customQuery}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
updateTracesToLogs({ customQuery: event.currentTarget.checked })
}
/>
</InlineField>
</InlineFieldRow>
{customQuery && (
<InlineField
label="Query"
labelWidth={26}
tooltip="The query that will run when navigating from a trace to logs data source. Interpolate tags using the `$__tags` keyword"
grow
>
<Input
label="Query"
type="text"
allowFullScreen
value={query}
onChange={(e) => updateTracesToLogs({ query: e.currentTarget.value })}
/>
</InlineField>
)}
</div>
);
}
interface IdFilterProps {
type: 'trace' | 'span';
id: string;
value: boolean;
onChange: (val: boolean) => void;
disabled: boolean;
}
function IdFilter(props: IdFilterProps) {
return (
<InlineFieldRow>
<InlineField
disabled={props.disabled}
label={`Filter by ${props.type} ID`}
labelWidth={26}
grow
tooltip={`Filters logs by ${props.type} ID`}
>
<InlineSwitch
id={props.id}
value={props.value}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) => props.onChange(event.currentTarget.checked)}
/>
</InlineField>
</InlineFieldRow>
);
}
export const getTimeShiftLabel = (type: 'start' | 'end') => {
return `Span ${type} time shift`;
};
export const getTimeShiftTooltip = (type: 'start' | 'end', defaultVal: string) => {
return `Shifts the ${type} time of the span. Default: ${defaultVal} (Time units can be used here, for example: 5s, -1m, 3h)`;
};
export const invalidTimeShiftError = 'Invalid time shift. See tooltip for examples.';
export const TraceToLogsSection = ({ options, onOptionsChange }: DataSourcePluginOptionsEditorProps) => {
let suffix = options.type;
suffix += options.type === 'tempo' ? '/configure-tempo-data-source/#trace-to-logs' : '/#trace-to-logs';
return (
<ConfigSection
title="Trace to logs"
description={
<ConfigDescriptionLink
description="Navigate from a trace span to the selected data source's logs."
suffix={suffix}
feature="trace to logs"
/>
}
isCollapsible={true}
isInitiallyOpen={true}
>
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</ConfigSection>
);
};

View File

@@ -1,240 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import {
DataSourceInstanceSettings,
DataSourceJsonData,
DataSourcePluginOptionsEditorProps,
GrafanaTheme2,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { ConfigSection } from '@grafana/experimental';
import { DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui';
import { ConfigDescriptionLink } from '../ConfigDescriptionLink';
import { IntervalInput } from '../IntervalInput/IntervalInput';
import { TagMappingInput } from '../TraceToLogs/TagMappingInput';
import { getTimeShiftLabel, getTimeShiftTooltip, invalidTimeShiftError } from '../TraceToLogs/TraceToLogsSettings';
export interface TraceToMetricsOptions {
datasourceUid?: string;
tags?: Array<{ key: string; value: string }>;
queries: TraceToMetricQuery[];
spanStartTimeShift?: string;
spanEndTimeShift?: string;
}
export interface TraceToMetricQuery {
name?: string;
query?: string;
}
export interface TraceToMetricsData extends DataSourceJsonData {
tracesToMetrics?: TraceToMetricsOptions;
}
interface Props extends DataSourcePluginOptionsEditorProps<TraceToMetricsData> {}
export function TraceToMetricsSettings({ options, onOptionsChange }: Props) {
const styles = useStyles2(getStyles);
return (
<div className={css({ width: '100%' })}>
<InlineFieldRow className={styles.row}>
<InlineField
tooltip="The Prometheus data source the trace is going to navigate to"
label="Data source"
labelWidth={26}
>
<DataSourcePicker
inputId="trace-to-metrics-data-source-picker"
pluginId="prometheus"
current={options.jsonData.tracesToMetrics?.datasourceUid}
noDefault={true}
width={40}
onChange={(ds: DataSourceInstanceSettings) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
datasourceUid: ds.uid,
})
}
/>
</InlineField>
{options.jsonData.tracesToMetrics?.datasourceUid ? (
<Button
type="button"
variant="secondary"
size="sm"
fill="text"
onClick={() => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
datasourceUid: undefined,
});
}}
>
Clear
</Button>
) : null}
</InlineFieldRow>
<InlineFieldRow>
<IntervalInput
label={getTimeShiftLabel('start')}
tooltip={getTimeShiftTooltip('start', '-2m')}
value={options.jsonData.tracesToMetrics?.spanStartTimeShift || ''}
onChange={(val) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanStartTimeShift: val,
});
}}
placeholder={'-2m'}
isInvalidError={invalidTimeShiftError}
/>
</InlineFieldRow>
<InlineFieldRow>
<IntervalInput
label={getTimeShiftLabel('end')}
tooltip={getTimeShiftTooltip('end', '2m')}
value={options.jsonData.tracesToMetrics?.spanEndTimeShift || ''}
onChange={(val) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanEndTimeShift: val,
});
}}
placeholder={'2m'}
isInvalidError={invalidTimeShiftError}
/>
</InlineFieldRow>
<InlineFieldRow>
<InlineField tooltip="Tags that will be used in the metrics query" label="Tags" labelWidth={26}>
<TagMappingInput
values={options.jsonData.tracesToMetrics?.tags ?? []}
onChange={(v) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
tags: v,
})
}
/>
</InlineField>
</InlineFieldRow>
{options.jsonData.tracesToMetrics?.queries?.map((query, i) => (
<div key={i} className={styles.queryRow}>
<InlineField label="Link Label" labelWidth={26} tooltip="Descriptive label for the linked query">
<Input
label="Link Label"
type="text"
allowFullScreen
value={query.name}
width={40}
onChange={(e) => {
let newQueries = options.jsonData.tracesToMetrics?.queries.slice() ?? [];
newQueries[i].name = e.currentTarget.value;
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
queries: newQueries,
});
}}
/>
</InlineField>
<InlineField
label="Query"
labelWidth={10}
tooltip="The Prometheus query that will run when navigating from a trace to metrics. Interpolate tags using the `$__tags` keyword"
grow
>
<Input
label="Query"
type="text"
allowFullScreen
value={query.query}
onChange={(e) => {
let newQueries = options.jsonData.tracesToMetrics?.queries.slice() ?? [];
newQueries[i].query = e.currentTarget.value;
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
queries: newQueries,
});
}}
/>
</InlineField>
<Button
variant="destructive"
title="Remove query"
icon="times"
type="button"
onClick={() => {
let newQueries = options.jsonData.tracesToMetrics?.queries.slice();
newQueries?.splice(i, 1);
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
queries: newQueries,
});
}}
/>
</div>
))}
<Button
variant="secondary"
title="Add query"
icon="plus"
type="button"
onClick={() => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
queries: [...(options.jsonData.tracesToMetrics?.queries ?? []), { query: '' }],
});
}}
>
Add query
</Button>
</div>
);
}
export const TraceToMetricsSection = ({ options, onOptionsChange }: DataSourcePluginOptionsEditorProps) => {
let suffix = options.type;
suffix += options.type === 'tempo' ? '/configure-tempo-data-source/#trace-to-metrics' : '/#trace-to-metrics';
return (
<ConfigSection
title="Trace to metrics"
description={
<ConfigDescriptionLink
description="Navigate from a trace span to the selected data source's metrics."
suffix={suffix}
feature="trace to metrics"
/>
}
isCollapsible={true}
isInitiallyOpen={true}
>
<TraceToMetricsSettings options={options} onOptionsChange={onOptionsChange} />
</ConfigSection>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
infoText: css`
padding-bottom: ${theme.spacing(2)};
color: ${theme.colors.text.secondary};
`,
row: css`
label: row;
align-items: baseline;
`,
queryRow: css`
label: queryRow;
display: flex;
flex-flow: wrap;
`,
});

View File

@@ -1,53 +0,0 @@
import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { DataSourceInstanceSettings, DataSourceSettings } from '@grafana/data';
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
import { TraceToProfilesData, TraceToProfilesSettings } from './TraceToProfilesSettings';
const defaultOption: DataSourceSettings<TraceToProfilesData> = {
jsonData: {
tracesToProfiles: {
datasourceUid: 'profiling1_uid',
tags: [{ key: 'someTag', value: 'newName' }],
customQuery: true,
query: '{${__tags}}',
},
},
} as unknown as DataSourceSettings<TraceToProfilesData>;
const pyroSettings = {
uid: 'profiling1_uid',
name: 'profiling1',
type: 'grafana-pyroscope-datasource',
meta: { info: { logos: { small: '' } } },
} as unknown as DataSourceInstanceSettings;
describe('TraceToProfilesSettings', () => {
beforeAll(() => {
setDataSourceSrv({
getList() {
return [pyroSettings];
},
getInstanceSettings() {
return pyroSettings;
},
} as unknown as DataSourceSrv);
});
it('should render without error', () => {
waitFor(() => {
expect(() =>
render(<TraceToProfilesSettings options={defaultOption} onOptionsChange={() => {}} />)
).not.toThrow();
});
});
it('should render all options', () => {
render(<TraceToProfilesSettings options={defaultOption} onOptionsChange={() => {}} />);
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Profile type')).toBeInTheDocument();
expect(screen.getByText('Use custom query')).toBeInTheDocument();
});
});

View File

@@ -1,186 +0,0 @@
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react';
import { useAsync } from 'react-use';
import {
DataSourceJsonData,
DataSourceInstanceSettings,
DataSourcePluginOptionsEditorProps,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { ConfigSection } from '@grafana/experimental';
import { DataSourcePicker, getDataSourceSrv } from '@grafana/runtime';
import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui';
import { ConfigDescriptionLink } from '../ConfigDescriptionLink';
import { TagMappingInput } from '../TraceToLogs/TagMappingInput';
import { ProfileTypesCascader } from '../pyroscope/ProfileTypesCascader';
import { PyroscopeDataSource } from '../pyroscope/datasource';
import { ProfileTypeMessage } from '../pyroscope/types';
export interface TraceToProfilesOptions {
datasourceUid?: string;
tags?: Array<{ key: string; value?: string }>;
query?: string;
profileTypeId?: string;
customQuery: boolean;
}
export interface TraceToProfilesData extends DataSourceJsonData {
tracesToProfiles?: TraceToProfilesOptions;
}
interface Props extends DataSourcePluginOptionsEditorProps<TraceToProfilesData> {}
export function TraceToProfilesSettings({ options, onOptionsChange }: Props) {
const supportedDataSourceTypes = useMemo(() => ['grafana-pyroscope-datasource'], []);
const [profileTypes, setProfileTypes] = useState<ProfileTypeMessage[]>([]);
const profileTypesPlaceholder = useMemo(() => {
let placeholder = profileTypes.length === 0 ? 'No profile types found' : 'Select profile type';
if (!options.jsonData.tracesToProfiles?.datasourceUid) {
placeholder = 'Please select profiling data source';
}
return placeholder;
}, [options.jsonData.tracesToProfiles?.datasourceUid, profileTypes]);
const { value: dataSource } = useAsync(async () => {
return await getDataSourceSrv().get(options.jsonData.tracesToProfiles?.datasourceUid);
}, [options.jsonData.tracesToProfiles?.datasourceUid]);
useEffect(() => {
if (
dataSource &&
dataSource instanceof PyroscopeDataSource &&
supportedDataSourceTypes.includes(dataSource.type) &&
dataSource.uid === options.jsonData.tracesToProfiles?.datasourceUid
) {
dataSource.getProfileTypes().then((profileTypes) => {
setProfileTypes(profileTypes);
});
} else {
setProfileTypes([]);
}
}, [dataSource, onOptionsChange, options, supportedDataSourceTypes]);
return (
<div className={css({ width: '100%' })}>
<InlineFieldRow>
<InlineField
tooltip="The profiles data source the trace is going to navigate to"
label="Data source"
labelWidth={26}
>
<DataSourcePicker
inputId="trace-to-profiles-data-source-picker"
filter={(ds) => supportedDataSourceTypes.includes(ds.type)}
current={options.jsonData.tracesToProfiles?.datasourceUid}
noDefault={true}
width={40}
onChange={(ds: DataSourceInstanceSettings) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', {
...options.jsonData.tracesToProfiles,
datasourceUid: ds.uid,
});
}}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField
tooltip="Tags that will be used in the query. Default tags: 'service.name', 'service.namespace'"
label="Tags"
labelWidth={26}
>
<TagMappingInput
values={options.jsonData.tracesToProfiles?.tags ?? []}
onChange={(v) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', {
...options.jsonData.tracesToProfiles,
tags: v,
});
}}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField tooltip="Profile type that will be used in the query" label="Profile type" labelWidth={26}>
<ProfileTypesCascader
profileTypes={profileTypes}
placeholder={profileTypesPlaceholder}
initialProfileTypeId={options.jsonData.tracesToProfiles?.profileTypeId}
onChange={(val) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', {
...options.jsonData.tracesToProfiles,
profileTypeId: val,
});
}}
width={40}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField
tooltip="Use a custom query with the possibility to interpolate variables from the trace or span"
label="Use custom query"
labelWidth={26}
>
<InlineSwitch
id={'profilesCustomQuerySwitch'}
value={options.jsonData.tracesToProfiles?.customQuery}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', {
...options.jsonData.tracesToProfiles,
customQuery: event.currentTarget.checked,
})
}
/>
</InlineField>
</InlineFieldRow>
{options.jsonData.tracesToProfiles?.customQuery && (
<InlineField
label="Query"
labelWidth={26}
tooltip="The query that will run when navigating from a trace to profiles data source. Interpolate tags using the `$__tags` keyword"
grow
>
<Input
label="Query"
type="text"
allowFullScreen
value={options.jsonData.tracesToProfiles?.query || ''}
onChange={(e) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', {
...options.jsonData.tracesToProfiles,
query: e.currentTarget.value,
})
}
/>
</InlineField>
)}
</div>
);
}
export const TraceToProfilesSection = ({ options, onOptionsChange }: DataSourcePluginOptionsEditorProps) => {
return (
<ConfigSection
title="Trace to profiles"
description={
<ConfigDescriptionLink
description="Navigate from a trace span to the selected data source's profiles."
suffix={`${options.type}/#trace-to-profiles`}
feature="trace to profiles"
/>
}
isCollapsible={true}
isInitiallyOpen={true}
>
<TraceToProfilesSettings options={options} onOptionsChange={onOptionsChange} />
</ConfigSection>
);
};

View File

@@ -1,8 +0,0 @@
/**
* A library containing logic to manage traces.
*
* @packageDocumentation
*/
type Props = {};
export { Props };

View File

@@ -1,12 +0,0 @@
/**
* A library containing logic to manage traces.
*
* @packageDocumentation
*/
export * from './IntervalInput/IntervalInput';
export * from './TraceToLogs/TagMappingInput';
export * from './TraceToLogs/TraceToLogsSettings';
export * from './TraceToMetrics/TraceToMetricsSettings';
export * from './TraceToProfiles/TraceToProfilesSettings';
export * from './utils';

View File

@@ -1,85 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Cascader, CascaderOption } from '@grafana/ui';
import { PyroscopeDataSource } from './datasource';
import { ProfileTypeMessage } from './types';
type Props = {
initialProfileTypeId?: string;
profileTypes?: ProfileTypeMessage[];
onChange: (value: string) => void;
placeholder?: string;
width?: number;
};
export function ProfileTypesCascader(props: Props) {
const cascaderOptions = useCascaderOptions(props.profileTypes);
return (
<Cascader
placeholder={props.placeholder}
separator={'-'}
displayAllSelectedLevels={true}
initialValue={props.initialProfileTypeId}
allowCustomValue={true}
onSelect={props.onChange}
options={cascaderOptions}
changeOnSelect={false}
width={props.width ?? 26}
/>
);
}
// Turn profileTypes into cascader options
function useCascaderOptions(profileTypes?: ProfileTypeMessage[]): CascaderOption[] {
return useMemo(() => {
if (!profileTypes) {
return [];
}
let mainTypes = new Map<string, CascaderOption>();
// Classify profile types by name then sample type.
// The profileTypes are something like cpu:sample:nanoseconds:sample:count or app.something.something
for (let profileType of profileTypes) {
let parts: string[] = [];
if (profileType.id.indexOf(':') > -1) {
parts = profileType.id.split(':');
}
const [name, type] = parts;
if (!mainTypes.has(name)) {
mainTypes.set(name, {
label: name,
value: name,
items: [],
});
}
mainTypes.get(name)?.items!.push({
label: type,
value: profileType.id,
});
}
return Array.from(mainTypes.values());
}, [profileTypes]);
}
/**
* Loads the profile types.
*
* This is exported and not used directly in the ProfileTypesCascader component because in some case we need to know
* the profileTypes before rendering the cascader.
* @param datasource
*/
export function useProfileTypes(datasource: PyroscopeDataSource) {
const [profileTypes, setProfileTypes] = useState<ProfileTypeMessage[]>();
useEffect(() => {
(async () => {
const profileTypes = await datasource.getProfileTypes();
setProfileTypes(profileTypes);
})();
}, [datasource]);
return profileTypes;
}

View File

@@ -1,44 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// public/app/plugins/gen.go
// Using jennies:
// TSTypesJenny
// PluginTSTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
import * as common from '@grafana/schema';
export type PyroscopeQueryType = ('metrics' | 'profile' | 'both');
export const defaultPyroscopeQueryType: PyroscopeQueryType = 'both';
export interface GrafanaPyroscope extends common.DataQuery {
/**
* Allows to group the results.
*/
groupBy: Array<string>;
/**
* Specifies the query label selectors.
*/
labelSelector: string;
/**
* Sets the maximum number of nodes in the flamegraph.
*/
maxNodes?: number;
/**
* Specifies the type of profile to query.
*/
profileTypeId: string;
/**
* Specifies the query span selectors.
*/
spanSelector?: Array<string>;
}
export const defaultGrafanaPyroscope: Partial<GrafanaPyroscope> = {
groupBy: [],
labelSelector: '{}',
spanSelector: [],
};

View File

@@ -1,28 +0,0 @@
import { Observable } from 'rxjs';
import { AbstractQuery, CoreApp, DataQueryRequest, DataQueryResponse, ScopedVars } from '@grafana/data';
import { DataSourceWithBackend } from '@grafana/runtime';
import { PyroscopeDataSourceOptions, Query, ProfileTypeMessage } from './types';
export abstract class PyroscopeDataSource extends DataSourceWithBackend<Query, PyroscopeDataSourceOptions> {
abstract query(request: DataQueryRequest<Query>): Observable<DataQueryResponse>;
abstract getProfileTypes(): Promise<ProfileTypeMessage[]>;
abstract getLabelNames(query: string, start: number, end: number): Promise<string[]>;
abstract getLabelValues(query: string, label: string, start: number, end: number): Promise<string[]>;
abstract applyTemplateVariables(query: Query, scopedVars: ScopedVars): Query;
abstract importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise<Query[]>;
abstract importFromAbstractQuery(labelBasedQuery: AbstractQuery): Query;
abstract exportToAbstractQueries(queries: Query[]): Promise<AbstractQuery[]>;
abstract exportToAbstractQuery(query: Query): AbstractQuery;
abstract getDefaultQuery(app: CoreApp): Partial<Query>;
}

View File

@@ -1,16 +0,0 @@
import { DataSourceJsonData } from '@grafana/data';
import { GrafanaPyroscope, PyroscopeQueryType } from './dataquery.gen';
export interface ProfileTypeMessage {
id: string;
label: string;
}
export interface PyroscopeDataSourceOptions extends DataSourceJsonData {
minStep?: string;
}
export interface Query extends GrafanaPyroscope {
queryType: PyroscopeQueryType;
}

View File

@@ -1,118 +0,0 @@
/**
* Get non overlapping duration of the ranges as they can overlap or have gaps.
*/
import { FieldType, MutableDataFrame, NodeGraphDataFrameFieldNames as Fields } from '@grafana/data';
export function getNonOverlappingDuration(ranges: Array<[number, number]>): number {
ranges.sort((a, b) => a[0] - b[0]);
const mergedRanges = ranges.reduce<Array<[number, number]>>((acc, range) => {
if (!acc.length) {
return [range];
}
const tail = acc.slice(-1)[0];
const [prevStart, prevEnd] = tail;
const [start, end] = range;
if (end < prevEnd) {
// In this case the range is completely inside the prev range so we can just ignore it.
return acc;
}
if (start > prevEnd) {
// There is no overlap so we can just add it to stack
return [...acc, range];
}
// We know there is overlap and current range ends later than previous so we can just extend the range
return [...acc.slice(0, -1), [prevStart, end]];
}, []);
return mergedRanges.reduce((acc, range) => {
return acc + (range[1] - range[0]);
}, 0);
}
/**
* Returns a map of the spans with children array for easier processing. It will also contain empty spans in case
* span is missing but other spans are its children. This is more generic because it needs to allow iterating over
* both arrays and dataframe views.
*/
export function makeSpanMap<T>(getSpan: (index: number) => { span: T; id: string; parentIds: string[] } | undefined): {
[id: string]: { span: T; children: string[] };
} {
const spanMap: { [id: string]: { span?: T; children: string[] } } = {};
let span;
for (let index = 0; (span = getSpan(index)), !!span; index++) {
if (!spanMap[span.id]) {
spanMap[span.id] = {
span: span.span,
children: [],
};
} else {
spanMap[span.id].span = span.span;
}
for (const parentId of span.parentIds) {
if (parentId) {
if (!spanMap[parentId]) {
spanMap[parentId] = {
span: undefined,
children: [span.id],
};
} else {
spanMap[parentId].children.push(span.id);
}
}
}
}
return spanMap as { [id: string]: { span: T; children: string[] } };
}
export function getStats(duration: number, traceDuration: number, selfDuration: number) {
return {
main: `${toFixedNoTrailingZeros(duration)}ms (${toFixedNoTrailingZeros((duration / traceDuration) * 100)}%)`,
secondary: `${toFixedNoTrailingZeros(selfDuration)}ms (${toFixedNoTrailingZeros(
(selfDuration / duration) * 100
)}%)`,
};
}
function toFixedNoTrailingZeros(n: number) {
return parseFloat(n.toFixed(2));
}
/**
* Create default frames used when returning data for node graph.
*/
export function makeFrames() {
const nodesFrame = new MutableDataFrame({
fields: [
{ name: Fields.id, type: FieldType.string },
{ name: Fields.title, type: FieldType.string },
{ name: Fields.subTitle, type: FieldType.string },
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } },
{ name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } },
{
name: Fields.color,
type: FieldType.number,
config: { color: { mode: 'continuous-GrYlRd' }, displayName: 'Self time / Trace duration' },
},
],
meta: {
preferredVisualisationType: 'nodeGraph',
},
});
const edgesFrame = new MutableDataFrame({
fields: [
{ name: Fields.id, type: FieldType.string },
{ name: Fields.target, type: FieldType.string },
{ name: Fields.source, type: FieldType.string },
],
meta: {
preferredVisualisationType: 'nodeGraph',
},
});
return [nodesFrame, edgesFrame];
}

View File

@@ -12,16 +12,12 @@ import {
convertLegacyAuthProps,
DataSourceDescription,
} from '@grafana/experimental';
import { TraceToLogsSection, TraceToMetricsSection, TraceToProfilesSection } from '@grafana/o11y-ds-frontend';
import { config } from '@grafana/runtime';
import { SecureSocksProxySettings, useStyles2, Divider, Stack } from '@grafana/ui';
import { NodeGraphSection } from '../_importedDependencies/components/NodeGraphSettings';
import { SpanBarSection } from '../_importedDependencies/components/TraceView/SpanBarSettings';
import {
TraceToLogsSection,
TraceToMetricsSection,
TraceToProfilesSection,
} from '../_importedDependencies/grafana-traces/src';
import { LokiSearchSettings } from './LokiSearchSettings';
import { QuerySettings } from './QuerySettings';

View File

@@ -2,9 +2,9 @@ import { css } from '@emotion/css';
import React from 'react';
import { DataSourcePluginOptionsEditorProps, GrafanaTheme2, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { IntervalInput, invalidTimeShiftError } from '@grafana/o11y-ds-frontend';
import { InlineField, InlineSwitch, useStyles2 } from '@grafana/ui';
import { IntervalInput, invalidTimeShiftError } from '../_importedDependencies/grafana-traces/src';
import { TempoJsonData } from '../types';
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}

View File

@@ -21,6 +21,7 @@ import {
TestDataSourceResponse,
urlUtil,
} from '@grafana/data';
import { TraceToLogsOptions } from '@grafana/o11y-ds-frontend';
import {
BackendSrvRequest,
config,
@@ -39,7 +40,6 @@ import { NodeGraphOptions } from './_importedDependencies/components/NodeGraphSe
import { SpanBarOptions } from './_importedDependencies/components/TraceView/SpanBarSettings';
import { LokiOptions } from './_importedDependencies/datasources/loki/types';
import { PromQuery, PrometheusDatasource } from './_importedDependencies/datasources/prometheus/types';
import { TraceToLogsOptions } from './_importedDependencies/grafana-traces/src';
import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen';
import {
defaultTableFilter,

View File

@@ -10,13 +10,7 @@ import {
FieldType,
toDataFrame,
} from '@grafana/data';
import {
getStats,
getNonOverlappingDuration,
makeSpanMap,
makeFrames,
} from './_importedDependencies/grafana-traces/src';
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '@grafana/o11y-ds-frontend';
/**
* Row in a trace dataFrame

View File

@@ -11,6 +11,7 @@
"@grafana/lezer-logql": "0.2.2",
"@grafana/lezer-traceql": "0.0.12",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/o11y-ds-frontend": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",

View File

@@ -23,9 +23,9 @@ import {
Field,
DataLinkConfigOrigin,
} from '@grafana/data';
import { TraceToProfilesData } from '@grafana/o11y-ds-frontend';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { TraceToProfilesData } from './_importedDependencies/grafana-traces/src';
import { SearchTableType } from './dataquery.gen';
import { createGraphFrames } from './graphTransform';
import { Span, SpanAttributes, Spanset, TempoJsonData, TraceSearchMetadata } from './types';

View File

@@ -1,8 +1,8 @@
import { DataSourceJsonData } from '@grafana/data/src';
import { TraceToLogsOptions } from '@grafana/o11y-ds-frontend';
import { NodeGraphOptions } from './_importedDependencies/components/NodeGraphSettings';
import { LokiQuery } from './_importedDependencies/datasources/loki/types';
import { TraceToLogsOptions } from './_importedDependencies/grafana-traces/src';
import { TempoQuery as TempoBase, TempoQueryType, TraceqlFilter } from './dataquery.gen';
export interface SearchQueryParams {