mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 20:52:10 +08:00

* feat: add a new UI extension type: component * tests: add tests for checking if it is a react component * fix: remove reference to not existing type * chore: update betterer results * review: remove unnecessary override function for components * review: use a single type notation in import * review: stop exporting `PluginExtensionBase` * refactor: make extension config types more explicit By using some repetition now these types are much easier to oversee.
169 lines
5.3 KiB
TypeScript
169 lines
5.3 KiB
TypeScript
import { isArray, isObject } from 'lodash';
|
|
import React from 'react';
|
|
|
|
import {
|
|
type PluginExtensionLinkConfig,
|
|
type PluginExtensionComponentConfig,
|
|
type PluginExtensionConfig,
|
|
type PluginExtensionEventHelpers,
|
|
PluginExtensionTypes,
|
|
} from '@grafana/data';
|
|
import { Modal } from '@grafana/ui';
|
|
import appEvents from 'app/core/app_events';
|
|
import { ShowModalReactEvent } from 'app/types/events';
|
|
|
|
export function logWarning(message: string) {
|
|
console.warn(`[Plugin Extensions] ${message}`);
|
|
}
|
|
|
|
export function isPluginExtensionLinkConfig(
|
|
extension: PluginExtensionConfig | undefined
|
|
): extension is PluginExtensionLinkConfig {
|
|
return typeof extension === 'object' && 'type' in extension && extension['type'] === PluginExtensionTypes.link;
|
|
}
|
|
|
|
export function isPluginExtensionComponentConfig(
|
|
extension: PluginExtensionConfig | undefined
|
|
): extension is PluginExtensionComponentConfig {
|
|
return typeof extension === 'object' && 'type' in extension && extension['type'] === PluginExtensionTypes.component;
|
|
}
|
|
|
|
export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') {
|
|
return (...args: unknown[]) => {
|
|
try {
|
|
return fn(...args);
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
console.warn(`${errorMessagePrefix}${e.message}`);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Event helpers are designed to make it easier to trigger "core actions" from an extension event handler, e.g. opening a modal or showing a notification.
|
|
export function getEventHelpers(context?: Readonly<object>): PluginExtensionEventHelpers {
|
|
const openModal: PluginExtensionEventHelpers['openModal'] = ({ title, body }) => {
|
|
appEvents.publish(new ShowModalReactEvent({ component: getModalWrapper({ title, body }) }));
|
|
};
|
|
|
|
return { openModal, context };
|
|
}
|
|
|
|
export type ModalWrapperProps = {
|
|
onDismiss: () => void;
|
|
};
|
|
|
|
// Wraps a component with a modal.
|
|
// This way we can make sure that the modal is closable, and we also make the usage simpler.
|
|
export const getModalWrapper = ({
|
|
// The title of the modal (appears in the header)
|
|
title,
|
|
// A component that serves the body of the modal
|
|
body: Body,
|
|
}: Parameters<PluginExtensionEventHelpers['openModal']>[0]) => {
|
|
const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => {
|
|
return (
|
|
<Modal title={title} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
|
|
<Body onDismiss={onDismiss} />
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
return ModalWrapper;
|
|
};
|
|
|
|
// Deep-clones and deep-freezes an object.
|
|
// (Returns with a new object, does not modify the original object)
|
|
//
|
|
// @param `object` The object to freeze
|
|
// @param `frozenProps` A set of objects that have already been frozen (used to prevent infinite recursion)
|
|
export function deepFreeze(value?: object | Record<string | symbol, unknown> | unknown[], frozenProps = new Map()) {
|
|
if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
|
|
return value;
|
|
}
|
|
|
|
// Deep cloning the object to prevent freezing the original object
|
|
const clonedValue = Array.isArray(value) ? [...value] : { ...value };
|
|
|
|
// Prevent infinite recursion by looking for cycles inside an object
|
|
if (frozenProps.has(value)) {
|
|
return frozenProps.get(value);
|
|
}
|
|
frozenProps.set(value, clonedValue);
|
|
|
|
const propNames = Reflect.ownKeys(clonedValue);
|
|
|
|
for (const name of propNames) {
|
|
const prop = Array.isArray(clonedValue) ? clonedValue[Number(name)] : clonedValue[name];
|
|
|
|
// If the property is an object:
|
|
// 1. clone it
|
|
// 2. freeze it
|
|
if (prop && (typeof prop === 'object' || typeof prop === 'function')) {
|
|
if (Array.isArray(clonedValue)) {
|
|
clonedValue[Number(name)] = deepFreeze(prop, frozenProps);
|
|
} else {
|
|
clonedValue[name] = deepFreeze(prop, frozenProps);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Object.freeze(clonedValue);
|
|
}
|
|
|
|
export function generateExtensionId(pluginId: string, extensionConfig: PluginExtensionConfig): string {
|
|
const str = `${pluginId}${extensionConfig.extensionPointId}${extensionConfig.title}`;
|
|
|
|
return Array.from(str)
|
|
.reduce((s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, 0)
|
|
.toString();
|
|
}
|
|
|
|
const _isProxy = Symbol('isReadOnlyProxy');
|
|
|
|
/**
|
|
* Returns a proxy that wraps the given object in a way that makes it read only.
|
|
* If you try to modify the object a TypeError exception will be thrown.
|
|
*
|
|
* @param obj The object to make read only
|
|
* @returns A new read only object, does not modify the original object
|
|
*/
|
|
export function getReadOnlyProxy<T extends object>(obj: T): T {
|
|
if (!obj || typeof obj !== 'object' || isReadOnlyProxy(obj)) {
|
|
return obj;
|
|
}
|
|
|
|
const cache = new WeakMap();
|
|
|
|
return new Proxy(obj, {
|
|
defineProperty: () => false,
|
|
deleteProperty: () => false,
|
|
isExtensible: () => false,
|
|
set: () => false,
|
|
get(target, prop, receiver) {
|
|
if (prop === _isProxy) {
|
|
return true;
|
|
}
|
|
|
|
const value = Reflect.get(target, prop, receiver);
|
|
|
|
if (isObject(value) || isArray(value)) {
|
|
if (!cache.has(value)) {
|
|
cache.set(value, getReadOnlyProxy(value));
|
|
}
|
|
return cache.get(value);
|
|
}
|
|
|
|
return value;
|
|
},
|
|
});
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string | number | symbol, unknown> {
|
|
return typeof value === 'object' && value !== null;
|
|
}
|
|
|
|
export function isReadOnlyProxy(value: unknown): boolean {
|
|
return isRecord(value) && value[_isProxy] === true;
|
|
}
|