diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index 067d4af8f92..cfdc9e82530 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -59,6 +59,13 @@ export interface QueryResultMeta { /** Currently used to show results in Explore only in preferred visualisation option */ preferredVisualisationType?: PreferredVisualisationType; + /** Set the panel plugin id to use to render the data when using Explore. If the plugin cannot be found + * will fall back to {@link preferredVisualisationType}. + * + * @alpha + */ + preferredVisualisationPluginId?: string; + /** The path for live stream updates for this frame */ channel?: string; diff --git a/public/app/features/explore/CustomContainer.tsx b/public/app/features/explore/CustomContainer.tsx new file mode 100644 index 00000000000..42629d97e15 --- /dev/null +++ b/public/app/features/explore/CustomContainer.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { AbsoluteTimeRange, DataFrame, dateTime, LoadingState } from '@grafana/data'; +import { PanelRenderer } from '@grafana/runtime'; +import { PanelChrome } from '@grafana/ui'; + +import { getPanelPluginMeta } from '../plugins/importPanelPlugin'; + +export interface Props { + width: number; + height: number; + timeZone: string; + pluginId: string; + frames: DataFrame[]; + absoluteRange: AbsoluteTimeRange; + state: LoadingState; +} + +export function CustomContainer({ width, height, timeZone, state, pluginId, frames, absoluteRange }: Props) { + const timeRange = { + from: dateTime(absoluteRange.from), + to: dateTime(absoluteRange.to), + raw: { + from: dateTime(absoluteRange.from), + to: dateTime(absoluteRange.to), + }, + }; + + const plugin = getPanelPluginMeta(pluginId); + + return ( + + {(innerWidth, innerHeight) => ( + + )} + + ); +} diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 0edebb7b230..d8432179815 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -86,6 +86,7 @@ const dummyProps: Props = { showLogs: true, showTable: true, showTrace: true, + showCustom: true, showNodeGraph: true, showFlameGraph: true, splitOpen: jest.fn(), diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 12b8482527b..fe3d3ca8170 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { get } from 'lodash'; +import { get, groupBy } from 'lodash'; import memoizeOne from 'memoize-one'; import React, { createRef } from 'react'; import { connect, ConnectedProps } from 'react-redux'; @@ -37,6 +37,7 @@ import { AbsoluteTimeEvent } from 'app/types/events'; import { getTimeZone } from '../profile/state/selectors'; +import { CustomContainer } from './CustomContainer'; import ExploreQueryInspector from './ExploreQueryInspector'; import { ExploreToolbar } from './ExploreToolbar'; import { FlameGraphExploreContainer } from './FlameGraph/FlameGraphExploreContainer'; @@ -283,6 +284,27 @@ export class Explore extends React.PureComponent { return ; } + renderCustom(width: number) { + const { timeZone, queryResponse, absoluteRange } = this.props; + + const groupedByPlugin = groupBy(queryResponse?.customFrames, 'meta.preferredVisualisationPluginId'); + + return Object.entries(groupedByPlugin).map(([pluginId, frames], index) => { + return ( + + ); + }); + } + renderGraphPanel(width: number) { const { graphResult, absoluteRange, timeZone, queryResponse, showFlameGraph } = this.props; @@ -423,6 +445,7 @@ export class Explore extends React.PureComponent { showRawPrometheus, showLogs, showTrace, + showCustom, showNodeGraph, showFlameGraph, timeZone, @@ -444,6 +467,7 @@ export class Explore extends React.PureComponent { queryResponse.tableFrames, queryResponse.rawPrometheusFrames, queryResponse.traceFrames, + queryResponse.customFrames, ].every((e) => e.length === 0); return ( @@ -496,6 +520,7 @@ export class Explore extends React.PureComponent { {config.featureToggles.logsSampleInExplore && showLogsSample && ( {this.renderLogsSamplePanel()} )} + {showCustom && {this.renderCustom(width)}} {showNoData && {this.renderNoData()}} )} @@ -546,6 +571,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { showMetrics, showTable, showTrace, + showCustom, absoluteRange, queryResponse, showNodeGraph, @@ -574,6 +600,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { showMetrics, showTable, showTrace, + showCustom, showNodeGraph, showRawPrometheus, showFlameGraph, diff --git a/public/app/features/explore/ExploreQueryInspector.test.tsx b/public/app/features/explore/ExploreQueryInspector.test.tsx index c07c2898484..950905a4d46 100644 --- a/public/app/features/explore/ExploreQueryInspector.test.tsx +++ b/public/app/features/explore/ExploreQueryInspector.test.tsx @@ -48,6 +48,7 @@ const setup = (propOverrides = {}) => { logsFrames: [], tableFrames: [], traceFrames: [], + customFrames: [], nodeGraphFrames: [], flameGraphFrames: [], rawPrometheusFrames: [], diff --git a/public/app/features/explore/__mocks__/data.ts b/public/app/features/explore/__mocks__/data.ts index aaeef2bb6da..46f9355c104 100644 --- a/public/app/features/explore/__mocks__/data.ts +++ b/public/app/features/explore/__mocks__/data.ts @@ -13,6 +13,7 @@ export const mockExplorePanelData = (props?: MockProps): Observable { ); } ); + + it('Sets visualization to plugin panel ID if there are custom panel frames', async () => { + const queries = [{ refId: 'A' }]; + const queryResponse: ExplorePanelData = { + ...createEmptyQueryResponse(), + ['customFrames']: [ + new MutableDataFrame({ + refId: 'A', + fields: [], + meta: { preferredVisualisationPluginId: 'someCustomPluginId' }, + }), + ], + }; + + await setDashboardInLocalStorage({ queries, queryResponse }); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + dashboard: expect.objectContaining({ + panels: expect.arrayContaining([expect.objectContaining({ type: 'someCustomPluginId' })]), + }), + }) + ); + }); }); }); }); diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts index 9720d5c2c76..3bee074b11c 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts @@ -80,6 +80,10 @@ function getPanelType(queries: DataQuery[], queryResponse: ExplorePanelData) { if (queryResponse.traceFrames.some(hasQueryRefId)) { return 'traces'; } + if (queryResponse.customFrames.some(hasQueryRefId)) { + // we will always have a custom frame and meta, it should never default to 'table' (but all paths must return a string) + return queryResponse.customFrames.find(hasQueryRefId)?.meta?.preferredVisualisationPluginId ?? 'table'; + } } // falling back to table diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index f2a5ccdc184..79ede4ef485 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -1182,6 +1182,7 @@ export const processQueryResponse = ( nodeGraphFrames, flameGraphFrames, rawPrometheusFrames, + customFrames, } = response; if (error) { @@ -1224,6 +1225,7 @@ export const processQueryResponse = ( showNodeGraph: !!nodeGraphFrames.length, showRawPrometheus: !!rawPrometheusFrames.length, showFlameGraph: !!flameGraphFrames.length, + showCustom: !!customFrames?.length, clearedAtIndex: state.isLive ? state.clearedAtIndex : null, }; }; diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 53c2f356191..fb486fed953 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -82,6 +82,7 @@ export const createEmptyQueryResponse = (): ExplorePanelData => ({ traceFrames: [], nodeGraphFrames: [], flameGraphFrames: [], + customFrames: [], tableFrames: [], rawPrometheusFrames: [], rawPrometheusResult: null, diff --git a/public/app/features/explore/utils/decorators.test.ts b/public/app/features/explore/utils/decorators.test.ts index cca227e2816..20296b97df8 100644 --- a/public/app/features/explore/utils/decorators.test.ts +++ b/public/app/features/explore/utils/decorators.test.ts @@ -17,6 +17,16 @@ jest.mock('@grafana/data', () => ({ dateTimeFormatTimeAgo: () => 'fromNow() jest mocked', })); +jest.mock('../../plugins/importPanelPlugin', () => { + const actual = jest.requireActual('../../plugins/importPanelPlugin'); + return { + ...actual, + hasPanelPlugin: (id: string) => { + return id === 'someCustomPanelPlugin'; + }, + }; +}); + const getTestContext = () => { const timeSeries = toDataFrame({ name: 'A-series', @@ -84,6 +94,7 @@ const createExplorePanelData = (args: Partial): ExplorePanelDa tableResult: null, traceFrames: [], nodeGraphFrames: [], + customFrames: [], flameGraphFrames: [], rawPrometheusFrames: [], rawPrometheusResult: null, @@ -111,6 +122,7 @@ describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => { tableFrames: [table, emptyTable], logsFrames: [logs], traceFrames: [], + customFrames: [], nodeGraphFrames: [], flameGraphFrames: [flameGraph], graphResult: null, @@ -139,6 +151,7 @@ describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => { logsFrames: [], traceFrames: [], nodeGraphFrames: [], + customFrames: [], flameGraphFrames: [], graphResult: null, tableResult: null, @@ -169,6 +182,7 @@ describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => { logsFrames: [logs], traceFrames: [], nodeGraphFrames: [], + customFrames: [], flameGraphFrames: [], graphResult: null, tableResult: null, @@ -314,3 +328,37 @@ describe('decorateWithLogsResult', () => { expect(decorateWithLogsResult()(panelData).logsResult).not.toBeNull(); }); }); + +describe('decorateWithCustomFrames', () => { + it('returns empty array if no custom frames', () => { + const { table, logs, timeSeries, emptyTable, flameGraph } = getTestContext(); + const series = [table, logs, timeSeries, emptyTable, flameGraph]; + const timeRange = getDefaultTimeRange(); + const panelData: PanelData = { + series, + state: LoadingState.Done, + timeRange, + }; + + expect(decorateWithFrameTypeMetadata(panelData).customFrames).toEqual([]); + }); + it('returns data if we have custom frames', () => { + const { table, logs, timeSeries, emptyTable, flameGraph } = getTestContext(); + const customFrame = toDataFrame({ + name: 'custom-panel', + refId: 'A', + fields: [], + meta: { preferredVisualisationType: 'table', preferredVisualisationPluginId: 'someCustomPanelPlugin' }, + }); + + const series = [table, logs, timeSeries, emptyTable, flameGraph, customFrame]; + const timeRange = getDefaultTimeRange(); + const panelData: PanelData = { + series, + state: LoadingState.Done, + timeRange, + }; + + expect(decorateWithFrameTypeMetadata(panelData).customFrames).toEqual([customFrame]); + }); +}); diff --git a/public/app/features/explore/utils/decorators.ts b/public/app/features/explore/utils/decorators.ts index bca06ac5894..99ac1dbcacb 100644 --- a/public/app/features/explore/utils/decorators.ts +++ b/public/app/features/explore/utils/decorators.ts @@ -20,6 +20,7 @@ import { CorrelationData } from '../../correlations/useCorrelations'; import { attachCorrelationsToDataFrames } from '../../correlations/utils'; import { dataFrameToLogsModel } from '../../logs/logsModel'; import { sortLogsResult } from '../../logs/utils'; +import { hasPanelPlugin } from '../../plugins/importPanelPlugin'; /** * When processing response first we try to determine what kind of dataframes we got as one query can return multiple @@ -34,8 +35,13 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData const traceFrames: DataFrame[] = []; const nodeGraphFrames: DataFrame[] = []; const flameGraphFrames: DataFrame[] = []; + const customFrames: DataFrame[] = []; for (const frame of data.series) { + if (canFindPanel(frame)) { + customFrames.push(frame); + continue; + } switch (frame.meta?.preferredVisualisationType) { case 'logs': logsFrames.push(frame); @@ -76,6 +82,7 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData logsFrames, traceFrames, nodeGraphFrames, + customFrames, flameGraphFrames, rawPrometheusFrames, graphResult: null, @@ -270,3 +277,15 @@ function isTimeSeries(frame: DataFrame): boolean { Object.keys(grouped).length === 2 && grouped[FieldType.time]?.length === 1 && grouped[FieldType.number] ); } + +/** + * Can we find a panel that matches the type defined on the frame + * + * @param frame + */ +function canFindPanel(frame: DataFrame): boolean { + if (!!frame.meta?.preferredVisualisationPluginId) { + return hasPanelPlugin(frame.meta?.preferredVisualisationPluginId); + } + return false; +} diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index e6a708789c2..00238238d86 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -14,7 +14,7 @@ export function importPanelPlugin(id: string): Promise { return loaded; } - const meta = config.panels[id] || Object.values(config.panels).find((p) => p.alias === id); + const meta = getPanelPluginMeta(id); if (!meta) { throw new Error(`Plugin ${id} not found`); @@ -28,6 +28,14 @@ export function importPanelPlugin(id: string): Promise { return promiseCache[id]; } +export function hasPanelPlugin(id: string): boolean { + return !!getPanelPluginMeta(id); +} + +export function getPanelPluginMeta(id: string): PanelPluginMeta { + return config.panels[id] || Object.values(config.panels).find((p) => p.alias === id); +} + export function importPanelPluginFromMeta(meta: PanelPluginMeta): Promise { return getPanelPlugin(meta); } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 29899e70840..1314f38779d 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -169,6 +169,7 @@ export interface ExploreItemState { showTrace?: boolean; showNodeGraph?: boolean; showFlameGraph?: boolean; + showCustom?: boolean; /** * History of all queries @@ -230,6 +231,7 @@ export interface ExplorePanelData extends PanelData { tableFrames: DataFrame[]; logsFrames: DataFrame[]; traceFrames: DataFrame[]; + customFrames: DataFrame[]; nodeGraphFrames: DataFrame[]; rawPrometheusFrames: DataFrame[]; flameGraphFrames: DataFrame[];