feat: component extension point for adaptive telemetry query actions (#101331)
* feat: component extension point for adaptive telemetry query actions - only render the first non-null added-component, and provide utility in the added component infrastructure to support this --------- Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
co-authored by
Levente Balogh
parent
a7a9f9b078
commit
e948376a95
@@ -557,6 +557,7 @@ export {
|
||||
type PluginExtensionComponentConfig,
|
||||
type PluginExtensionEventHelpers,
|
||||
type PluginExtensionPanelContext,
|
||||
type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context,
|
||||
type PluginExtensionDataSourceConfigContext,
|
||||
type PluginExtensionCommandPaletteContext,
|
||||
type PluginExtensionOpenModalOptions,
|
||||
|
||||
@@ -186,6 +186,7 @@ export enum PluginExtensionPoints {
|
||||
ExploreToolbarAction = 'grafana/explore/toolbar/action',
|
||||
UserProfileTab = 'grafana/user/profile/tab',
|
||||
TraceViewDetails = 'grafana/traceview/details',
|
||||
QueryEditorRowAdaptiveTelemetryV1 = 'grafana/query-editor-row/adaptivetelemetry/v1',
|
||||
}
|
||||
|
||||
export type PluginExtensionPanelContext = {
|
||||
@@ -200,6 +201,12 @@ export type PluginExtensionPanelContext = {
|
||||
data?: PanelData;
|
||||
};
|
||||
|
||||
export type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context = {
|
||||
/** An ordered list of lower-case [a-z]+ string identifiers to provide context clues of where this component is being embedded and how we might want to consider displaying it */
|
||||
contextHints?: string[];
|
||||
query?: DataQuery;
|
||||
};
|
||||
|
||||
export type PluginExtensionDataSourceConfigContext<
|
||||
JsonData extends DataSourceJsonData = DataSourceJsonData,
|
||||
SecureJsonData = {},
|
||||
|
||||
@@ -53,7 +53,12 @@ export {
|
||||
type UsePluginFunctionsResult,
|
||||
} from './pluginExtensions/usePluginFunctions';
|
||||
|
||||
export { isPluginExtensionLink, isPluginExtensionComponent } from './pluginExtensions/utils';
|
||||
export {
|
||||
isPluginExtensionLink,
|
||||
isPluginExtensionComponent,
|
||||
getLimitedComponentsToRender,
|
||||
renderLimitedComponents,
|
||||
} from './pluginExtensions/utils';
|
||||
export { setCurrentUser } from './user';
|
||||
export { RuntimeDataSource } from './RuntimeDataSource';
|
||||
export { ScopesContext, type ScopesContextValueState, type ScopesContextValue, useScopes } from './ScopesContext';
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { PluginExtension, PluginExtensionTypes } from '@grafana/data';
|
||||
|
||||
import { isPluginExtensionLink } from './utils';
|
||||
|
||||
describe('Plugin Extensions / Utils', () => {
|
||||
describe('isPluginExtensionLink()', () => {
|
||||
test('should return TRUE if the object is a link extension', () => {
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
id: 'id',
|
||||
pluginId: 'plugin-id',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
path: '...',
|
||||
} as PluginExtension)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
id: 'id',
|
||||
pluginId: 'plugin-id',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
onClick: () => {},
|
||||
} as PluginExtension)
|
||||
).toBe(true);
|
||||
});
|
||||
test('should return FALSE if the object is NOT a link extension', () => {
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
} as PluginExtension)
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
// @ts-ignore (Right now we only have a single type of extension)
|
||||
isPluginExtensionLink({
|
||||
type: 'unknown',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
path: '...',
|
||||
} as PluginExtension)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
ComponentTypeWithExtensionMeta,
|
||||
PluginExtension,
|
||||
PluginExtensionComponentMeta,
|
||||
PluginExtensionTypes,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { getLimitedComponentsToRender, isPluginExtensionLink, renderLimitedComponents } from './utils';
|
||||
|
||||
describe('Plugin Extensions / Utils', () => {
|
||||
describe('isPluginExtensionLink()', () => {
|
||||
test('should return TRUE if the object is a link extension', () => {
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
id: 'id',
|
||||
pluginId: 'plugin-id',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
path: '...',
|
||||
} as PluginExtension)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
id: 'id',
|
||||
pluginId: 'plugin-id',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
onClick: () => {},
|
||||
} as PluginExtension)
|
||||
).toBe(true);
|
||||
});
|
||||
test('should return FALSE if the object is NOT a link extension', () => {
|
||||
expect(
|
||||
isPluginExtensionLink({
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
} as PluginExtension)
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
// @ts-ignore (Right now we only have a single type of extension)
|
||||
isPluginExtensionLink({
|
||||
type: 'unknown',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
path: '...',
|
||||
} as PluginExtension)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLimitedComponentsToRender()', () => {
|
||||
test('should return `null` if it receives an empty array of components', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [];
|
||||
const limitedComponents = getLimitedComponentsToRender({ props, components });
|
||||
expect(limitedComponents).toEqual(null);
|
||||
});
|
||||
|
||||
test('should return all components if no limit is provided', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(() => <div>Test 1</div>, undefined, 'id-1'),
|
||||
createComponent(() => <div>Test 2</div>, undefined, 'id-2'),
|
||||
createComponent(() => <div>Test 3</div>, undefined, 'id-3'),
|
||||
];
|
||||
|
||||
expect(getLimitedComponentsToRender({ props, components })?.length).toEqual(3);
|
||||
});
|
||||
|
||||
test('should limit the number of components', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(() => <div>Test 1</div>, undefined, 'id-1'),
|
||||
createComponent(() => <div>Test 2</div>, undefined, 'id-2'),
|
||||
createComponent(() => <div>Test 3</div>, undefined, 'id-3'),
|
||||
createComponent(() => <div>Test 4</div>, undefined, 'id-4'),
|
||||
createComponent(() => <div>Test 5</div>, undefined, 'id-5'),
|
||||
];
|
||||
|
||||
// Check if the limit is respected
|
||||
expect(getLimitedComponentsToRender({ props, components, limit: 1 })?.length).toEqual(1);
|
||||
expect(getLimitedComponentsToRender({ props, components, limit: 3 })?.length).toEqual(3);
|
||||
|
||||
// Check if the right components are selected
|
||||
const limitedComponents = getLimitedComponentsToRender({ props, components, limit: 3 });
|
||||
const rendered = render(
|
||||
<>{limitedComponents?.map((Component, index) => <Component key={index} {...props} />)}</>
|
||||
);
|
||||
|
||||
expect(rendered.getByText('Test 1')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Test 2')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Test 3')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 4')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 5')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should work when using class components', () => {
|
||||
const props = {};
|
||||
const Component1 = class extends React.Component<{}> {
|
||||
render() {
|
||||
return <div>Test 1</div>;
|
||||
}
|
||||
};
|
||||
const Component2 = class extends React.Component<{}> {
|
||||
render() {
|
||||
return <div>Test 2</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(Component1, undefined, 'id-1'),
|
||||
createComponent(Component2, undefined, 'id-2'),
|
||||
];
|
||||
|
||||
// Check if the limit is respected
|
||||
expect(getLimitedComponentsToRender({ props, components, limit: 1 })?.length).toEqual(1);
|
||||
expect(getLimitedComponentsToRender({ props, components, limit: 2 })?.length).toEqual(2);
|
||||
|
||||
// Check if the right components are selected
|
||||
const limitedComponents = getLimitedComponentsToRender({ props, components, limit: 1 });
|
||||
const rendered = render(
|
||||
<>{limitedComponents?.map((Component, index) => <Component key={index} {...props} />)}</>
|
||||
);
|
||||
|
||||
expect(rendered.getByText('Test 1')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should filter components by plugin id', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(() => <div>Test 1</div>, 'plugin-id-1', 'id-1'),
|
||||
createComponent(() => <div>Test 2</div>, 'plugin-id-2', 'id-2'),
|
||||
createComponent(() => <div>Test 3</div>, 'plugin-id-3', 'id-3'),
|
||||
createComponent(() => <div>Test 4</div>, 'plugin-id-4', 'id-4'),
|
||||
createComponent(() => <div>Test 5</div>, 'plugin-id-5', 'id-5'),
|
||||
];
|
||||
|
||||
// Check if the filtering works
|
||||
expect(getLimitedComponentsToRender({ props, components, pluginId: 'plugin-id-1' })?.length).toEqual(1);
|
||||
expect(
|
||||
getLimitedComponentsToRender({ props, components, pluginId: ['plugin-id-1', 'plugin-id-2'] })?.length
|
||||
).toEqual(2);
|
||||
expect(getLimitedComponentsToRender({ props, components, pluginId: /plugin-id.*/ })?.length).toEqual(5);
|
||||
|
||||
// Check if the right components are selected
|
||||
const limitedComponents = getLimitedComponentsToRender({
|
||||
props,
|
||||
components,
|
||||
pluginId: ['plugin-id-2', 'plugin-id-3'],
|
||||
});
|
||||
const rendered = render(
|
||||
<>{limitedComponents?.map((Component, index) => <Component key={index} {...props} />)}</>
|
||||
);
|
||||
|
||||
expect(rendered.getByText('Test 2')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Test 3')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 1')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 4')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 5')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should filter components based on both limit and plugin id', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(() => <div>Test 1</div>, 'plugin-id-1', 'id-1'),
|
||||
createComponent(() => <div>Test 2</div>, 'plugin-id-2', 'id-2'),
|
||||
createComponent(() => <div>Test 3</div>, 'plugin-id-3', 'id-3'),
|
||||
createComponent(() => <div>Test 4</div>, 'plugin-id-4', 'id-4'),
|
||||
createComponent(() => <div>Test 5</div>, 'plugin-id-5', 'id-5'),
|
||||
];
|
||||
|
||||
// Check if the filtering works
|
||||
expect(getLimitedComponentsToRender({ props, components, limit: 1, pluginId: /plugin-id.*/ })?.length).toEqual(1);
|
||||
expect(getLimitedComponentsToRender({ props, components, limit: 2, pluginId: 'plugin-id-3' })?.length).toEqual(1);
|
||||
expect(
|
||||
getLimitedComponentsToRender({
|
||||
props,
|
||||
components,
|
||||
limit: 1,
|
||||
pluginId: ['plugin-id-1', 'plugin-id-2', 'plugin-id-3'],
|
||||
})?.length
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderLimitedComponents()', () => {
|
||||
test('should render all components if no limit is provided', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(() => <div>Test 1</div>, 'plugin-id-1', 'id-1'),
|
||||
createComponent(() => <div>Test 2</div>, 'plugin-id-1', 'id-2'),
|
||||
createComponent(() => <div>Test 3</div>, 'plugin-id-2', 'id-3'),
|
||||
createComponent(() => <div>Test 4</div>, 'plugin-id-3', 'id-4'),
|
||||
];
|
||||
|
||||
const rendered = render(<>{renderLimitedComponents({ props, components })}</>);
|
||||
|
||||
expect(rendered.getByText('Test 1')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Test 2')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Test 3')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Test 4')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should limit the number of components', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(() => <div>Test 1</div>, 'plugin-id-1', 'id-1'),
|
||||
createComponent(() => <div>Test 2</div>, 'plugin-id-2', 'id-2'),
|
||||
createComponent(() => <div>Test 3</div>, 'plugin-id-3', 'id-3'),
|
||||
];
|
||||
|
||||
const rendered = render(<>{renderLimitedComponents({ props, components, limit: 1 })}</>);
|
||||
|
||||
expect(rendered.getByText('Test 1')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 2')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should filter components by plugin id', () => {
|
||||
const props = {};
|
||||
const components: Array<ComponentTypeWithExtensionMeta<{}>> = [
|
||||
createComponent(() => <div>Test 1</div>, 'plugin-id-1', 'id-1'),
|
||||
createComponent(() => <div>Test 2</div>, 'plugin-id-2', 'id-2'),
|
||||
createComponent(() => <div>Test 3</div>, 'plugin-id-3', 'id-3'),
|
||||
];
|
||||
|
||||
const rendered = render(<>{renderLimitedComponents({ props, components, pluginId: ['plugin-id-2'] })}</>);
|
||||
expect(rendered.getByText('Test 2')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 1')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('Test 3')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createComponent<Props extends JSX.IntrinsicAttributes>(
|
||||
Implementation?: React.ComponentType<Props>,
|
||||
pluginId?: string,
|
||||
id?: string
|
||||
): ComponentTypeWithExtensionMeta<Props> {
|
||||
function ComponentWithMeta(props: Props) {
|
||||
if (Implementation) {
|
||||
return <Implementation {...props} />;
|
||||
}
|
||||
|
||||
return <div>Test</div>;
|
||||
}
|
||||
|
||||
ComponentWithMeta.displayName = '';
|
||||
ComponentWithMeta.propTypes = {};
|
||||
ComponentWithMeta.contextTypes = {};
|
||||
ComponentWithMeta.meta = {
|
||||
id: id ?? '',
|
||||
pluginId: pluginId ?? '',
|
||||
title: '',
|
||||
description: '',
|
||||
type: PluginExtensionTypes.component,
|
||||
} satisfies PluginExtensionComponentMeta;
|
||||
|
||||
return ComponentWithMeta;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import {
|
||||
type PluginExtension,
|
||||
type PluginExtensionComponent,
|
||||
type PluginExtensionLink,
|
||||
PluginExtensionTypes,
|
||||
} from '@grafana/data';
|
||||
|
||||
export function isPluginExtensionLink(extension: PluginExtension | undefined): extension is PluginExtensionLink {
|
||||
if (!extension) {
|
||||
return false;
|
||||
}
|
||||
return extension.type === PluginExtensionTypes.link && ('path' in extension || 'onClick' in extension);
|
||||
}
|
||||
|
||||
export function isPluginExtensionComponent(
|
||||
extension: PluginExtension | undefined
|
||||
): extension is PluginExtensionComponent {
|
||||
if (!extension) {
|
||||
return false;
|
||||
}
|
||||
return extension.type === PluginExtensionTypes.component && 'component' in extension;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
ComponentTypeWithExtensionMeta,
|
||||
type PluginExtension,
|
||||
type PluginExtensionComponent,
|
||||
type PluginExtensionLink,
|
||||
PluginExtensionTypes,
|
||||
} from '@grafana/data';
|
||||
|
||||
export function isPluginExtensionLink(extension: PluginExtension | undefined): extension is PluginExtensionLink {
|
||||
if (!extension) {
|
||||
return false;
|
||||
}
|
||||
return extension.type === PluginExtensionTypes.link && ('path' in extension || 'onClick' in extension);
|
||||
}
|
||||
|
||||
export function isPluginExtensionComponent(
|
||||
extension: PluginExtension | undefined
|
||||
): extension is PluginExtensionComponent {
|
||||
if (!extension) {
|
||||
return false;
|
||||
}
|
||||
return extension.type === PluginExtensionTypes.component && 'component' in extension;
|
||||
}
|
||||
|
||||
export function getLimitedComponentsToRender<Props extends {}>({
|
||||
props,
|
||||
components,
|
||||
limit,
|
||||
pluginId,
|
||||
}: {
|
||||
props: Props;
|
||||
components: Array<ComponentTypeWithExtensionMeta<Props>>;
|
||||
limit?: number;
|
||||
pluginId?: string | string[] | RegExp;
|
||||
}) {
|
||||
if (!components.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderedComponents: Array<ComponentTypeWithExtensionMeta<Props>> = [];
|
||||
|
||||
for (const Component of components) {
|
||||
const { meta } = Component;
|
||||
|
||||
if (pluginId && typeof pluginId === 'string' && pluginId !== meta.pluginId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pluginId && Array.isArray(pluginId) && !pluginId.includes(meta.pluginId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pluginId instanceof RegExp && !pluginId.test(meta.pluginId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If no limit is provided, return all components
|
||||
if (limit === undefined) {
|
||||
renderedComponents.push(Component);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If a component does not render anything, do not count it in the limit
|
||||
if (React.createElement<Props>(Component, props) !== null) {
|
||||
renderedComponents.push(Component);
|
||||
}
|
||||
|
||||
// Stop if we've reached the limit
|
||||
if (renderedComponents.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return renderedComponents;
|
||||
}
|
||||
|
||||
export function renderLimitedComponents<Props extends {}>({
|
||||
props,
|
||||
components,
|
||||
limit,
|
||||
pluginId,
|
||||
}: {
|
||||
props: Props;
|
||||
components: Array<ComponentTypeWithExtensionMeta<Props>>;
|
||||
limit?: number;
|
||||
pluginId?: string | string[] | RegExp;
|
||||
}) {
|
||||
const limitedComponents = getLimitedComponentsToRender({ props, components, limit, pluginId });
|
||||
|
||||
if (!limitedComponents?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{limitedComponents.map((Component) => (
|
||||
<Component key={Component.meta.id} {...props} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourcePluginContextProvider,
|
||||
PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context,
|
||||
EventBusExtended,
|
||||
EventBusSrv,
|
||||
HistoryItem,
|
||||
@@ -21,9 +22,17 @@ import {
|
||||
TimeRange,
|
||||
getDataSourceRef,
|
||||
toLegacyResponseData,
|
||||
PluginExtensionPoints,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { AngularComponent, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
|
||||
import {
|
||||
AngularComponent,
|
||||
getAngularLoader,
|
||||
getDataSourceSrv,
|
||||
renderLimitedComponents,
|
||||
reportInteraction,
|
||||
usePluginComponents,
|
||||
} from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Badge, ErrorBoundaryAlert } from '@grafana/ui';
|
||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||
@@ -458,6 +467,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
|
||||
extraActions.push(this.renderWarnings('info'));
|
||||
extraActions.push(this.renderWarnings('warning'));
|
||||
extraActions.push(<AdaptiveTelemetryQueryActions key="adaptive-telemetry-actions" query={query} />);
|
||||
|
||||
return extraActions;
|
||||
};
|
||||
@@ -664,3 +674,27 @@ function MaybeQueryLibrarySaveButton(props: { query: DataQuery }) {
|
||||
const { renderSaveQueryButton } = useQueryLibraryContext();
|
||||
return renderSaveQueryButton(props.query);
|
||||
}
|
||||
|
||||
function AdaptiveTelemetryQueryActions({ query }: { query: DataQuery }) {
|
||||
try {
|
||||
const { isLoading, components } = usePluginComponents<PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context>({
|
||||
extensionPointId: PluginExtensionPoints.QueryEditorRowAdaptiveTelemetryV1,
|
||||
});
|
||||
|
||||
if (isLoading || !components.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return renderLimitedComponents({
|
||||
props: { query, contextHints: ['queryeditorrow', 'header'] },
|
||||
components,
|
||||
limit: 1,
|
||||
pluginId: /grafana-adaptive.*/,
|
||||
});
|
||||
} catch (error) {
|
||||
// If `usePluginComponents` isn't properly resolved, tests will fail with 'setPluginComponentsHook(options) can only be used after the Grafana instance has started.'
|
||||
// This will be resolved in https://github.com/grafana/grafana/pull/92983
|
||||
// In this case, Return `null` like when there are no extensions.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user