The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/core/services/ng_react.ts

301 lines
9.7 KiB

//
// This is using ng-react with this PR applied https://github.com/ngReact/ngReact/pull/199
//
// # ngReact
// ### Use React Components inside of your Angular applications
//
// Composed of
// - reactComponent (generic directive for delegating off to React Components)
// - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
import React from 'react';
import ReactDOM from 'react-dom';
import angular from 'angular';
// get a react component from name (components can be an angular injectable e.g. value, factory or
// available on window
function getReactComponent(name, $injector) {
// if name is a function assume it is component and return it
if (angular.isFunction(name)) {
return name;
}
// a React component name must be specified
if (!name) {
throw new Error('ReactComponent name attribute must be specified');
}
// ensure the specified React component is accessible, and fail fast if it's not
let reactComponent;
try {
reactComponent = $injector.get(name);
} catch (e) {}
if (!reactComponent) {
try {
reactComponent = name.split('.').reduce((current, namePart) => {
return current[namePart];
}, window);
} catch (e) {}
}
if (!reactComponent) {
throw Error('Cannot find react component ' + name);
}
return reactComponent;
}
// wraps a function with scope.$apply, if already applied just return
function applied(fn, scope) {
if (fn.wrappedInApply) {
return fn;
}
//tslint:disable-next-line:only-arrow-functions
const wrapped: any = function() {
const args = arguments;
const phase = scope.$root.$$phase;
if (phase === '$apply' || phase === '$digest') {
return fn.apply(null, args);
} else {
return scope.$apply(() => {
return fn.apply(null, args);
});
}
};
wrapped.wrappedInApply = true;
return wrapped;
}
/**
* wraps functions on obj in scope.$apply
*
* keeps backwards compatibility, as if propsConfig is not passed, it will
* work as before, wrapping all functions and won't wrap only when specified.
*
* @version 0.4.1
* @param obj react component props
* @param scope current scope
* @param propsConfig configuration object for all properties
* @returns {Object} props with the functions wrapped in scope.$apply
*/
function applyFunctions(obj, scope, propsConfig?) {
return Object.keys(obj || {}).reduce((prev, key) => {
const value = obj[key];
const config = (propsConfig || {})[key] || {};
/**
* wrap functions in a function that ensures they are scope.$applied
* ensures that when function is called from a React component
* the Angular digest cycle is run
*/
prev[key] = angular.isFunction(value) && config.wrapApply !== false ? applied(value, scope) : value;
return prev;
}, {});
}
/**
*
* @param watchDepth (value of HTML watch-depth attribute)
* @param scope (angular scope)
*
* Uses the watchDepth attribute to determine how to watch props on scope.
* If watchDepth attribute is NOT reference or collection, watchDepth defaults to deep watching by value
*/
function watchProps(watchDepth, scope, watchExpressions, listener) {
const supportsWatchCollection = angular.isFunction(scope.$watchCollection);
const supportsWatchGroup = angular.isFunction(scope.$watchGroup);
const watchGroupExpressions = [];
watchExpressions.forEach(expr => {
const actualExpr = getPropExpression(expr);
const exprWatchDepth = getPropWatchDepth(watchDepth, expr);
if (exprWatchDepth === 'collection' && supportsWatchCollection) {
scope.$watchCollection(actualExpr, listener);
} else if (exprWatchDepth === 'reference' && supportsWatchGroup) {
watchGroupExpressions.push(actualExpr);
} else if (exprWatchDepth === 'one-time') {
//do nothing because we handle our one time bindings after this
} else {
scope.$watch(actualExpr, listener, exprWatchDepth !== 'reference');
}
});
if (watchDepth === 'one-time') {
listener();
}
if (watchGroupExpressions.length) {
scope.$watchGroup(watchGroupExpressions, listener);
}
}
// render React component, with scope[attrs.props] being passed in as the component props
function renderComponent(component, props, scope, elem) {
scope.$evalAsync(() => {
ReactDOM.render(React.createElement(component, props), elem[0]);
});
}
// get prop name from prop (string or array)
function getPropName(prop) {
return Array.isArray(prop) ? prop[0] : prop;
}
// get prop name from prop (string or array)
function getPropConfig(prop) {
return Array.isArray(prop) ? prop[1] : {};
}
// get prop expression from prop (string or array)
function getPropExpression(prop) {
return Array.isArray(prop) ? prop[0] : prop;
}
// find the normalized attribute knowing that React props accept any type of capitalization
function findAttribute(attrs, propName) {
const index = Object.keys(attrs).filter(attr => {
return attr.toLowerCase() === propName.toLowerCase();
})[0];
return attrs[index];
}
// get watch depth of prop (string or array)
function getPropWatchDepth(defaultWatch, prop) {
const customWatchDepth = Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth;
return customWatchDepth || defaultWatch;
}
// # reactComponent
// Directive that allows React components to be used in Angular templates.
//
// Usage:
// <react-component name="Hello" props="name"/>
//
// This requires that there exists an injectable or globally available 'Hello' React component.
// The 'props' attribute is optional and is passed to the component.
//
// The following would would create and register the component:
//
// var module = angular.module('ace.react.components');
// module.value('Hello', React.createClass({
// render: function() {
// return <div>Hello {this.props.name}</div>;
// }
// }));
//
const reactComponent = $injector => {
return {
restrict: 'E',
replace: true,
link: function(scope, elem, attrs) {
const reactComponent = getReactComponent(attrs.name, $injector);
const renderMyComponent = () => {
const scopeProps = scope.$eval(attrs.props);
const props = applyFunctions(scopeProps, scope);
renderComponent(reactComponent, props, scope, elem);
};
// If there are props, re-render when they change
attrs.props ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) : renderMyComponent();
// cleanup when scope is destroyed
scope.$on('$destroy', () => {
if (!attrs.onScopeDestroy) {
ReactDOM.unmountComponentAtNode(elem[0]);
} else {
scope.$eval(attrs.onScopeDestroy, {
unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
});
}
});
},
};
};
// # reactDirective
// Factory function to create directives for React components.
//
// With a component like this:
//
// var module = angular.module('ace.react.components');
// module.value('Hello', React.createClass({
// render: function() {
// return <div>Hello {this.props.name}</div>;
// }
// }));
//
// A directive can be created and registered with:
//
// module.directive('hello', function(reactDirective) {
// return reactDirective('Hello', ['name']);
// });
//
// Where the first argument is the injectable or globally accessible name of the React component
// and the second argument is an array of property names to be watched and passed to the React component
// as props.
//
// This directive can then be used like this:
//
// <hello name="name"/>
//
const reactDirective = $injector => {
return (reactComponentName, props, conf, injectableProps) => {
const directive = {
restrict: 'E',
replace: true,
link: function(scope, elem, attrs) {
const reactComponent = getReactComponent(reactComponentName, $injector);
// if props is not defined, fall back to use the React component's propTypes if present
props = props || Object.keys(reactComponent.propTypes || {});
// for each of the properties, get their scope value and set it to scope.props
const renderMyComponent = () => {
let scopeProps = {};
const config = {};
props.forEach(prop => {
const propName = getPropName(prop);
scopeProps[propName] = scope.$eval(findAttribute(attrs, propName));
config[propName] = getPropConfig(prop);
});
scopeProps = applyFunctions(scopeProps, scope, config);
scopeProps = angular.extend({}, scopeProps, injectableProps);
renderComponent(reactComponent, scopeProps, scope, elem);
};
// watch each property name and trigger an update whenever something changes,
// to update scope.props with new values
const propExpressions = props.map(prop => {
return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop];
});
// If we don't have any props, then our watch statement won't fire.
props.length ? watchProps(attrs.watchDepth, scope, propExpressions, renderMyComponent) : renderMyComponent();
// cleanup when scope is destroyed
scope.$on('$destroy', () => {
if (!attrs.onScopeDestroy) {
ReactDOM.unmountComponentAtNode(elem[0]);
} else {
scope.$eval(attrs.onScopeDestroy, {
unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
});
}
});
},
};
return angular.extend(directive, conf);
};
};
const ngModule = angular.module('react', []);
ngModule.directive('reactComponent', ['$injector', reactComponent]);
ngModule.factory('reactDirective', ['$injector', reactDirective]);