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[];