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,7 +340,7 @@ | |||
| /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  | ||||
| .browserslistrc @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