mirror of https://github.com/grafana/grafana.git
Add grafana-o11y-ds-frontend workspace package (#80362)
This commit is contained in:
parent
3a10e480ba
commit
6cbc3df11e
|
|
@ -529,6 +529,9 @@ exports[`better eslint`] = {
|
|||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"packages/grafana-o11y-ds-frontend/src/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-runtime/src/analytics/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
|
@ -1051,14 +1054,6 @@ exports[`better eslint`] = {
|
|||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/core/components/TraceToLogs/TagMappingInput.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"]
|
||||
],
|
||||
"public/app/core/components/Upgrade/ProBadge.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
|
|
@ -1202,9 +1197,6 @@ exports[`better eslint`] = {
|
|||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/core/utils/tracing.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/admin/LicenseChrome.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
|
|
@ -5750,17 +5742,6 @@ exports[`better eslint`] = {
|
|||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/_importedDependencies/grafana-traces/src/TraceToLogs/TagMappingInput.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/_importedDependencies/grafana-traces/src/TraceToMetrics/TraceToMetricsSettings.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/_importedDependencies/grafana-traces/src/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/_importedDependencies/store.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
|
|
|||
|
|
@ -340,9 +340,9 @@
|
|||
/packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling
|
||||
/plugins-bundled/ @grafana/plugins-platform-frontend
|
||||
/packages/grafana-plugin-configs/ @grafana/plugins-platform-frontend
|
||||
/packages/grafana-o11y-ds-frontend/ @grafana/observability-logs @grafana/observability-traces-and-profiling
|
||||
|
||||
|
||||
# root files, mostly frontend
|
||||
# root files, mostly frontend
|
||||
.browserslistrc @grafana/frontend-ops
|
||||
package.json @grafana/frontend-ops
|
||||
tsconfig.json @grafana/frontend-ops
|
||||
|
|
@ -375,7 +375,6 @@ cypress.config.js @grafana/grafana-frontend-platform
|
|||
/public/app/core/ @grafana/grafana-frontend-platform
|
||||
/public/app/core/components/TimePicker/ @grafana/grafana-frontend-platform
|
||||
/public/app/core/components/Layers/ @grafana/dataviz-squad
|
||||
/public/app/core/components/TraceToLogs @grafana/observability-traces-and-profiling
|
||||
/public/app/core/components/GraphNG/ @grafana/dataviz-squad
|
||||
/public/app/core/components/TimeSeries/ @grafana/dataviz-squad
|
||||
/public/app/core/components/TimelineChart/ @grafana/dataviz-squad
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@
|
|||
"@grafana/google-sdk": "0.1.2",
|
||||
"@grafana/lezer-logql": "0.2.2",
|
||||
"@grafana/monaco-logql": "^0.0.7",
|
||||
"@grafana/o11y-ds-frontend": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "1.30.0",
|
||||
"@grafana/schema": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"author": "Grafana Labs",
|
||||
"license": "AGPL-3.0-only",
|
||||
"name": "@grafana/o11y-ds-frontend",
|
||||
"private": true,
|
||||
"version": "10.4.0-pre",
|
||||
"description": "Library to manage traces in Grafana.",
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git",
|
||||
"directory": "packages/grafana-o11y-ds-frontend"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.11.2",
|
||||
"@grafana/data": "workspace:*",
|
||||
"@grafana/e2e-selectors": "workspace:*",
|
||||
"@grafana/experimental": "1.7.5",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"react": "18.2.0",
|
||||
"react-use": "17.4.0",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@testing-library/jest-dom": "^6.1.2",
|
||||
"@testing-library/react": "14.1.2",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/systemjs": "6.13.5",
|
||||
"@types/testing-library__jest-dom": "5.14.9",
|
||||
"jest": "^29.6.4",
|
||||
"react": "18.2.0",
|
||||
"ts-jest": "29.1.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ 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';
|
||||
|
||||
|
|
@ -13,8 +12,6 @@ interface Props {
|
|||
id?: string;
|
||||
}
|
||||
|
||||
const VARIANT = 'none' as ToolbarButtonVariant;
|
||||
|
||||
export const TagMappingInput = ({ values, onChange, id }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
|
|
@ -60,7 +57,6 @@ export const TagMappingInput = ({ values, onChange, id }: Props) => {
|
|||
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"
|
||||
/>
|
||||
|
|
@ -71,7 +67,6 @@ export const TagMappingInput = ({ values, onChange, id }: Props) => {
|
|||
className="query-part"
|
||||
aria-label="Add tag"
|
||||
type="button"
|
||||
variant={VARIANT}
|
||||
icon="plus"
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -84,7 +79,6 @@ export const TagMappingInput = ({ values, onChange, id }: Props) => {
|
|||
className="query-part"
|
||||
aria-label="Add tag"
|
||||
type="button"
|
||||
variant={VARIANT}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -3,8 +3,8 @@ import React, { useCallback, useMemo } from 'react';
|
|||
|
||||
import { DataSourceJsonData, DataSourceInstanceSettings, DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental';
|
||||
import { DataSourcePicker } from '@grafana/runtime';
|
||||
import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
|
||||
import { IntervalInput } from '../IntervalInput/IntervalInput';
|
||||
|
||||
|
|
@ -9,8 +9,8 @@ import {
|
|||
updateDatasourcePluginJsonDataOption,
|
||||
} from '@grafana/data';
|
||||
import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental';
|
||||
import { DataSourcePicker } from '@grafana/runtime';
|
||||
import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
|
||||
import { IntervalInput } from '../IntervalInput/IntervalInput';
|
||||
import { TagMappingInput } from '../TraceToLogs/TagMappingInput';
|
||||
|
|
@ -228,17 +228,17 @@ export const TraceToMetricsSection = ({ options, onOptionsChange }: DataSourcePl
|
|||
};
|
||||
|
||||
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;
|
||||
`,
|
||||
infoText: {
|
||||
paddingBottom: theme.spacing(2),
|
||||
color: theme.colors.text.secondary,
|
||||
},
|
||||
row: css({
|
||||
label: 'row',
|
||||
alignItems: 'baseline',
|
||||
}),
|
||||
queryRow: css({
|
||||
label: 'queryRow',
|
||||
display: 'flex',
|
||||
flexFlow: 'wrap',
|
||||
}),
|
||||
});
|
||||
|
|
@ -9,13 +9,13 @@ import {
|
|||
updateDatasourcePluginJsonDataOption,
|
||||
} from '@grafana/data';
|
||||
import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental';
|
||||
import { DataSourceWithBackend, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataSourcePicker, DataSourceWithBackend, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
import { ProfileTypesCascader } from 'app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/ProfileTypesCascader';
|
||||
import { ProfileTypeMessage } from 'app/plugins/datasource/grafana-pyroscope-datasource/types';
|
||||
|
||||
import { TagMappingInput } from '../TraceToLogs/TagMappingInput';
|
||||
import { ProfileTypesCascader } from '../pyroscope/ProfileTypesCascader';
|
||||
import { ProfileTypeMessage } from '../pyroscope/types';
|
||||
|
||||
export interface TraceToProfilesOptions {
|
||||
datasourceUid?: string;
|
||||
tags?: Array<{ key: string; value?: string }>;
|
||||
|
|
@ -36,9 +36,3 @@ export interface GrafanaPyroscope extends common.DataQuery {
|
|||
*/
|
||||
spanSelector?: Array<string>;
|
||||
}
|
||||
|
||||
export const defaultGrafanaPyroscope: Partial<GrafanaPyroscope> = {
|
||||
groupBy: [],
|
||||
labelSelector: '{}',
|
||||
spanSelector: [],
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Observable } from 'rxjs';
|
||||
|
||||
import { CoreApp, DataQueryRequest, DataQueryResponse, ScopedVars } from '@grafana/data';
|
||||
import { DataSourceWithBackend } from '@grafana/runtime';
|
||||
|
||||
import { PyroscopeDataSourceOptions, ProfileTypeMessage, Query } from './types';
|
||||
|
||||
export abstract class PyroscopeDataSource extends DataSourceWithBackend<Query, PyroscopeDataSourceOptions> {
|
||||
abstract applyTemplateVariables(query: Query, scopedVars: ScopedVars): Query;
|
||||
abstract getDefaultQuery(app: CoreApp): Partial<Query>;
|
||||
abstract getProfileTypes(): Promise<ProfileTypeMessage[]>;
|
||||
abstract query(request: DataQueryRequest<Query>): Observable<DataQueryResponse>;
|
||||
}
|
||||
|
|
@ -65,6 +65,7 @@ export function makeSpanMap<T>(getSpan: (index: number) => { span: T; id: string
|
|||
}
|
||||
}
|
||||
}
|
||||
// Discussion on this type assertion here: https://github.com/grafana/grafana/pull/80362/files#r1451019375
|
||||
return spanMap as { [id: string]: { span: T; children: string[] } };
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"declarationDir": "./compiled",
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"rootDirs": ["."]
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"extends": "@grafana/tsconfig",
|
||||
"include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"]
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -14,13 +14,11 @@ import {
|
|||
mapInternalLinkToExplore,
|
||||
SplitOpen,
|
||||
} from '@grafana/data';
|
||||
import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { TempoQuery } from '@grafana-plugins/tempo/types';
|
||||
import { getTraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { TraceToMetricsData } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
|
||||
import { TraceToProfilesData } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { useDispatch, useSelector } from 'app/types';
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import React, { useState, useEffect, memo, useCallback } from 'react';
|
|||
|
||||
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
|
||||
import { AccessoryButton } from '@grafana/experimental';
|
||||
import { IntervalInput } from '@grafana/o11y-ds-frontend';
|
||||
import { Collapse, HorizontalGroup, Icon, InlineField, InlineFieldRow, Select, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { IntervalInput } from 'app/core/components/IntervalInput/IntervalInput';
|
||||
|
||||
import { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch';
|
||||
import SearchBarInput from '../../common/SearchBarInput';
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import {
|
|||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { FlameGraph } from '@grafana/flamegraph';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { PyroscopeQueryType } from 'app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen';
|
||||
import { Query } from 'app/plugins/datasource/grafana-pyroscope-datasource/types';
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ import cx from 'classnames';
|
|||
import React from 'react';
|
||||
|
||||
import { DataFrame, dateTimeFormat, GrafanaTheme2, IconName, LinkModel } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { DataLinkButton, Icon, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||
import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
|
||||
|
||||
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||
import { autoColor } from '../../Theme';
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ import classNames from 'classnames';
|
|||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui';
|
||||
import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
|
||||
|
||||
import { autoColor } from '../Theme';
|
||||
import { SpanLinkFunc } from '../types';
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ import * as React from 'react';
|
|||
import { RefObject } from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui';
|
||||
import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
|
||||
|
||||
import { PEER_SERVICE } from '../constants/tag-keys';
|
||||
import { CriticalPathSection, SpanBarOptions, SpanLinkFunc, TNil } from '../types';
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ import { css } from '@emotion/css';
|
|||
import React, { RefObject } from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
||||
import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
|
||||
|
||||
import { autoColor } from '../Theme';
|
||||
import { merge as mergeShortcuts } from '../keyboard-shortcuts';
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import {
|
|||
FieldType,
|
||||
DataFrame,
|
||||
} from '@grafana/data';
|
||||
import { TraceToLogsOptionsV2, TraceToMetricsOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
|
||||
import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
import { TraceToLogsOptionsV2 } from '../../../core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,16 @@ import {
|
|||
SplitOpen,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
TraceToProfilesOptions,
|
||||
TraceToMetricQuery,
|
||||
TraceToMetricsOptions,
|
||||
TraceToLogsOptionsV2,
|
||||
TraceToLogsTag,
|
||||
} from '@grafana/o11y-ds-frontend';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { TraceToLogsOptionsV2, TraceToLogsTag } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { TraceToMetricQuery, TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
|
||||
import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import React from 'react';
|
|||
|
||||
import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data';
|
||||
import { ConfigSection, DataSourceDescription } from '@grafana/experimental';
|
||||
import { TraceToLogsSection, TraceToMetricsSection } from '@grafana/o11y-ds-frontend';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataSourceHttpSettings, useStyles2, Divider, Stack } from '@grafana/ui';
|
||||
import { NodeGraphSection } from 'app/core/components/NodeGraphSettings';
|
||||
import { TraceToLogsSection } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { TraceToMetricsSection } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
|
||||
import { SpanBarSection } from 'app/features/explore/TraceView/components/settings/SpanBarSettings';
|
||||
|
||||
import { TraceIdTimeParams } from './TraceIdTimeParams';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { DataFrame, NodeGraphDataFrameFieldNames as Fields } from '@grafana/data';
|
||||
|
||||
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '../../../core/utils/tracing';
|
||||
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '@grafana/o11y-ds-frontend';
|
||||
|
||||
import { Span, TraceResponse } from './types';
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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),
|
||||
}),
|
||||
});
|
||||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
`,
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* A library containing logic to manage traces.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
type Props = {};
|
||||
|
||||
export { Props };
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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> {}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import React from 'react';
|
|||
|
||||
import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data';
|
||||
import { ConfigSection, DataSourceDescription } from '@grafana/experimental';
|
||||
import { TraceToLogsSection, TraceToMetricsSection } from '@grafana/o11y-ds-frontend';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { DataSourceHttpSettings, useStyles2, Divider, Stack } from '@grafana/ui';
|
||||
import { NodeGraphSection } from 'app/core/components/NodeGraphSettings';
|
||||
import { TraceToLogsSection } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { TraceToMetricsSection } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
|
||||
import { SpanBarSection } from 'app/features/explore/TraceView/components/settings/SpanBarSettings';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { DataFrame, NodeGraphDataFrameFieldNames as Fields } from '@grafana/data';
|
||||
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '@grafana/o11y-ds-frontend';
|
||||
|
||||
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '../../../../core/utils/tracing';
|
||||
import { ZipkinSpan } from '../types';
|
||||
|
||||
interface Node {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
PACKAGES=$(ls -d ./packages/*/)
|
||||
EXIT_CODE=0
|
||||
GITHUB_MESSAGE=""
|
||||
SKIP_PACKAGES=("grafana-eslint-rules" "grafana-plugin-configs")
|
||||
SKIP_PACKAGES=("grafana-eslint-rules" "grafana-plugin-configs" "grafana-o11y-ds-frontend")
|
||||
|
||||
# Loop through the packages
|
||||
while IFS=" " read -r -a package; do
|
||||
|
|
|
|||
35
yarn.lock
35
yarn.lock
|
|
@ -3067,6 +3067,7 @@ __metadata:
|
|||
"@grafana/lezer-logql": "npm:0.2.2"
|
||||
"@grafana/lezer-traceql": "npm:0.0.12"
|
||||
"@grafana/monaco-logql": "npm:^0.0.7"
|
||||
"@grafana/o11y-ds-frontend": "workspace:*"
|
||||
"@grafana/plugin-configs": "npm:10.4.0-pre"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/schema": "workspace:*"
|
||||
|
|
@ -3457,6 +3458,39 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/o11y-ds-frontend@workspace:*, @grafana/o11y-ds-frontend@workspace:packages/grafana-o11y-ds-frontend":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@grafana/o11y-ds-frontend@workspace:packages/grafana-o11y-ds-frontend"
|
||||
dependencies:
|
||||
"@emotion/css": "npm:11.11.2"
|
||||
"@grafana/data": "workspace:*"
|
||||
"@grafana/e2e-selectors": "workspace:*"
|
||||
"@grafana/experimental": "npm:1.7.5"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/tsconfig": "npm:^1.2.0-rc1"
|
||||
"@grafana/ui": "workspace:*"
|
||||
"@testing-library/jest-dom": "npm:^6.1.2"
|
||||
"@testing-library/react": "npm:14.1.2"
|
||||
"@testing-library/user-event": "npm:14.5.2"
|
||||
"@types/jest": "npm:^29.5.4"
|
||||
"@types/react": "npm:18.2.15"
|
||||
"@types/systemjs": "npm:6.13.5"
|
||||
"@types/testing-library__jest-dom": "npm:5.14.9"
|
||||
jest: "npm:^29.6.4"
|
||||
react: "npm:18.2.0"
|
||||
react-use: "npm:17.4.0"
|
||||
rxjs: "npm:7.8.1"
|
||||
ts-jest: "npm:29.1.1"
|
||||
ts-node: "npm:10.9.1"
|
||||
tslib: "npm:2.6.0"
|
||||
typescript: "npm:5.2.2"
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/plugin-configs@npm:10.4.0-pre, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs"
|
||||
|
|
@ -16933,6 +16967,7 @@ __metadata:
|
|||
"@grafana/google-sdk": "npm:0.1.2"
|
||||
"@grafana/lezer-logql": "npm:0.2.2"
|
||||
"@grafana/monaco-logql": "npm:^0.0.7"
|
||||
"@grafana/o11y-ds-frontend": "workspace:*"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:1.30.0"
|
||||
"@grafana/schema": "workspace:*"
|
||||
|
|
|
|||
Loading…
Reference in New Issue