diff --git a/package.json b/package.json index 492bbf5359e..59608e80403 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,6 @@ "lodash": "^4.17.4", "moment": "^2.18.1", "mousetrap": "^1.6.0", - "ngreact": "^0.4.1", "perfect-scrollbar": "^1.2.0", "prop-types": "^15.6.0", "react": "^16.1.1", diff --git a/public/app/app.ts b/public/app/app.ts index 8e345c6abed..b76dceb4943 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -9,7 +9,6 @@ import 'angular-native-dragdrop'; import 'angular-bindonce'; import 'react'; import 'react-dom'; -import 'ngreact'; import 'vendor/bootstrap/bootstrap'; import 'vendor/angular-ui/ui-bootstrap-tpls'; diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 39601f903ec..eff55405d6f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -1,5 +1,3 @@ -/// - import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 2c5c5653fb2..5a9b4265f8c 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -18,6 +18,7 @@ import './components/colorpicker/ColorPicker'; import './components/colorpicker/SeriesColorPicker'; import './components/colorpicker/spectrum_picker'; import './services/search_srv'; +import './services/ng_react'; import {grafanaAppDirective} from './components/grafana_app'; import {sideMenuDirective} from './components/sidemenu/sidemenu'; diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts new file mode 100644 index 00000000000..9922772c730 --- /dev/null +++ b/public/app/core/services/ng_react.ts @@ -0,0 +1,301 @@ +// +// 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 + var reactComponent; + try { + reactComponent = $injector.get(name); + } catch (e) {} + + if (!reactComponent) { + try { + reactComponent = name.split('.').reduce(function(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; + } + var wrapped: any = function() { + var args = arguments; + var phase = scope.$root.$$phase; + if (phase === '$apply' || phase === '$digest') { + return fn.apply(null, args); + } else { + return scope.$apply(function() { + 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(function(prev, key) { + var value = obj[key]; + var 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) { + var supportsWatchCollection = angular.isFunction(scope.$watchCollection); + var supportsWatchGroup = angular.isFunction(scope.$watchGroup); + + var watchGroupExpressions = []; + + watchExpressions.forEach(function(expr) { + var actualExpr = getPropExpression(expr); + var 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(function() { + 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) { + var index = Object.keys(attrs).filter(function(attr) { + return attr.toLowerCase() === propName.toLowerCase(); + })[0]; + return attrs[index]; +} + +// get watch depth of prop (string or array) +function getPropWatchDepth(defaultWatch, prop) { + var 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: +// +// +// 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
Hello {this.props.name}
; +// } +// })); +// +var reactComponent = function($injector) { + return { + restrict: 'E', + replace: true, + link: function(scope, elem, attrs) { + var reactComponent = getReactComponent(attrs.name, $injector); + + var renderMyComponent = function() { + var scopeProps = scope.$eval(attrs.props); + var 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', function() { + 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
Hello {this.props.name}
; +// } +// })); +// +// 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: +// +// +// +var reactDirective = function($injector) { + return function(reactComponentName, props, conf, injectableProps) { + var directive = { + restrict: 'E', + replace: true, + link: function(scope, elem, attrs) { + var 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 + var renderMyComponent = function() { + var scopeProps = {}, + config = {}; + + props.forEach(function(prop) { + var 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 + var propExpressions = props.map(function(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', function() { + if (!attrs.onScopeDestroy) { + ReactDOM.unmountComponentAtNode(elem[0]); + } else { + scope.$eval(attrs.onScopeDestroy, { + unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]), + }); + } + }); + }, + }; + return angular.extend(directive, conf); + }; +}; + +let ngModule = angular.module('react', []); +ngModule.directive('reactComponent', ['$injector', reactComponent]); +ngModule.factory('reactDirective', ['$injector', reactDirective]); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 467a65597a5..7b0f040e199 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -160,7 +160,6 @@ export class DashboardGrid extends React.Component { } render() { - console.log('animated', this.state.animated); return (