diff --git a/packages/grafana-data/src/datetime/datemath.ts b/packages/grafana-data/src/datetime/datemath.ts index d8c850c9746..af7d8cceb79 100644 --- a/packages/grafana-data/src/datetime/datemath.ts +++ b/packages/grafana-data/src/datetime/datemath.ts @@ -26,7 +26,11 @@ export namespace dateMath { * @param roundUp See parseDateMath function. * @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used. */ - export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: TimeZone): DateTime | undefined { + export function parse( + text?: string | DateTime | Date | null, + roundUp?: boolean, + timezone?: TimeZone + ): DateTime | undefined { if (!text) { return undefined; } diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 0e6fcb6769b..e399e5c55eb 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -27,6 +27,9 @@ export interface DataLink { // 1: If exists, handle click directly // Not saved in JSON/DTO onClick?: (event: DataLinkClickEvent) => void; + + // At the moment this is used for derived fields for metadata about internal linking. + meta?: any; } export type LinkTarget = '_blank' | '_self'; diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 783c01eab77..cbe639e2dbb 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -115,6 +115,7 @@ export interface DataSourcePluginMeta extends PluginMet logs?: boolean; annotations?: boolean; alerting?: boolean; + tracing?: boolean; mixed?: boolean; hasQueryHelp?: boolean; category?: string; @@ -316,6 +317,7 @@ export enum DataSourceStatus { export enum ExploreMode { Logs = 'Logs', Metrics = 'Metrics', + Tracing = 'Tracing', } export interface ExploreQueryFieldProps< diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index fd033e1aaed..dde91c87a1f 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -19,6 +19,7 @@ interface FeatureToggles { newEdit: boolean; meta: boolean; newVariables: boolean; + tracingIntegration: boolean; } interface LicenseInfo { @@ -71,6 +72,7 @@ export class GrafanaBootConfig { newEdit: false, meta: false, newVariables: false, + tracingIntegration: false, }; licenseInfo: LicenseInfo = {} as LicenseInfo; phantomJSRenderer = false; diff --git a/packages/grafana-ui/src/components/Logs/LogDetails.tsx b/packages/grafana-ui/src/components/Logs/LogDetails.tsx index 791edf0b09c..9d5a1c642c4 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetails.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetails.tsx @@ -24,7 +24,7 @@ import { LogDetailsRow } from './LogDetailsRow'; type FieldDef = { key: string; value: string; - links?: string[]; + links?: Array>; fieldIndex?: number; }; @@ -99,7 +99,7 @@ class UnThemedLogDetails extends PureComponent { return { key: field.name, value: field.values.get(row.rowIndex).toString(), - links: links.map(link => link.href), + links: links, fieldIndex: field.index, }; }) diff --git a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx index bf53bacd1e7..b83d46bc8e7 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { css, cx } from 'emotion'; -import { LogLabelStatsModel, GrafanaTheme } from '@grafana/data'; +import { Field, LinkModel, LogLabelStatsModel, GrafanaTheme } from '@grafana/data'; import { Themeable } from '../../types/theme'; import { withTheme } from '../../themes/index'; @@ -9,6 +9,7 @@ import { stylesFactory } from '../../themes/stylesFactory'; //Components import { LogLabelStats } from './LogLabelStats'; +import { LinkButton } from '../Button/Button'; export interface Props extends Themeable { parsedValue: string; @@ -16,7 +17,7 @@ export interface Props extends Themeable { isLabel?: boolean; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; - links?: string[]; + links?: Array>; getStats: () => LogLabelStatsModel[] | null; } @@ -122,11 +123,27 @@ class UnThemedLogDetailsRow extends PureComponent { {links && links.map(link => { return ( - -   - - - + + <> +   + { + if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) { + event.preventDefault(); + link.onClick(event); + } + } + : undefined + } + /> + ); })} diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 645a8a3c239..5f13c90767e 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -92,7 +92,11 @@ func pluginScenario(desc string, t *testing.T, fn func()) { _, err := sec.NewKey("path", "testdata/test-app") So(err, ShouldBeNil) - pm := &PluginManager{} + pm := &PluginManager{ + Cfg: &setting.Cfg{ + FeatureToggles: map[string]bool{}, + }, + } err = pm.Init() So(err, ShouldBeNil) diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go index 4ea9d7abd7d..d264889fd88 100644 --- a/pkg/plugins/dashboards_test.go +++ b/pkg/plugins/dashboards_test.go @@ -18,7 +18,11 @@ func TestPluginDashboards(t *testing.T) { _, err := sec.NewKey("path", "testdata/test-app") So(err, ShouldBeNil) - pm := &PluginManager{} + pm := &PluginManager{ + Cfg: &setting.Cfg{ + FeatureToggles: map[string]bool{}, + }, + } err = pm.Init() So(err, ShouldBeNil) diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go index 0b6654a04c9..7159312f07b 100644 --- a/pkg/plugins/datasource_plugin.go +++ b/pkg/plugins/datasource_plugin.go @@ -23,6 +23,7 @@ type DataSourcePlugin struct { Explore bool `json:"explore"` Table bool `json:"tables"` Logs bool `json:"logs"` + Tracing bool `json:"tracing"` QueryOptions map[string]bool `json:"queryOptions,omitempty"` BuiltIn bool `json:"builtIn,omitempty"` Mixed bool `json:"mixed,omitempty"` diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 7d929e11e61..30d2a64d4c1 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -42,10 +42,12 @@ type PluginScanner struct { pluginPath string errors []error backendPluginManager backendplugin.Manager + cfg *setting.Cfg } type PluginManager struct { BackendPluginManager backendplugin.Manager `inject:""` + Cfg *setting.Cfg `inject:""` log log.Logger } @@ -164,6 +166,7 @@ func (pm *PluginManager) scan(pluginDir string) error { scanner := &PluginScanner{ pluginPath: pluginDir, backendPluginManager: pm.BackendPluginManager, + cfg: pm.Cfg, } if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil { @@ -213,6 +216,14 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro return nil } + if !scanner.cfg.FeatureToggles["tracingIntegration"] { + // Do not load tracing datasources if + prefix := path.Join(setting.StaticRootPath, "app/plugins/datasource") + if strings.Contains(currentPath, path.Join(prefix, "jaeger")) || strings.Contains(currentPath, path.Join(prefix, "zipkin")) { + return nil + } + } + if f.Name() == "plugin.json" { err := scanner.loadPluginJson(currentPath) if err != nil { diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 00c1c42b182..f1a36ee7bc5 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -15,7 +15,11 @@ func TestPluginScans(t *testing.T) { setting.StaticRootPath, _ = filepath.Abs("../../public/") setting.Raw = ini.Empty() - pm := &PluginManager{} + pm := &PluginManager{ + Cfg: &setting.Cfg{ + FeatureToggles: map[string]bool{}, + }, + } err := pm.Init() So(err, ShouldBeNil) @@ -34,7 +38,11 @@ func TestPluginScans(t *testing.T) { _, err = sec.NewKey("path", "testdata/test-app") So(err, ShouldBeNil) - pm := &PluginManager{} + pm := &PluginManager{ + Cfg: &setting.Cfg{ + FeatureToggles: map[string]bool{}, + }, + } err = pm.Init() So(err, ShouldBeNil) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 3dd932a4749..a202095681a 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -281,6 +281,7 @@ type Cfg struct { ApiKeyMaxSecondsToLive int64 + // Use to enable new features which may still be in alpha/beta stage. FeatureToggles map[string]bool } diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 4d5962babc5..78952430aec 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -66,17 +66,17 @@ export interface GetExploreUrlArguments { datasourceSrv: DataSourceSrv; timeSrv: TimeSrv; } -export async function getExploreUrl(args: GetExploreUrlArguments) { +export async function getExploreUrl(args: GetExploreUrlArguments): Promise { const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args; let exploreDatasource = panelDatasource; let exploreTargets: DataQuery[] = panelTargets; - let url: string; + let url: string | undefined; // Mixed datasources need to choose only one datasource - if (panelDatasource.meta.id === 'mixed' && exploreTargets) { + if (panelDatasource.meta?.id === 'mixed' && exploreTargets) { // Find first explore datasource among targets for (const t of exploreTargets) { - const datasource = await datasourceSrv.get(t.datasource); + const datasource = await datasourceSrv.get(t.datasource || undefined); if (datasource) { exploreDatasource = datasource; exploreTargets = panelTargets.filter(t => t.datasource === datasource.name); @@ -183,7 +183,7 @@ enum ParseUiStateIndex { Strategy = 3, } -export const safeParseJson = (text: string) => { +export const safeParseJson = (text?: string): any | undefined => { if (!text) { return; } @@ -365,7 +365,7 @@ export function clearHistory(datasourceId: string) { } export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => { - const queryKeys = queries.reduce((newQueryKeys, query, index) => { + const queryKeys = queries.reduce((newQueryKeys, query, index) => { const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key; return newQueryKeys.concat(`${primaryKey}-${index}`); }, []); @@ -381,7 +381,7 @@ export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRa }; }; -const parseRawTime = (value: any): TimeFragment => { +const parseRawTime = (value: any): TimeFragment | null => { if (value === null) { return null; } @@ -442,7 +442,7 @@ export const getValueWithRefId = (value?: any): any => { return undefined; }; -export const getFirstQueryErrorWithoutRefId = (errors?: DataQueryError[]) => { +export const getFirstQueryErrorWithoutRefId = (errors?: DataQueryError[]): DataQueryError | undefined => { if (!errors) { return undefined; } @@ -530,7 +530,7 @@ export const stopQueryState = (querySubscription: Unsubscribable) => { } }; -export function getIntervals(range: TimeRange, lowLimit: string, resolution: number): IntervalValues { +export function getIntervals(range: TimeRange, lowLimit: string, resolution?: number): IntervalValues { if (!resolution) { return { interval: '1s', intervalMs: 1000 }; } @@ -542,7 +542,7 @@ export function deduplicateLogRowsById(rows: LogRowModel[]) { return _.uniqBy(rows, 'uid'); } -export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]) => { +export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]): DataQueryError | undefined => { const refId = getValueWithRefId(queryErrors); - return refId ? null : getFirstQueryErrorWithoutRefId(queryErrors); + return refId ? undefined : getFirstQueryErrorWithoutRefId(queryErrors); }; diff --git a/public/app/core/utils/fetch.ts b/public/app/core/utils/fetch.ts index 5d1630b5f77..e7fa4b84dbb 100644 --- a/public/app/core/utils/fetch.ts +++ b/public/app/core/utils/fetch.ts @@ -88,7 +88,7 @@ export const parseBody = (options: BackendSrvRequest, isAppJson: boolean) => { return isAppJson ? JSON.stringify(options.data) : new URLSearchParams(options.data); }; -function serializeParams(data: Record): string { +export function serializeParams(data: Record): string { return Object.keys(data) .map(key => { const value = data[key]; diff --git a/public/app/core/utils/query.ts b/public/app/core/utils/query.ts index 5a6cdfb0b52..0b5c2740174 100644 --- a/public/app/core/utils/query.ts +++ b/public/app/core/utils/query.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { DataQuery } from '@grafana/data'; -export const getNextRefIdChar = (queries: DataQuery[]): string => { +export const getNextRefIdChar = (queries: DataQuery[]): string | undefined => { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; return _.find(letters, refId => { diff --git a/public/app/features/datasources/state/buildCategories.ts b/public/app/features/datasources/state/buildCategories.ts index 995b9809827..a6d906e7929 100644 --- a/public/app/features/datasources/state/buildCategories.ts +++ b/public/app/features/datasources/state/buildCategories.ts @@ -1,15 +1,17 @@ import { DataSourcePluginMeta, PluginType } from '@grafana/data'; import { DataSourcePluginCategory } from 'app/types'; +import { config } from '@grafana/runtime'; export function buildCategories(plugins: DataSourcePluginMeta[]): DataSourcePluginCategory[] { const categories: DataSourcePluginCategory[] = [ { id: 'tsdb', title: 'Time series databases', plugins: [] }, { id: 'logging', title: 'Logging & document databases', plugins: [] }, + config.featureToggles.tracingIntegration ? { id: 'tracing', title: 'Distributed tracing', plugins: [] } : null, { id: 'sql', title: 'SQL', plugins: [] }, { id: 'cloud', title: 'Cloud', plugins: [] }, { id: 'enterprise', title: 'Enterprise plugins', plugins: [] }, { id: 'other', title: 'Others', plugins: [] }, - ]; + ].filter(item => item); const categoryIndex: Record = {}; const pluginIndex: Record = {}; @@ -66,6 +68,7 @@ function sortPlugins(plugins: DataSourcePluginMeta[]) { graphite: 95, loki: 90, mysql: 80, + jaeger: 100, postgres: 79, gcloud: -1, }; diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index a4c963c6ce6..545f664bcd6 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -148,13 +148,12 @@ describe('Explore', () => { it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => { const queryErrors = setupErrors(true); const queryError = getFirstNonQueryRowSpecificError(queryErrors); - expect(queryError).toBeNull(); + expect(queryError).toBeUndefined(); }); it('should not filter out a generic error when looking for non-query-row-specific errors', async () => { const queryErrors = setupErrors(); const queryError = getFirstNonQueryRowSpecificError(queryErrors); - expect(queryError).not.toBeNull(); expect(queryError).toEqual({ message: 'Error message', status: '400', diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 469df73ae15..2b5bdf42a10 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -20,33 +20,33 @@ import { changeSize, initializeExplore, modifyQueries, + refreshExplore, scanStart, setQueries, - refreshExplore, - updateTimeRange, toggleGraph, addQueryRow, + updateTimeRange, } from './state/actions'; // Types import { + AbsoluteTimeRange, DataQuery, DataSourceApi, + GraphSeriesXY, PanelData, RawTimeRange, TimeRange, - GraphSeriesXY, TimeZone, - AbsoluteTimeRange, LoadingState, ExploreMode, } from '@grafana/data'; -import { ExploreItemState, ExploreUrlState, ExploreId, ExploreUpdateState, ExploreUIState } from 'app/types/explore'; +import { ExploreId, ExploreItemState, ExploreUIState, ExploreUpdateState, ExploreUrlState } from 'app/types/explore'; import { StoreState } from 'app/types'; import { - ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE, + ensureQueries, getTimeRangeFromUrl, getTimeRange, lastUsedDatasourceKeyForOrgId, @@ -70,6 +70,18 @@ const getStyles = stylesFactory(() => { button: css` margin: 1em 4px 0 0; `, + // Utility class for iframe parents so that we can show iframe content with reasonable height instead of squished + // or some random explicit height. + fullHeight: css` + label: fullHeight; + height: 100%; + `, + iframe: css` + label: iframe; + border: none; + width: 100%; + height: 100%; + `, }; }); @@ -328,14 +340,14 @@ export class Explore extends React.PureComponent { - + {({ width }) => { if (width === 0) { return null; } return ( -
+
{showStartPage && StartPage && (
@@ -379,6 +391,18 @@ export class Explore extends React.PureComponent { onStopScanning={this.onStopScanning} /> )} + {mode === ExploreMode.Tracing && ( +
+ {queryResponse && + !!queryResponse.series.length && + queryResponse.series[0].fields[0].values.get(0) && ( +