mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Allow the use of plugin panels (#66982)
* Explore: Allow the use of plugin panels Allow plugins to define a visualisation to use in explore that comes from a plugin. * Explore: Allow the use of plugin panels Rename ExplorePanel to CustomContainer * Explore: Allow the use of plugin panels Changed CustomContainer to take all frames for plugin id. Add field preferredVisualisationPluginId to frame metadata. Updated decorators to check for plugin and fallback to preferredVisualisationType if plugin cannot be found. * Explore: Allow the use of plugin panels Handle case where there are no custom frames * Explore: Allow the use of plugin panels Add test cases
This commit is contained in:
parent
b947222bf5
commit
524f111ab3
@ -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;
|
||||
|
||||
|
45
public/app/features/explore/CustomContainer.tsx
Normal file
45
public/app/features/explore/CustomContainer.tsx
Normal file
@ -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 (
|
||||
<PanelChrome title={plugin.name} width={width} height={height} loadingState={state}>
|
||||
{(innerWidth, innerHeight) => (
|
||||
<PanelRenderer
|
||||
data={{ series: frames, state: state, timeRange }}
|
||||
pluginId={pluginId}
|
||||
title=""
|
||||
width={innerWidth}
|
||||
height={innerHeight}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
)}
|
||||
</PanelChrome>
|
||||
);
|
||||
}
|
@ -86,6 +86,7 @@ const dummyProps: Props = {
|
||||
showLogs: true,
|
||||
showTable: true,
|
||||
showTrace: true,
|
||||
showCustom: true,
|
||||
showNodeGraph: true,
|
||||
showFlameGraph: true,
|
||||
splitOpen: jest.fn(),
|
||||
|
@ -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<Props, ExploreState> {
|
||||
return <NoData />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<CustomContainer
|
||||
key={index}
|
||||
timeZone={timeZone}
|
||||
pluginId={pluginId}
|
||||
frames={frames}
|
||||
state={queryResponse.state}
|
||||
absoluteRange={absoluteRange}
|
||||
height={400}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderGraphPanel(width: number) {
|
||||
const { graphResult, absoluteRange, timeZone, queryResponse, showFlameGraph } = this.props;
|
||||
|
||||
@ -423,6 +445,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
showRawPrometheus,
|
||||
showLogs,
|
||||
showTrace,
|
||||
showCustom,
|
||||
showNodeGraph,
|
||||
showFlameGraph,
|
||||
timeZone,
|
||||
@ -444,6 +467,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
queryResponse.tableFrames,
|
||||
queryResponse.rawPrometheusFrames,
|
||||
queryResponse.traceFrames,
|
||||
queryResponse.customFrames,
|
||||
].every((e) => e.length === 0);
|
||||
|
||||
return (
|
||||
@ -496,6 +520,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
{config.featureToggles.logsSampleInExplore && showLogsSample && (
|
||||
<ErrorBoundaryAlert>{this.renderLogsSamplePanel()}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showCustom && <ErrorBoundaryAlert>{this.renderCustom(width)}</ErrorBoundaryAlert>}
|
||||
{showNoData && <ErrorBoundaryAlert>{this.renderNoData()}</ErrorBoundaryAlert>}
|
||||
</>
|
||||
)}
|
||||
@ -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,
|
||||
|
@ -48,6 +48,7 @@ const setup = (propOverrides = {}) => {
|
||||
logsFrames: [],
|
||||
tableFrames: [],
|
||||
traceFrames: [],
|
||||
customFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
rawPrometheusFrames: [],
|
||||
|
@ -13,6 +13,7 @@ export const mockExplorePanelData = (props?: MockProps): Observable<ExplorePanel
|
||||
flameGraphFrames: [],
|
||||
graphFrames: [],
|
||||
graphResult: [],
|
||||
customFrames: [],
|
||||
logsFrames: [],
|
||||
logsResult: {
|
||||
hasUniqueLabels: false,
|
||||
|
@ -137,6 +137,29 @@ describe('addPanelToDashboard', () => {
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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' })]),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -82,6 +82,7 @@ export const createEmptyQueryResponse = (): ExplorePanelData => ({
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
customFrames: [],
|
||||
tableFrames: [],
|
||||
rawPrometheusFrames: [],
|
||||
rawPrometheusResult: null,
|
||||
|
@ -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<ExplorePanelData>): 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]);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export function importPanelPlugin(id: string): Promise<PanelPlugin> {
|
||||
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<PanelPlugin> {
|
||||
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<PanelPlugin> {
|
||||
return getPanelPlugin(meta);
|
||||
}
|
||||
|
@ -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[];
|
||||
|
Loading…
Reference in New Issue
Block a user