mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 04:22:13 +08:00
Sidebar: Create a sidebar that can render an extension (#102626)
* Extension Sidebar: Add missing `web-section` icon * Extension Sidebar: Add core extension sidebar components * Extension Sidebar: Integrate extension sidebar into Grafana * Extension Sidebar: Change extension point to alpha * Extension Sidebar: Fix saved state of docked extensions * Extension Sidebar: Delete from local storage if undocked * Extension Sidebar: Move main scrollbar from body to pane * Extension Sidebar: Expose `ExtensionInfo` * Extension Sidebar: Move `useComponents` into ExtensionSidebar * Extension Sidebar: Store selection in `localStorage` * Extension Sidebar: Simplify return of extension point meta * Extension Sidebar: Ensure `body` is scrollable when sidebar is closed * Extension Sidebar: Add missing `GlobalStyles` change * Extension Sidebar: Don't render `ExtensionSidebar` unless it should be open * Extension Sidebar: Better toggle handling * Extension Sidebar: Fix wrong header height * Extension Sidebar: Change `getExtensionPointPluginMeta` to use `addedComponents` and add documentation * Extension Sidebar: Add tests for `getExtensionPointPluginMeta` * Extension Sidebar: Add tests for `ExtensionSidebarProvider` * Extension Sidebar: Fix tests `ExtensionSidebarProvider` * Extension Sidebar: Add tests `ExtensionToolbarItem` * Extension Sidebar: Add `extensionSidebar` feature toggle * Extension Sidebar: Put implementation behind `extensionSidebar` feature toggle * update feature toggles * fix lint * Extension Sidebar: Only toggle if clicking the same button * Extension Sidebar: Hide sidebar if chromeless * Update feature toggles doc * Sidebar: Add `isEnabled` to ExtensionSidebarProvider * Extension Sidebar: Use conditional CSS classes * Extension Sidebar: Move header height to GrafanaContext * Sidebar: Adapt to feature toggle change * Sidebar: Remove unused import * Sidebar: Keep featuretoggles in ExtensionSidebar tests * ProviderConfig: Keep `config` import in tests * FeatureToggles: adapt to docs review * fix typo
This commit is contained in:
@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import { type Unsubscribable } from 'rxjs';
|
||||
|
||||
import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, AppPluginConfig } from '@grafana/runtime';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
getAppPluginConfigs,
|
||||
getAppPluginIdFromExposedComponentId,
|
||||
getAppPluginDependencies,
|
||||
getExtensionPointPluginMeta,
|
||||
} from './utils';
|
||||
|
||||
jest.mock('app/features/plugins/pluginSettings', () => ({
|
||||
@ -982,4 +983,121 @@ describe('Plugin Extensions / Utils', () => {
|
||||
expect(appPluginIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExtensionPointPluginMeta()', () => {
|
||||
const originalApps = config.apps;
|
||||
const mockExtensionPointId = 'test-extension-point';
|
||||
const mockApp1: AppPluginConfig = {
|
||||
id: 'app1',
|
||||
path: 'app1',
|
||||
version: '1.0.0',
|
||||
preload: false,
|
||||
angular: { detected: false, hideDeprecation: false },
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedComponents: [
|
||||
{ title: 'Component 1', targets: [mockExtensionPointId] },
|
||||
{ title: 'Component 2', targets: ['other-point'] },
|
||||
],
|
||||
addedLinks: [
|
||||
{ title: 'Link 1', targets: [mockExtensionPointId] },
|
||||
{ title: 'Link 2', targets: ['other-point'] },
|
||||
],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
|
||||
const mockApp2: AppPluginConfig = {
|
||||
id: 'app2',
|
||||
path: 'app2',
|
||||
version: '1.0.0',
|
||||
preload: false,
|
||||
angular: { detected: false, hideDeprecation: false },
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedComponents: [{ title: 'Component 3', targets: [mockExtensionPointId] }],
|
||||
addedLinks: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.apps = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return empty map when no plugins have extensions for the point', () => {
|
||||
config.apps = {
|
||||
app1: { ...mockApp1, extensions: { ...mockApp1.extensions, addedComponents: [], addedLinks: [] } },
|
||||
app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } },
|
||||
};
|
||||
|
||||
const result = getExtensionPointPluginMeta(mockExtensionPointId);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should return map with plugins that have components for the extension point', () => {
|
||||
config.apps = {
|
||||
app1: mockApp1,
|
||||
app2: mockApp2,
|
||||
};
|
||||
|
||||
const result = getExtensionPointPluginMeta(mockExtensionPointId);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get('app1')).toEqual({
|
||||
addedComponents: [{ title: 'Component 1', targets: [mockExtensionPointId] }],
|
||||
addedLinks: [{ title: 'Link 1', targets: [mockExtensionPointId] }],
|
||||
});
|
||||
expect(result.get('app2')).toEqual({
|
||||
addedComponents: [{ title: 'Component 3', targets: [mockExtensionPointId] }],
|
||||
addedLinks: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out plugins that do not have any extensions for the point', () => {
|
||||
config.apps = {
|
||||
app1: mockApp1,
|
||||
app2: { ...mockApp2, extensions: { ...mockApp2.extensions, addedComponents: [], addedLinks: [] } },
|
||||
app3: {
|
||||
...mockApp1,
|
||||
id: 'app3',
|
||||
extensions: {
|
||||
...mockApp1.extensions,
|
||||
addedComponents: [{ title: 'Component 4', targets: ['other-point'] }],
|
||||
addedLinks: [{ title: 'Link 3', targets: ['other-point'] }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getExtensionPointPluginMeta(mockExtensionPointId);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get('app1')).toEqual({
|
||||
addedComponents: [{ title: 'Component 1', targets: [mockExtensionPointId] }],
|
||||
addedLinks: [{ title: 'Link 1', targets: [mockExtensionPointId] }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
PluginExtensionAddedLinkConfig,
|
||||
urlUtil,
|
||||
PluginExtensionPoints,
|
||||
ExtensionInfo,
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime';
|
||||
import { Modal } from '@grafana/ui';
|
||||
@ -446,6 +447,46 @@ export const getExtensionPointPluginDependencies = (extensionPointId: string): s
|
||||
}, []);
|
||||
};
|
||||
|
||||
export type ExtensionPointPluginMeta = Map<
|
||||
string,
|
||||
{
|
||||
readonly addedComponents: ExtensionInfo[];
|
||||
readonly addedLinks: ExtensionInfo[];
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Returns a map of plugin ids and their addedComponents and addedLinks to the extension point.
|
||||
* @param extensionPointId - The id of the extension point.
|
||||
* @returns A map of plugin ids and their addedComponents and addedLinks to the extension point.
|
||||
*/
|
||||
export const getExtensionPointPluginMeta = (extensionPointId: string): ExtensionPointPluginMeta => {
|
||||
return new Map(
|
||||
getExtensionPointPluginDependencies(extensionPointId)
|
||||
.map((pluginId) => {
|
||||
const app = config.apps[pluginId];
|
||||
// if the plugin does not exist or does not expose any components or links to the extension point, return undefined
|
||||
if (
|
||||
!app ||
|
||||
(!app.extensions.addedComponents.some((component) => component.targets.includes(extensionPointId)) &&
|
||||
!app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return [
|
||||
pluginId,
|
||||
{
|
||||
addedComponents: app.extensions.addedComponents.filter((component) =>
|
||||
component.targets.includes(extensionPointId)
|
||||
),
|
||||
addedLinks: app.extensions.addedLinks.filter((link) => link.targets.includes(extensionPointId)),
|
||||
},
|
||||
] as const;
|
||||
})
|
||||
.filter((c): c is NonNullable<typeof c> => c !== undefined)
|
||||
);
|
||||
};
|
||||
|
||||
// Returns a list of app plugin ids that are necessary to be loaded to use the exposed component.
|
||||
// (It is first the plugin that exposes the component, and then the ones that it depends on.)
|
||||
export const getExposedComponentPluginDependencies = (exposedComponentId: string) => {
|
||||
|
Reference in New Issue
Block a user