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.", "0"], | ||||||
|       [0, 0, 0, "Styles should be written using objects.", "1"] |       [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": [ |     "packages/grafana-runtime/src/analytics/types.ts:5381": [ | ||||||
|       [0, 0, 0, "Unexpected any. Specify a different type.", "0"] |       [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, "Do not use any type assertions.", "1"], | ||||||
|       [0, 0, 0, "Unexpected any. Specify a different type.", "2"] |       [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": [ |     "public/app/core/components/Upgrade/ProBadge.tsx:5381": [ | ||||||
|       [0, 0, 0, "Styles should be written using objects.", "0"] |       [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.", "0"], | ||||||
|       [0, 0, 0, "Do not use any type assertions.", "1"] |       [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": [ |     "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.", "0"], | ||||||
|       [0, 0, 0, "Styles should be written using objects.", "1"], |       [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.", "1"], | ||||||
|       [0, 0, 0, "Unexpected any. Specify a different type.", "2"] |       [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": [ |     "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.", "0"], | ||||||
|       [0, 0, 0, "Unexpected any. Specify a different type.", "1"], |       [0, 0, 0, "Unexpected any. Specify a different type.", "1"], | ||||||
|  |  | ||||||
|  | @ -340,9 +340,9 @@ | ||||||
| /packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling | /packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling | ||||||
| /plugins-bundled/ @grafana/plugins-platform-frontend | /plugins-bundled/ @grafana/plugins-platform-frontend | ||||||
| /packages/grafana-plugin-configs/ @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 | .browserslistrc @grafana/frontend-ops | ||||||
| package.json @grafana/frontend-ops | package.json @grafana/frontend-ops | ||||||
| tsconfig.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/ @grafana/grafana-frontend-platform | ||||||
| /public/app/core/components/TimePicker/ @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/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/GraphNG/ @grafana/dataviz-squad | ||||||
| /public/app/core/components/TimeSeries/ @grafana/dataviz-squad | /public/app/core/components/TimeSeries/ @grafana/dataviz-squad | ||||||
| /public/app/core/components/TimelineChart/ @grafana/dataviz-squad | /public/app/core/components/TimelineChart/ @grafana/dataviz-squad | ||||||
|  |  | ||||||
|  | @ -243,6 +243,7 @@ | ||||||
|     "@grafana/google-sdk": "0.1.2", |     "@grafana/google-sdk": "0.1.2", | ||||||
|     "@grafana/lezer-logql": "0.2.2", |     "@grafana/lezer-logql": "0.2.2", | ||||||
|     "@grafana/monaco-logql": "^0.0.7", |     "@grafana/monaco-logql": "^0.0.7", | ||||||
|  |     "@grafana/o11y-ds-frontend": "workspace:*", | ||||||
|     "@grafana/runtime": "workspace:*", |     "@grafana/runtime": "workspace:*", | ||||||
|     "@grafana/scenes": "1.30.0", |     "@grafana/scenes": "1.30.0", | ||||||
|     "@grafana/schema": "workspace:*", |     "@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 { GrafanaTheme2 } from '@grafana/data'; | ||||||
| import { InlineLabel, SegmentInput, ToolbarButton, useStyles2 } from '@grafana/ui'; | import { InlineLabel, SegmentInput, ToolbarButton, useStyles2 } from '@grafana/ui'; | ||||||
| import { ToolbarButtonVariant } from '@grafana/ui/src/components/ToolbarButton'; |  | ||||||
| 
 | 
 | ||||||
| import { TraceToLogsTag } from './TraceToLogsSettings'; | import { TraceToLogsTag } from './TraceToLogsSettings'; | ||||||
| 
 | 
 | ||||||
|  | @ -13,8 +12,6 @@ interface Props { | ||||||
|   id?: string; |   id?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const VARIANT = 'none' as ToolbarButtonVariant; |  | ||||||
| 
 |  | ||||||
| export const TagMappingInput = ({ values, onChange, id }: Props) => { | export const TagMappingInput = ({ values, onChange, id }: Props) => { | ||||||
|   const styles = useStyles2(getStyles); |   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)])} |               onClick={() => onChange([...values.slice(0, idx), ...values.slice(idx + 1)])} | ||||||
|               className={cx(styles.removeTag, 'query-part')} |               className={cx(styles.removeTag, 'query-part')} | ||||||
|               aria-label="Remove tag" |               aria-label="Remove tag" | ||||||
|               variant={VARIANT} |  | ||||||
|               type="button" |               type="button" | ||||||
|               icon="times" |               icon="times" | ||||||
|             /> |             /> | ||||||
|  | @ -71,7 +67,6 @@ export const TagMappingInput = ({ values, onChange, id }: Props) => { | ||||||
|                 className="query-part" |                 className="query-part" | ||||||
|                 aria-label="Add tag" |                 aria-label="Add tag" | ||||||
|                 type="button" |                 type="button" | ||||||
|                 variant={VARIANT} |  | ||||||
|                 icon="plus" |                 icon="plus" | ||||||
|               /> |               /> | ||||||
|             ) : null} |             ) : null} | ||||||
|  | @ -84,7 +79,6 @@ export const TagMappingInput = ({ values, onChange, id }: Props) => { | ||||||
|           className="query-part" |           className="query-part" | ||||||
|           aria-label="Add tag" |           aria-label="Add tag" | ||||||
|           type="button" |           type="button" | ||||||
|           variant={VARIANT} |  | ||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
|     </div> |     </div> | ||||||
|  | @ -3,8 +3,8 @@ import React, { useCallback, useMemo } from 'react'; | ||||||
| 
 | 
 | ||||||
| import { DataSourceJsonData, DataSourceInstanceSettings, DataSourcePluginOptionsEditorProps } from '@grafana/data'; | import { DataSourceJsonData, DataSourceInstanceSettings, DataSourcePluginOptionsEditorProps } from '@grafana/data'; | ||||||
| import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; | import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; | ||||||
|  | import { DataSourcePicker } from '@grafana/runtime'; | ||||||
| import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; | import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; | ||||||
| import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; |  | ||||||
| 
 | 
 | ||||||
| import { IntervalInput } from '../IntervalInput/IntervalInput'; | import { IntervalInput } from '../IntervalInput/IntervalInput'; | ||||||
| 
 | 
 | ||||||
|  | @ -9,8 +9,8 @@ import { | ||||||
|   updateDatasourcePluginJsonDataOption, |   updateDatasourcePluginJsonDataOption, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
| import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; | import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; | ||||||
|  | import { DataSourcePicker } from '@grafana/runtime'; | ||||||
| import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui'; | import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui'; | ||||||
| import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; |  | ||||||
| 
 | 
 | ||||||
| import { IntervalInput } from '../IntervalInput/IntervalInput'; | import { IntervalInput } from '../IntervalInput/IntervalInput'; | ||||||
| import { TagMappingInput } from '../TraceToLogs/TagMappingInput'; | import { TagMappingInput } from '../TraceToLogs/TagMappingInput'; | ||||||
|  | @ -228,17 +228,17 @@ export const TraceToMetricsSection = ({ options, onOptionsChange }: DataSourcePl | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const getStyles = (theme: GrafanaTheme2) => ({ | const getStyles = (theme: GrafanaTheme2) => ({ | ||||||
|   infoText: css` |   infoText: { | ||||||
|     padding-bottom: ${theme.spacing(2)}; |     paddingBottom: theme.spacing(2), | ||||||
|     color: ${theme.colors.text.secondary}; |     color: theme.colors.text.secondary, | ||||||
|   `,
 |   }, | ||||||
|   row: css` |   row: css({ | ||||||
|     label: row; |     label: 'row', | ||||||
|     align-items: baseline; |     alignItems: 'baseline', | ||||||
|   `,
 |   }), | ||||||
|   queryRow: css` |   queryRow: css({ | ||||||
|     label: queryRow; |     label: 'queryRow', | ||||||
|     display: flex; |     display: 'flex', | ||||||
|     flex-flow: wrap; |     flexFlow: 'wrap', | ||||||
|   `,
 |   }), | ||||||
| }); | }); | ||||||
|  | @ -9,13 +9,13 @@ import { | ||||||
|   updateDatasourcePluginJsonDataOption, |   updateDatasourcePluginJsonDataOption, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
| import { ConfigDescriptionLink, ConfigSection } from '@grafana/experimental'; | 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 { 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 { TagMappingInput } from '../TraceToLogs/TagMappingInput'; | ||||||
|  | import { ProfileTypesCascader } from '../pyroscope/ProfileTypesCascader'; | ||||||
|  | import { ProfileTypeMessage } from '../pyroscope/types'; | ||||||
|  | 
 | ||||||
| export interface TraceToProfilesOptions { | export interface TraceToProfilesOptions { | ||||||
|   datasourceUid?: string; |   datasourceUid?: string; | ||||||
|   tags?: Array<{ key: string; value?: string }>; |   tags?: Array<{ key: string; value?: string }>; | ||||||
|  | @ -36,9 +36,3 @@ export interface GrafanaPyroscope extends common.DataQuery { | ||||||
|    */ |    */ | ||||||
|   spanSelector?: Array<string>; |   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[] } }; |   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, |   mapInternalLinkToExplore, | ||||||
|   SplitOpen, |   SplitOpen, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
|  | import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend'; | ||||||
| import { getTemplateSrv } from '@grafana/runtime'; | import { getTemplateSrv } from '@grafana/runtime'; | ||||||
| import { DataQuery } from '@grafana/schema'; | import { DataQuery } from '@grafana/schema'; | ||||||
| import { useStyles2 } from '@grafana/ui'; | import { useStyles2 } from '@grafana/ui'; | ||||||
| import { TempoQuery } from '@grafana-plugins/tempo/types'; | 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 { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; | ||||||
| import { getTimeZone } from 'app/features/profile/state/selectors'; | import { getTimeZone } from 'app/features/profile/state/selectors'; | ||||||
| import { useDispatch, useSelector } from 'app/types'; | 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 { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data'; | ||||||
| import { AccessoryButton } from '@grafana/experimental'; | 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 { 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 { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch'; | ||||||
| import SearchBarInput from '../../common/SearchBarInput'; | import SearchBarInput from '../../common/SearchBarInput'; | ||||||
|  |  | ||||||
|  | @ -13,9 +13,9 @@ import { | ||||||
|   TimeZone, |   TimeZone, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
| import { FlameGraph } from '@grafana/flamegraph'; | import { FlameGraph } from '@grafana/flamegraph'; | ||||||
|  | import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config, DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; | import { config, DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; | ||||||
| import { useStyles2 } from '@grafana/ui'; | import { useStyles2 } from '@grafana/ui'; | ||||||
| import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; |  | ||||||
| import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; | import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; | ||||||
| import { PyroscopeQueryType } from 'app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen'; | import { PyroscopeQueryType } from 'app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen'; | ||||||
| import { Query } from 'app/plugins/datasource/grafana-pyroscope-datasource/types'; | import { Query } from 'app/plugins/datasource/grafana-pyroscope-datasource/types'; | ||||||
|  |  | ||||||
|  | @ -18,11 +18,11 @@ import cx from 'classnames'; | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { DataFrame, dateTimeFormat, GrafanaTheme2, IconName, LinkModel } from '@grafana/data'; | import { DataFrame, dateTimeFormat, GrafanaTheme2, IconName, LinkModel } from '@grafana/data'; | ||||||
|  | import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config, locationService, reportInteraction } from '@grafana/runtime'; | import { config, locationService, reportInteraction } from '@grafana/runtime'; | ||||||
| import { TimeZone } from '@grafana/schema'; | import { TimeZone } from '@grafana/schema'; | ||||||
| import { DataLinkButton, Icon, TextArea, useStyles2 } from '@grafana/ui'; | import { DataLinkButton, Icon, TextArea, useStyles2 } from '@grafana/ui'; | ||||||
| import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer'; | import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer'; | ||||||
| import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; |  | ||||||
| 
 | 
 | ||||||
| import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; | import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; | ||||||
| import { autoColor } from '../../Theme'; | import { autoColor } from '../../Theme'; | ||||||
|  |  | ||||||
|  | @ -17,9 +17,9 @@ import classNames from 'classnames'; | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { GrafanaTheme2, LinkModel } from '@grafana/data'; | import { GrafanaTheme2, LinkModel } from '@grafana/data'; | ||||||
|  | import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| import { TimeZone } from '@grafana/schema'; | import { TimeZone } from '@grafana/schema'; | ||||||
| import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui'; | import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui'; | ||||||
| import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; |  | ||||||
| 
 | 
 | ||||||
| import { autoColor } from '../Theme'; | import { autoColor } from '../Theme'; | ||||||
| import { SpanLinkFunc } from '../types'; | import { SpanLinkFunc } from '../types'; | ||||||
|  |  | ||||||
|  | @ -19,10 +19,10 @@ import * as React from 'react'; | ||||||
| import { RefObject } from 'react'; | import { RefObject } from 'react'; | ||||||
| 
 | 
 | ||||||
| import { GrafanaTheme2, LinkModel } from '@grafana/data'; | import { GrafanaTheme2, LinkModel } from '@grafana/data'; | ||||||
|  | import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config, reportInteraction } from '@grafana/runtime'; | import { config, reportInteraction } from '@grafana/runtime'; | ||||||
| import { TimeZone } from '@grafana/schema'; | import { TimeZone } from '@grafana/schema'; | ||||||
| import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui'; | import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui'; | ||||||
| import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; |  | ||||||
| 
 | 
 | ||||||
| import { PEER_SERVICE } from '../constants/tag-keys'; | import { PEER_SERVICE } from '../constants/tag-keys'; | ||||||
| import { CriticalPathSection, SpanBarOptions, SpanLinkFunc, TNil } from '../types'; | import { CriticalPathSection, SpanBarOptions, SpanLinkFunc, TNil } from '../types'; | ||||||
|  |  | ||||||
|  | @ -16,10 +16,10 @@ import { css } from '@emotion/css'; | ||||||
| import React, { RefObject } from 'react'; | import React, { RefObject } from 'react'; | ||||||
| 
 | 
 | ||||||
| import { GrafanaTheme2, LinkModel } from '@grafana/data'; | import { GrafanaTheme2, LinkModel } from '@grafana/data'; | ||||||
|  | import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config, reportInteraction } from '@grafana/runtime'; | import { config, reportInteraction } from '@grafana/runtime'; | ||||||
| import { TimeZone } from '@grafana/schema'; | import { TimeZone } from '@grafana/schema'; | ||||||
| import { stylesFactory, withTheme2 } from '@grafana/ui'; | import { stylesFactory, withTheme2 } from '@grafana/ui'; | ||||||
| import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; |  | ||||||
| 
 | 
 | ||||||
| import { autoColor } from '../Theme'; | import { autoColor } from '../Theme'; | ||||||
| import { merge as mergeShortcuts } from '../keyboard-shortcuts'; | import { merge as mergeShortcuts } from '../keyboard-shortcuts'; | ||||||
|  |  | ||||||
|  | @ -7,11 +7,10 @@ import { | ||||||
|   FieldType, |   FieldType, | ||||||
|   DataFrame, |   DataFrame, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
|  | import { TraceToLogsOptionsV2, TraceToMetricsOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config, DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; | 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 { DatasourceSrv } from 'app/features/plugins/datasource_srv'; | ||||||
| 
 | 
 | ||||||
| import { TraceToLogsOptionsV2 } from '../../../core/components/TraceToLogs/TraceToLogsSettings'; |  | ||||||
| import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv'; | import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv'; | ||||||
| import { TemplateSrv } from '../../templating/template_srv'; | import { TemplateSrv } from '../../templating/template_srv'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,12 +14,16 @@ import { | ||||||
|   SplitOpen, |   SplitOpen, | ||||||
|   TimeRange, |   TimeRange, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
|  | import { | ||||||
|  |   TraceToProfilesOptions, | ||||||
|  |   TraceToMetricQuery, | ||||||
|  |   TraceToMetricsOptions, | ||||||
|  |   TraceToLogsOptionsV2, | ||||||
|  |   TraceToLogsTag, | ||||||
|  | } from '@grafana/o11y-ds-frontend'; | ||||||
| import { getTemplateSrv } from '@grafana/runtime'; | import { getTemplateSrv } from '@grafana/runtime'; | ||||||
| import { DataQuery } from '@grafana/schema'; | import { DataQuery } from '@grafana/schema'; | ||||||
| import { Icon } from '@grafana/ui'; | 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 { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; | ||||||
| import { PromQuery } from 'app/plugins/datasource/prometheus/types'; | import { PromQuery } from 'app/plugins/datasource/prometheus/types'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,11 +3,10 @@ import React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data'; | import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data'; | ||||||
| import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; | import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; | ||||||
|  | import { TraceToLogsSection, TraceToMetricsSection } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config } from '@grafana/runtime'; | import { config } from '@grafana/runtime'; | ||||||
| import { DataSourceHttpSettings, useStyles2, Divider, Stack } from '@grafana/ui'; | import { DataSourceHttpSettings, useStyles2, Divider, Stack } from '@grafana/ui'; | ||||||
| import { NodeGraphSection } from 'app/core/components/NodeGraphSettings'; | 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 { SpanBarSection } from 'app/features/explore/TraceView/components/settings/SpanBarSettings'; | ||||||
| 
 | 
 | ||||||
| import { TraceIdTimeParams } from './TraceIdTimeParams'; | import { TraceIdTimeParams } from './TraceIdTimeParams'; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import { DataFrame, NodeGraphDataFrameFieldNames as Fields } from '@grafana/data'; | 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 { Span, TraceResponse } from './types'; | 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, |   convertLegacyAuthProps, | ||||||
|   DataSourceDescription, |   DataSourceDescription, | ||||||
| } from '@grafana/experimental'; | } from '@grafana/experimental'; | ||||||
|  | import { TraceToLogsSection, TraceToMetricsSection, TraceToProfilesSection } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config } from '@grafana/runtime'; | import { config } from '@grafana/runtime'; | ||||||
| import { SecureSocksProxySettings, useStyles2, Divider, Stack } from '@grafana/ui'; | import { SecureSocksProxySettings, useStyles2, Divider, Stack } from '@grafana/ui'; | ||||||
| 
 | 
 | ||||||
| import { NodeGraphSection } from '../_importedDependencies/components/NodeGraphSettings'; | import { NodeGraphSection } from '../_importedDependencies/components/NodeGraphSettings'; | ||||||
| import { SpanBarSection } from '../_importedDependencies/components/TraceView/SpanBarSettings'; | import { SpanBarSection } from '../_importedDependencies/components/TraceView/SpanBarSettings'; | ||||||
| import { |  | ||||||
|   TraceToLogsSection, |  | ||||||
|   TraceToMetricsSection, |  | ||||||
|   TraceToProfilesSection, |  | ||||||
| } from '../_importedDependencies/grafana-traces/src'; |  | ||||||
| 
 | 
 | ||||||
| import { LokiSearchSettings } from './LokiSearchSettings'; | import { LokiSearchSettings } from './LokiSearchSettings'; | ||||||
| import { QuerySettings } from './QuerySettings'; | import { QuerySettings } from './QuerySettings'; | ||||||
|  |  | ||||||
|  | @ -2,9 +2,9 @@ import { css } from '@emotion/css'; | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { DataSourcePluginOptionsEditorProps, GrafanaTheme2, updateDatasourcePluginJsonDataOption } from '@grafana/data'; | import { DataSourcePluginOptionsEditorProps, GrafanaTheme2, updateDatasourcePluginJsonDataOption } from '@grafana/data'; | ||||||
|  | import { IntervalInput, invalidTimeShiftError } from '@grafana/o11y-ds-frontend'; | ||||||
| import { InlineField, InlineSwitch, useStyles2 } from '@grafana/ui'; | import { InlineField, InlineSwitch, useStyles2 } from '@grafana/ui'; | ||||||
| 
 | 
 | ||||||
| import { IntervalInput, invalidTimeShiftError } from '../_importedDependencies/grafana-traces/src'; |  | ||||||
| import { TempoJsonData } from '../types'; | import { TempoJsonData } from '../types'; | ||||||
| 
 | 
 | ||||||
| interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {} | interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {} | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ import { | ||||||
|   TestDataSourceResponse, |   TestDataSourceResponse, | ||||||
|   urlUtil, |   urlUtil, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
|  | import { TraceToLogsOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| import { | import { | ||||||
|   BackendSrvRequest, |   BackendSrvRequest, | ||||||
|   config, |   config, | ||||||
|  | @ -39,7 +40,6 @@ import { NodeGraphOptions } from './_importedDependencies/components/NodeGraphSe | ||||||
| import { SpanBarOptions } from './_importedDependencies/components/TraceView/SpanBarSettings'; | import { SpanBarOptions } from './_importedDependencies/components/TraceView/SpanBarSettings'; | ||||||
| import { LokiOptions } from './_importedDependencies/datasources/loki/types'; | import { LokiOptions } from './_importedDependencies/datasources/loki/types'; | ||||||
| import { PromQuery, PrometheusDatasource } from './_importedDependencies/datasources/prometheus/types'; | import { PromQuery, PrometheusDatasource } from './_importedDependencies/datasources/prometheus/types'; | ||||||
| import { TraceToLogsOptions } from './_importedDependencies/grafana-traces/src'; |  | ||||||
| import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; | import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; | ||||||
| import { | import { | ||||||
|   defaultTableFilter, |   defaultTableFilter, | ||||||
|  |  | ||||||
|  | @ -10,13 +10,7 @@ import { | ||||||
|   FieldType, |   FieldType, | ||||||
|   toDataFrame, |   toDataFrame, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
| 
 | import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '@grafana/o11y-ds-frontend'; | ||||||
| import { |  | ||||||
|   getStats, |  | ||||||
|   getNonOverlappingDuration, |  | ||||||
|   makeSpanMap, |  | ||||||
|   makeFrames, |  | ||||||
| } from './_importedDependencies/grafana-traces/src'; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Row in a trace dataFrame |  * Row in a trace dataFrame | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
|     "@grafana/lezer-logql": "0.2.2", |     "@grafana/lezer-logql": "0.2.2", | ||||||
|     "@grafana/lezer-traceql": "0.0.12", |     "@grafana/lezer-traceql": "0.0.12", | ||||||
|     "@grafana/monaco-logql": "^0.0.7", |     "@grafana/monaco-logql": "^0.0.7", | ||||||
|  |     "@grafana/o11y-ds-frontend": "workspace:*", | ||||||
|     "@grafana/runtime": "workspace:*", |     "@grafana/runtime": "workspace:*", | ||||||
|     "@grafana/schema": "workspace:*", |     "@grafana/schema": "workspace:*", | ||||||
|     "@grafana/ui": "workspace:*", |     "@grafana/ui": "workspace:*", | ||||||
|  |  | ||||||
|  | @ -23,9 +23,9 @@ import { | ||||||
|   Field, |   Field, | ||||||
|   DataLinkConfigOrigin, |   DataLinkConfigOrigin, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
|  | import { TraceToProfilesData } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config, getDataSourceSrv } from '@grafana/runtime'; | import { config, getDataSourceSrv } from '@grafana/runtime'; | ||||||
| 
 | 
 | ||||||
| import { TraceToProfilesData } from './_importedDependencies/grafana-traces/src'; |  | ||||||
| import { SearchTableType } from './dataquery.gen'; | import { SearchTableType } from './dataquery.gen'; | ||||||
| import { createGraphFrames } from './graphTransform'; | import { createGraphFrames } from './graphTransform'; | ||||||
| import { Span, SpanAttributes, Spanset, TempoJsonData, TraceSearchMetadata } from './types'; | import { Span, SpanAttributes, Spanset, TempoJsonData, TraceSearchMetadata } from './types'; | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { DataSourceJsonData } from '@grafana/data/src'; | import { DataSourceJsonData } from '@grafana/data/src'; | ||||||
|  | import { TraceToLogsOptions } from '@grafana/o11y-ds-frontend'; | ||||||
| 
 | 
 | ||||||
| import { NodeGraphOptions } from './_importedDependencies/components/NodeGraphSettings'; | import { NodeGraphOptions } from './_importedDependencies/components/NodeGraphSettings'; | ||||||
| import { LokiQuery } from './_importedDependencies/datasources/loki/types'; | import { LokiQuery } from './_importedDependencies/datasources/loki/types'; | ||||||
| import { TraceToLogsOptions } from './_importedDependencies/grafana-traces/src'; |  | ||||||
| import { TempoQuery as TempoBase, TempoQueryType, TraceqlFilter } from './dataquery.gen'; | import { TempoQuery as TempoBase, TempoQueryType, TraceqlFilter } from './dataquery.gen'; | ||||||
| 
 | 
 | ||||||
| export interface SearchQueryParams { | export interface SearchQueryParams { | ||||||
|  |  | ||||||
|  | @ -3,11 +3,10 @@ import React from 'react'; | ||||||
| 
 | 
 | ||||||
| import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data'; | import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data'; | ||||||
| import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; | import { ConfigSection, DataSourceDescription } from '@grafana/experimental'; | ||||||
|  | import { TraceToLogsSection, TraceToMetricsSection } from '@grafana/o11y-ds-frontend'; | ||||||
| import { config } from '@grafana/runtime'; | import { config } from '@grafana/runtime'; | ||||||
| import { DataSourceHttpSettings, useStyles2, Divider, Stack } from '@grafana/ui'; | import { DataSourceHttpSettings, useStyles2, Divider, Stack } from '@grafana/ui'; | ||||||
| import { NodeGraphSection } from 'app/core/components/NodeGraphSettings'; | 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 { SpanBarSection } from 'app/features/explore/TraceView/components/settings/SpanBarSettings'; | ||||||
| 
 | 
 | ||||||
| export type Props = DataSourcePluginOptionsEditorProps; | export type Props = DataSourcePluginOptionsEditorProps; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { DataFrame, NodeGraphDataFrameFieldNames as Fields } from '@grafana/data'; | 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'; | import { ZipkinSpan } from '../types'; | ||||||
| 
 | 
 | ||||||
| interface Node { | interface Node { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| PACKAGES=$(ls -d ./packages/*/) | PACKAGES=$(ls -d ./packages/*/) | ||||||
| EXIT_CODE=0 | EXIT_CODE=0 | ||||||
| GITHUB_MESSAGE="" | 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 | # Loop through the packages | ||||||
| while IFS=" " read -r -a package; do | 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-logql": "npm:0.2.2" | ||||||
|     "@grafana/lezer-traceql": "npm:0.0.12" |     "@grafana/lezer-traceql": "npm:0.0.12" | ||||||
|     "@grafana/monaco-logql": "npm:^0.0.7" |     "@grafana/monaco-logql": "npm:^0.0.7" | ||||||
|  |     "@grafana/o11y-ds-frontend": "workspace:*" | ||||||
|     "@grafana/plugin-configs": "npm:10.4.0-pre" |     "@grafana/plugin-configs": "npm:10.4.0-pre" | ||||||
|     "@grafana/runtime": "workspace:*" |     "@grafana/runtime": "workspace:*" | ||||||
|     "@grafana/schema": "workspace:*" |     "@grafana/schema": "workspace:*" | ||||||
|  | @ -3457,6 +3458,39 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   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": | "@grafana/plugin-configs@npm:10.4.0-pre, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs": | ||||||
|   version: 0.0.0-use.local |   version: 0.0.0-use.local | ||||||
|   resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" |   resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" | ||||||
|  | @ -16933,6 +16967,7 @@ __metadata: | ||||||
|     "@grafana/google-sdk": "npm:0.1.2" |     "@grafana/google-sdk": "npm:0.1.2" | ||||||
|     "@grafana/lezer-logql": "npm:0.2.2" |     "@grafana/lezer-logql": "npm:0.2.2" | ||||||
|     "@grafana/monaco-logql": "npm:^0.0.7" |     "@grafana/monaco-logql": "npm:^0.0.7" | ||||||
|  |     "@grafana/o11y-ds-frontend": "workspace:*" | ||||||
|     "@grafana/runtime": "workspace:*" |     "@grafana/runtime": "workspace:*" | ||||||
|     "@grafana/scenes": "npm:1.30.0" |     "@grafana/scenes": "npm:1.30.0" | ||||||
|     "@grafana/schema": "workspace:*" |     "@grafana/schema": "workspace:*" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue