mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 22:42:18 +08:00

* refactor(frontend): rename all @grafana/data/src imports to @grafana/data * feat(grafana-data): introduce internal entrypoint for sharing code only with grafana * feat(grafana-data): add test entrypoint for data test utils usage in core * refactor(frontend): update import paths to use grafana/data exports entrypoints * docs(grafana-data): update comment in internal/index.ts * refactor(frontend): prefer public namespaced exports over re-exporting via internal * chore(frontend): fix a couple more weird paths that typescript complains about
259 lines
8.6 KiB
TypeScript
259 lines
8.6 KiB
TypeScript
import {
|
|
type PluginExtensionAddedLinkConfig,
|
|
type PluginExtension,
|
|
type PluginExtensionLink,
|
|
type PluginContextType,
|
|
type PluginExtensionAddedComponentConfig,
|
|
type PluginExtensionExposedComponentConfig,
|
|
type PluginExtensionAddedFunctionConfig,
|
|
PluginExtensionPoints,
|
|
} from '@grafana/data';
|
|
import { PluginAddedLinksConfigureFunc } from '@grafana/data/internal';
|
|
import { config, isPluginExtensionLink } from '@grafana/runtime';
|
|
|
|
import * as errors from './errors';
|
|
import { ExtensionsLog } from './logs/log';
|
|
|
|
export function assertPluginExtensionLink(
|
|
extension: PluginExtension | undefined,
|
|
errorMessage = 'extension is not a link extension'
|
|
): asserts extension is PluginExtensionLink {
|
|
if (!isPluginExtensionLink(extension)) {
|
|
throw new Error(errorMessage);
|
|
}
|
|
}
|
|
|
|
export function assertLinkPathIsValid(pluginId: string, path: string) {
|
|
if (!isLinkPathValid(pluginId, path)) {
|
|
throw new Error(
|
|
`Invalid link extension. The "path" is required and should start with "/a/${pluginId}/" (currently: "${path}"). Skipping the extension.`
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertIsReactComponent(component: React.ComponentType) {
|
|
if (!isReactComponent(component)) {
|
|
throw new Error(`Invalid component extension, the "component" property needs to be a valid React component.`);
|
|
}
|
|
}
|
|
|
|
export function assertConfigureIsValid(config: PluginExtensionAddedLinkConfig) {
|
|
if (!isConfigureFnValid(config.configure)) {
|
|
throw new Error(
|
|
`Invalid extension "${config.title}". The "configure" property must be a function. Skipping the extension.`
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertStringProps(extension: Record<string, unknown>, props: string[]) {
|
|
for (const prop of props) {
|
|
if (!isStringPropValid(extension[prop])) {
|
|
throw new Error(
|
|
`Invalid extension "${extension.title}". Property "${prop}" must be a string and cannot be empty. Skipping the extension.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function assertIsNotPromise(value: unknown, errorMessage = 'The provided value is a Promise.'): void {
|
|
if (isPromise(value)) {
|
|
throw new Error(errorMessage);
|
|
}
|
|
}
|
|
|
|
export function isLinkPathValid(pluginId: string, path: string) {
|
|
return Boolean(typeof path === 'string' && path.length > 0 && path.startsWith(`/a/${pluginId}/`));
|
|
}
|
|
|
|
export function isExtensionPointIdValid({
|
|
extensionPointId,
|
|
pluginId,
|
|
}: {
|
|
extensionPointId: string;
|
|
pluginId: string;
|
|
}) {
|
|
if (extensionPointId.startsWith('grafana/')) {
|
|
return true;
|
|
}
|
|
|
|
return Boolean(extensionPointId.startsWith(`plugins/${pluginId}/`) || extensionPointId.startsWith(`${pluginId}/`));
|
|
}
|
|
|
|
export function extensionPointEndsWithVersion(extensionPointId: string) {
|
|
return extensionPointId.match(/.*\/v\d+$/);
|
|
}
|
|
|
|
export function isGrafanaCoreExtensionPoint(extensionPointId: string) {
|
|
return Object.values(PluginExtensionPoints)
|
|
.map((v) => v.toString())
|
|
.includes(extensionPointId);
|
|
}
|
|
|
|
export function isConfigureFnValid(configure?: PluginAddedLinksConfigureFunc<object> | undefined) {
|
|
return configure ? typeof configure === 'function' : true;
|
|
}
|
|
|
|
export function isStringPropValid(prop: unknown) {
|
|
return typeof prop === 'string' && prop.length > 0;
|
|
}
|
|
|
|
export function isPromise(value: unknown): value is Promise<unknown> {
|
|
return (
|
|
value instanceof Promise || (typeof value === 'object' && value !== null && 'then' in value && 'catch' in value)
|
|
);
|
|
}
|
|
|
|
export function isReactComponent(component: unknown): component is React.ComponentType {
|
|
const hasReactTypeProp = (obj: unknown): obj is { $$typeof: Symbol } =>
|
|
typeof obj === 'object' && obj !== null && '$$typeof' in obj;
|
|
|
|
// The sandbox wraps the plugin components with React.memo.
|
|
const isReactMemoObject = (obj: unknown): boolean =>
|
|
hasReactTypeProp(obj) && obj.$$typeof === Symbol.for('react.memo');
|
|
|
|
// We currently don't have any strict runtime-checking for this.
|
|
// (The main reason is that we don't want to start depending on React implementation details.)
|
|
return typeof component === 'function' || isReactMemoObject(component);
|
|
}
|
|
|
|
// Checks if the meta information is missing from the plugin's plugin.json file
|
|
export const isExtensionPointMetaInfoMissing = (extensionPointId: string, pluginContext: PluginContextType) => {
|
|
const extensionPoints = pluginContext.meta?.extensions?.extensionPoints;
|
|
|
|
return !extensionPoints || !extensionPoints.some((ep) => ep.id === extensionPointId);
|
|
};
|
|
|
|
// Checks if an exposed component that the plugin is depending on is missing from the `dependencies` in the plugin.json file
|
|
export const isExposedComponentDependencyMissing = (id: string, pluginContext: PluginContextType) => {
|
|
const exposedComponentsDependencies = pluginContext.meta?.dependencies?.extensions?.exposedComponents;
|
|
|
|
return !exposedComponentsDependencies || !exposedComponentsDependencies.includes(id);
|
|
};
|
|
|
|
export const isAddedLinkMetaInfoMissing = (
|
|
pluginId: string,
|
|
metaInfo: PluginExtensionAddedLinkConfig,
|
|
log: ExtensionsLog
|
|
) => {
|
|
const logPrefix = 'Could not register link extension. Reason:';
|
|
const app = config.apps[pluginId];
|
|
const pluginJsonMetaInfo = app ? app.extensions.addedLinks.find(({ title }) => title === metaInfo.title) : null;
|
|
|
|
if (!app) {
|
|
log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`);
|
|
return true;
|
|
}
|
|
|
|
if (!pluginJsonMetaInfo) {
|
|
log.error(`${logPrefix} ${errors.ADDED_LINK_META_INFO_MISSING}`);
|
|
return true;
|
|
}
|
|
|
|
const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets];
|
|
if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) {
|
|
log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`);
|
|
return true;
|
|
}
|
|
|
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
|
log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
export const isAddedFunctionMetaInfoMissing = (
|
|
pluginId: string,
|
|
metaInfo: PluginExtensionAddedFunctionConfig,
|
|
log: ExtensionsLog
|
|
) => {
|
|
const logPrefix = 'Could not register function extension. Reason:';
|
|
const app = config.apps[pluginId];
|
|
const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.find(({ title }) => title === metaInfo.title) : null;
|
|
|
|
if (!app) {
|
|
log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`);
|
|
return true;
|
|
}
|
|
|
|
if (!pluginJsonMetaInfo) {
|
|
log.error(`${logPrefix} ${errors.ADDED_FUNCTION_META_INFO_MISSING}`);
|
|
return true;
|
|
}
|
|
|
|
const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets];
|
|
if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) {
|
|
log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`);
|
|
return true;
|
|
}
|
|
|
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
|
log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
export const isAddedComponentMetaInfoMissing = (
|
|
pluginId: string,
|
|
metaInfo: PluginExtensionAddedComponentConfig,
|
|
log: ExtensionsLog
|
|
) => {
|
|
const logPrefix = 'Could not register component extension. Reason:';
|
|
const app = config.apps[pluginId];
|
|
const pluginJsonMetaInfo = app ? app.extensions.addedComponents.find(({ title }) => title === metaInfo.title) : null;
|
|
|
|
if (!app) {
|
|
log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`);
|
|
return true;
|
|
}
|
|
|
|
if (!pluginJsonMetaInfo) {
|
|
log.error(`${logPrefix} ${errors.ADDED_COMPONENT_META_INFO_MISSING}`);
|
|
return true;
|
|
}
|
|
|
|
const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets];
|
|
if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) {
|
|
log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`);
|
|
return true;
|
|
}
|
|
|
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
|
log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
export const isExposedComponentMetaInfoMissing = (
|
|
pluginId: string,
|
|
metaInfo: PluginExtensionExposedComponentConfig,
|
|
log: ExtensionsLog
|
|
) => {
|
|
const logPrefix = 'Could not register exposed component extension. Reason:';
|
|
const app = config.apps[pluginId];
|
|
const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.find(({ id }) => id === metaInfo.id) : null;
|
|
|
|
if (!app) {
|
|
log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`);
|
|
return true;
|
|
}
|
|
|
|
if (!pluginJsonMetaInfo) {
|
|
log.error(`${logPrefix} ${errors.EXPOSED_COMPONENT_META_INFO_MISSING}`);
|
|
return true;
|
|
}
|
|
|
|
if (pluginJsonMetaInfo.title !== metaInfo.title) {
|
|
log.error(`${logPrefix} ${errors.TITLE_NOT_MATCHING_META_INFO}`);
|
|
return true;
|
|
}
|
|
|
|
if (pluginJsonMetaInfo.description !== metaInfo.description) {
|
|
log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO);
|
|
}
|
|
|
|
return false;
|
|
};
|