├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── GUIDES.MD ├── README.MD ├── chrome ├── devtools.html ├── fontawesome │ ├── css │ │ └── font-awesome.min.css │ └── fonts │ │ └── fontawesome-webfont.woff2 ├── icons │ ├── client-added.png │ ├── client-changed.png │ ├── client-connect.png │ ├── client-method.png │ ├── client-ping.png │ ├── client-pong.png │ ├── client-removed.png │ ├── client-sub.png │ ├── client.png │ ├── icon128.png │ ├── icon16.png │ ├── icon19.png │ ├── icon48.png │ ├── server-added.png │ ├── server-changed.png │ ├── server-connected.png │ ├── server-ping.png │ ├── server-pong.png │ ├── server-ready.png │ ├── server-removed.png │ ├── server-result.png │ ├── server-updated.png │ ├── server.png │ └── toolbarButtonGlyphs_2x.png ├── manifest.json ├── panel.html ├── scripts │ ├── background.js │ ├── content.js │ └── devtools.js └── styles │ └── icons.css ├── index.html ├── license ├── package.json ├── server.js ├── src ├── common │ ├── actions.js │ ├── analytics.js │ ├── bridge.js │ ├── constants.js │ ├── inject.js │ ├── reducers.js │ └── styles │ │ └── app.scss ├── main.jsx ├── patch │ └── react-tabs │ │ ├── components │ │ ├── Tab.js │ │ ├── TabList.js │ │ ├── TabPanel.js │ │ └── Tabs.js │ │ ├── helpers │ │ ├── childrenPropType.js │ │ ├── styles.js │ │ └── uuid.js │ │ └── index.js └── plugins │ ├── blaze │ ├── actions │ │ └── index.js │ ├── blaze.css │ ├── components │ │ ├── props │ │ │ └── index.jsx │ │ └── tree │ │ │ ├── index.jsx │ │ │ ├── node-prop-type.js │ │ │ └── node.jsx │ ├── constants │ │ └── index.js │ ├── fake.js │ ├── highlighter │ │ ├── index.js │ │ ├── multi-overlay.js │ │ └── overlay.js │ ├── index.jsx │ ├── inject.js │ └── reducers │ │ └── index.js │ ├── ddp │ ├── __tests__ │ │ ├── ddp-message-generator.js │ │ ├── processors.js │ │ ├── trace-filters.js │ │ └── warnings.js │ ├── actions │ │ ├── filters.js │ │ └── traces.js │ ├── components │ │ ├── clear-logs-button.jsx │ │ ├── filter.jsx │ │ ├── json-tree-item.jsx │ │ ├── stats.jsx │ │ ├── trace-item-prop-types.jsx │ │ ├── trace-item-tabs.jsx │ │ ├── trace-item.jsx │ │ ├── trace-list.jsx │ │ └── warnings.jsx │ ├── constants │ │ ├── action-types.js │ │ └── operation-types.js │ ├── ddp.css │ ├── index.jsx │ ├── inject.js │ ├── lib │ │ ├── ddp-generator.js │ │ ├── helpers.js │ │ ├── processors │ │ │ ├── associations.js │ │ │ ├── groups.js │ │ │ ├── labels.js │ │ │ ├── message-size.js │ │ │ ├── operation-types.js │ │ │ └── operations.js │ │ ├── stats.js │ │ ├── trace-filter.js │ │ ├── trace-processor.js │ │ └── warnings │ │ │ ├── index.js │ │ │ ├── unknown-publication.js │ │ │ └── user-overpublish.js │ └── reducers │ │ ├── filters.js │ │ ├── index.js │ │ └── traces.js │ ├── index.js │ ├── minimongo │ ├── actions │ │ └── index.js │ ├── components │ │ ├── collection-input.jsx │ │ ├── collection-item.jsx │ │ ├── collection-list.jsx │ │ └── data-tree.jsx │ ├── constants │ │ └── index.js │ ├── fake.js │ ├── index.jsx │ ├── inject.js │ ├── lib │ │ ├── doc-matcher.js │ │ ├── doc-projector.js │ │ └── doc-sorter.js │ ├── minimongo.scss │ └── reducers │ │ └── index.js │ └── security │ ├── actions │ └── index.js │ ├── components │ ├── collection-audit.js │ ├── collections-panel.js │ ├── method-audit.js │ ├── methods-panel.js │ └── package-audit.js │ ├── constants │ └── index.js │ ├── index.jsx │ ├── inject.js │ ├── lib │ └── index.js │ ├── reducers │ └── index.js │ └── security.scss └── webpack ├── app.dev.js ├── base.js ├── chrome.dev.js ├── chrome.inject.js └── chrome.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015","react"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | chrome/build/bundle.js 2 | chrome/scripts/lib 3 | node_modules -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "rules": { 11 | "react/jsx-boolean-value": 1, 12 | "react/jsx-no-undef": 1, 13 | "jsx-quotes": [2, "prefer-double"], 14 | "react/jsx-sort-props": 1, 15 | "react/jsx-uses-react": 1, 16 | "react/jsx-uses-vars": 1, 17 | "react/no-did-mount-set-state": 1, 18 | "react/no-did-update-set-state": 1, 19 | "react/no-unknown-property": 1, 20 | "react/prop-types": 1, 21 | "react/self-closing-comp": 1, 22 | "react/sort-comp": 0, 23 | "react/wrap-multilines": 1, 24 | 25 | // In declarations, required props should be separate 26 | "react/jsx-sort-prop-types": 0, 27 | 28 | // Useful for "inner" components 29 | "react/no-multi-comp": 0, 30 | 31 | // babel does this automatically 32 | "react/display-name": 0, 33 | 34 | // Not needed, and we usually do `var {Component} = require('react');` 35 | "react/react-in-jsx-scope": 0, 36 | 37 | // babel removes these 38 | "comma-dangle": 0, 39 | 40 | "quotes": [2, "single"], 41 | "no-unused-expressions": 0, 42 | "no-underscore-dangle": 0, 43 | "no-use-before-define": 0, 44 | "strict": 0, 45 | "no-loop-func": 0, 46 | "new-cap": 0, 47 | "no-unused-vars": 0, 48 | "curly": [2, "multi-line"] 49 | } 50 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | chrome/build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | sudo: false -------------------------------------------------------------------------------- /GUIDES.MD: -------------------------------------------------------------------------------- 1 | # Things **NOT** to do in Meteor 2 | 3 | ## Overpublishing user data 4 | 5 | Limit fields published using Meteor.users.find() 6 | 7 | ``` 8 | Looks harmless right? Yeah, it would seem so, until you go into your browser console and type Meteor.users.find({}).fetch() and inspect the records that come back. You've just published all the data for any users attached to games, such as oauth tokens, bcrypted password hash, resume tokens, etc. This move will earn you the nickname 'LinkedIn' from your peers :) 9 | ``` 10 | via Josh Owens in [Meteor Security 101](http://joshowens.me/meteor-security-101/) 11 | 12 | ## Using client-side operations with collections 13 | 14 | Do not invoke insert/update/remove directly on collections from client side, user methods instead 15 | 16 | ``` 17 | - Collections can only be updated through methods (no client-side operations). 18 | - We'll use two-tiered methods along with the mutator pattern. 19 | ``` 20 | via David Weldon in [meteor: how we define methods](https://dweldon.silvrback.com/methods) 21 | 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ![Meteor DevTools Extension](https://dl.dropboxusercontent.com/u/9224326/meteor-devtools-1.1.1.gif) 2 | # Meteor Dev Tools Chrome Extension 3 | [![Build Status](https://travis-ci.org/thebakeryio/meteor-devtools.svg)](https://travis-ci.org/thebakeryio/meteor-devtools) 4 | [![Dependency Status](https://david-dm.org/thebakeryio/meteor-devtools.svg)](https://david-dm.org/thebakeryio/meteor-devtools) 5 | [![devDependency Status](https://david-dm.org/thebakeryio/meteor-devtools/dev-status.svg)](https://david-dm.org/thebakeryio/meteor-devtools#info=devDependencies) 6 | [![ES-2015](https://img.shields.io/badge/ES-2015-brightgreen.svg)](https://babeljs.io/docs/learn-es2015/) 7 | 8 | Meteor Devtools (MD) is an extension for Chrome Developer Tools that makes the process of developing Meteor apps even more enjoyable. It also allows you to look under the hood of existing applications and learn how they are built. MD includes a plugin framework and currrently comes with 3 plugins: DDP Monitor, Blaze Inspector and MiniMongo Explorer. You can [install](https://chrome.google.com/webstore/detail/meteor-devtools/ippapidnnboiophakmmhkdlchoccbgje) it from Chrome Web Store. 9 | 10 | ## Development 11 | 12 | Running local dev server 13 | 14 | ```bash 15 | npm start 16 | ``` 17 | 18 | Running tests while developing (with reload) 19 | 20 | ```bash 21 | npm test -- --watch 22 | ``` 23 | 24 | Building chrome extension for local testing (result in ./chrome) 25 | 26 | ```bash 27 | npm run chrome 28 | ``` 29 | 30 | Building chrome extension for production (result in ./chrome) 31 | 32 | ```bash 33 | npm run chrome:build 34 | ``` 35 | 36 | ## Credits 37 | 38 | Meteor Devtools Extension is made by Meteor loving folks at [The Bakery](http://thebakery.io). Blaze Inspector is inspired by [React Devtools](https://github.com/facebook/react-devtools) and uses Element highlighter from that project. -------------------------------------------------------------------------------- /chrome/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /chrome/fontawesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/fontawesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /chrome/icons/client-added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-added.png -------------------------------------------------------------------------------- /chrome/icons/client-changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-changed.png -------------------------------------------------------------------------------- /chrome/icons/client-connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-connect.png -------------------------------------------------------------------------------- /chrome/icons/client-method.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-method.png -------------------------------------------------------------------------------- /chrome/icons/client-ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-ping.png -------------------------------------------------------------------------------- /chrome/icons/client-pong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-pong.png -------------------------------------------------------------------------------- /chrome/icons/client-removed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-removed.png -------------------------------------------------------------------------------- /chrome/icons/client-sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client-sub.png -------------------------------------------------------------------------------- /chrome/icons/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/client.png -------------------------------------------------------------------------------- /chrome/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/icon128.png -------------------------------------------------------------------------------- /chrome/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/icon16.png -------------------------------------------------------------------------------- /chrome/icons/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/icon19.png -------------------------------------------------------------------------------- /chrome/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/icon48.png -------------------------------------------------------------------------------- /chrome/icons/server-added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-added.png -------------------------------------------------------------------------------- /chrome/icons/server-changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-changed.png -------------------------------------------------------------------------------- /chrome/icons/server-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-connected.png -------------------------------------------------------------------------------- /chrome/icons/server-ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-ping.png -------------------------------------------------------------------------------- /chrome/icons/server-pong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-pong.png -------------------------------------------------------------------------------- /chrome/icons/server-ready.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-ready.png -------------------------------------------------------------------------------- /chrome/icons/server-removed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-removed.png -------------------------------------------------------------------------------- /chrome/icons/server-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-result.png -------------------------------------------------------------------------------- /chrome/icons/server-updated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server-updated.png -------------------------------------------------------------------------------- /chrome/icons/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/server.png -------------------------------------------------------------------------------- /chrome/icons/toolbarButtonGlyphs_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/meteor-devtools/071a6b89c70c0e46b0fe2c441c6c67a77c811a8e/chrome/icons/toolbarButtonGlyphs_2x.png -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Meteor DevTools", 3 | "version": "1.6.0", 4 | "description": "Developer tools for Meteor", 5 | "background" : { 6 | "scripts": ["scripts/background.js"], 7 | "persistent": false 8 | }, 9 | "icons": { 10 | "16": "icons/icon16.png", 11 | "48": "icons/icon48.png", 12 | "128": "icons/icon128.png" 13 | }, 14 | "content_scripts": [{ 15 | "matches": [""], 16 | "js": ["scripts/content.js"], 17 | "run_at": "document_end", 18 | "all_frames": true 19 | }], 20 | "content_security_policy": "script-src 'self' 'unsafe-eval' https://www.google-analytics.com; object-src 'self'", 21 | 22 | "devtools_page": "devtools.html", 23 | "options_page": "panel.html", 24 | "manifest_version": 2 25 | } -------------------------------------------------------------------------------- /chrome/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Meteor Devtools 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /chrome/scripts/background.js: -------------------------------------------------------------------------------- 1 | var connections = {}; 2 | 3 | // Receive message from content script and relay to the devTools page for the 4 | // current tab 5 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 6 | // Messages from content scripts should have sender.tab set 7 | if (sender.tab) { 8 | var tabId = sender.tab.id; 9 | if (tabId in connections) { 10 | connections[tabId].postMessage(request); 11 | } 12 | } 13 | return true; 14 | }); 15 | 16 | chrome.runtime.onConnect.addListener(function(port) { 17 | // Listen to messages sent from the DevTools page 18 | port.onMessage.addListener(function(request) { 19 | // Register initial connection 20 | if (request.name === 'init') { 21 | connections[request.tabId] = port; 22 | 23 | port.onDisconnect.addListener(function() { 24 | delete connections[request.tabId]; 25 | }); 26 | 27 | return; 28 | } 29 | }); 30 | 31 | }); 32 | 33 | chrome.tabs.onRemoved.addListener(function (tabId) { 34 | if (connections[tabId]) { 35 | delete connections[tabId]; 36 | } 37 | }); -------------------------------------------------------------------------------- /chrome/scripts/content.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('message', function(event) { 2 | // Only accept messages from same frame 3 | if (event.source !== window) { 4 | return; 5 | } 6 | 7 | var message = event.data; 8 | 9 | // Only accept messages that we know are ours 10 | if (typeof message !== 'object' || message === null || 11 | (message.source !== 'ddp-monitor-extension')) { 12 | return; 13 | } 14 | 15 | chrome.runtime.sendMessage(message); 16 | }); -------------------------------------------------------------------------------- /chrome/scripts/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('Meteor', 'assets/icons/icon16.png', 'panel.html'); 2 | -------------------------------------------------------------------------------- /chrome/styles/icons.css: -------------------------------------------------------------------------------- 1 | .client, .server { 2 | height: 24px; 3 | width: 24px; 4 | display: inline-block; 5 | vertical-align: middle; 6 | margin-right: 5px; 7 | background-size: 100%; 8 | } 9 | 10 | .server { 11 | background-image: url('../icons/server.png'); 12 | } 13 | 14 | .server.added { 15 | background-image: url('../icons/server-added.png'); 16 | } 17 | 18 | .server.removed { 19 | background-image: url('../icons/server-removed.png'); 20 | } 21 | 22 | .server.updated { 23 | background-image: url('../icons/server-updated.png'); 24 | } 25 | 26 | .server.changed { 27 | background-image: url('../icons/server-changed.png'); 28 | } 29 | 30 | .server.ping { 31 | background-image: url('../icons/server-ping.png'); 32 | } 33 | 34 | .server.pong { 35 | background-image: url('../icons/server-pong.png'); 36 | } 37 | 38 | .server.ready { 39 | background-image: url('../icons/server-ready.png'); 40 | } 41 | 42 | .server.connected { 43 | background-image: url('../icons/server-connected.png'); 44 | } 45 | 46 | .server.result { 47 | background-image: url('../icons/server-result.png'); 48 | } 49 | 50 | .client { 51 | background-image: url('../icons/client.png'); 52 | } 53 | 54 | .client.added { 55 | background-image: url('../icons/client-added.png'); 56 | } 57 | 58 | .client.removed { 59 | background-image: url('../icons/client-removed.png'); 60 | } 61 | 62 | .client.changed { 63 | background-image: url('../icons/client-changed.png'); 64 | } 65 | 66 | .client.sub { 67 | background-image: url('../icons/client-sub.png'); 68 | } 69 | 70 | .client.ping { 71 | background-image: url('../icons/client-ping.png'); 72 | } 73 | 74 | .client.pong { 75 | background-image: url('../icons/client-pong.png'); 76 | } 77 | 78 | .client.method { 79 | background-image: url('../icons/client-method.png'); 80 | } 81 | 82 | .client.connect { 83 | background-image: url('../icons/client-connect.png'); 84 | } 85 | 86 | .toolbar-glyph { 87 | -webkit-mask-image: url('../icons/toolbarButtonGlyphs_2x.png'); 88 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | testing stuff 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) The Bakery (thebakery.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-dev-tools", 3 | "version": "1.6.0", 4 | "description": "Meteor Dev Tools extension", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/thebakeryio/meteor-ddp-monitor.git" 8 | }, 9 | "keywords": [ 10 | "meteor", 11 | "ddp" 12 | ], 13 | "scripts": { 14 | "start": "node server.js", 15 | "test": "jest", 16 | "chrome": "npm run test && webpack --config webpack/chrome.dev.js && webpack --config webpack/chrome.inject.js", 17 | "chrome:build": "npm run test && webpack --config webpack/chrome.prod.js && webpack --config webpack/chrome.inject.js" 18 | }, 19 | "jest": { 20 | "scriptPreprocessor": "./node_modules/babel-jest", 21 | "testFileExtensions": [ 22 | "js" 23 | ], 24 | "moduleFileExtensions": [ 25 | "js", 26 | "jsx" 27 | ] 28 | }, 29 | "author": "The Bakery", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/thebakeryio/meteor-ddp-monitor/issues" 33 | }, 34 | "homepage": "https://github.com/thebakeryio/meteor-ddp-monitor", 35 | "devDependencies": { 36 | "babel-eslint": "^6.0.4", 37 | "babel-jest": "^12.0.2", 38 | "babel-loader": "^6.2.0", 39 | "babel-polyfill": "^6.2.0", 40 | "babel-preset-es2015": "^6.1.18", 41 | "babel-preset-react": "^6.1.18", 42 | "css-loader": "^0.23.1", 43 | "eslint": "^2.1.0", 44 | "eslint-plugin-react": "^5.1.1", 45 | "jest-cli": "^12.0.2", 46 | "marsdb": "^0.6.9", 47 | "node-sass": "^3.8.0", 48 | "postcss-loader": "^0.9.1", 49 | "precss": "^1.4.0", 50 | "react-hot-loader": "^1.3.0", 51 | "sass-loader": "^4.0.0", 52 | "style-loader": "^0.13.1", 53 | "webpack": "^1.13.1", 54 | "webpack-dev-server": "^1.12.0" 55 | }, 56 | "dependencies": { 57 | "classnames": "^2.2.0", 58 | "error-stack-parser": "^1.3.3", 59 | "immutable": "^3.8.1", 60 | "js-stylesheet": "0.0.1", 61 | "moment": "^2.11.2", 62 | "object-assign": "^4.0.1", 63 | "pretty-bytes": "^3.0.1", 64 | "react": "^15.0.2", 65 | "react-dom": "^15.0.2", 66 | "react-json-tree": "0.7.4", 67 | "react-notification-system": "^0.2.7", 68 | "react-redux": "^4.4.0", 69 | "react-split-pane": "^0.1.43", 70 | "redux": "^3.3.1", 71 | "slugify": "^0.1.1", 72 | "underscore": "^1.8.3" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack/app.dev'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(3000, 'localhost', function (err, result) { 10 | if (err) { 11 | console.log(err); 12 | } 13 | console.log('Listening at localhost:3000'); 14 | }); -------------------------------------------------------------------------------- /src/common/actions.js: -------------------------------------------------------------------------------- 1 | import { SET_TAB_INDEX } from './constants' 2 | 3 | export function setTabIndex(data) { 4 | return { 5 | type: SET_TAB_INDEX, 6 | index: data 7 | } 8 | } -------------------------------------------------------------------------------- /src/common/analytics.js: -------------------------------------------------------------------------------- 1 | const isProduction = (process.env.NODE_ENV === 'production'); 2 | 3 | module.exports = { 4 | setup() { 5 | if(!isProduction) { 6 | return; 7 | } 8 | 9 | var _gaq = _gaq || []; 10 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 11 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 12 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 13 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); // Note: https protocol here 14 | 15 | ga('create', 'UA-41814664-7', 'auto'); 16 | // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200 17 | ga('set', 'checkProtocolTask', function(){}); 18 | ga('require', 'displayfeatures'); 19 | }, 20 | 21 | trackPageView(pageUrl) { 22 | isProduction && ga && ga('send', 'pageview', pageUrl); 23 | }, 24 | 25 | trackEvent(eventCategory, eventName, eventData) { 26 | isProduction && ga && ga('send', 'event', eventCategory, eventName, eventData); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/common/bridge.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | let __messageCallbacks = []; 4 | let __pageReloadCallbacks = []; 5 | 6 | const __registerCallback = (callback, callbackList) => { 7 | if (!callback) { 8 | return; 9 | } 10 | 11 | const existingCallback = _.find(callbackList, (c) => c === callback); 12 | if (existingCallback) { 13 | return; 14 | } 15 | 16 | callbackList.push(callback); 17 | }; 18 | 19 | const __unregisterCallback = (callback, callbackList) => { 20 | if (!callback) { 21 | return; 22 | } 23 | callbackList = _.filter(callbackList, (c) => c !== callback); 24 | }; 25 | 26 | export default { 27 | registerMessageCallback(callback) { 28 | __registerCallback(callback, __messageCallbacks); 29 | }, 30 | 31 | removeMessageCallback(callback) { 32 | __unregisterCallback(callback, __messageCallbacks); 33 | }, 34 | 35 | registerPageReloadCallback(callback) { 36 | __registerCallback(callback, __pageReloadCallbacks); 37 | }, 38 | 39 | removePageReloadCallback(callback) { 40 | __unregisterCallback(callback, __pageReloadCallbacks); 41 | }, 42 | 43 | openResource(url, lineNumber, callback){ 44 | if(chrome && chrome.devtools){ 45 | chrome.devtools.panels.openResource(url, lineNumber, callback); 46 | } 47 | }, 48 | 49 | sendMessageToThePage(message) { 50 | if(chrome && chrome.devtools){ 51 | chrome.devtools.inspectedWindow.eval( 52 | `__meteor_devtools_receiveMessage(${JSON.stringify(message)})` 53 | ); 54 | } 55 | }, 56 | 57 | setup(){ 58 | if(chrome && chrome.devtools){ 59 | let chromeSetup = function(){ 60 | // Create a connection to the background page 61 | const backgroundPageConnection = chrome.runtime.connect({ 62 | name: 'panel' 63 | }); 64 | 65 | backgroundPageConnection.postMessage({ 66 | name: 'init', 67 | tabId: chrome.devtools.inspectedWindow.tabId 68 | }); 69 | 70 | backgroundPageConnection.onMessage.addListener(function(msg) { 71 | _.each(__messageCallbacks, (mc) => mc && mc.call(this, null, msg)); 72 | }); 73 | }; 74 | 75 | let injectScript = (scriptUrl) => { 76 | let xhr = new XMLHttpRequest(); 77 | xhr.open('GET', chrome.extension.getURL(scriptUrl), false); 78 | xhr.send(); 79 | chrome.devtools.inspectedWindow.eval(xhr.responseText); 80 | }; 81 | 82 | let pageSetup = () => injectScript('/build/inject.js'); 83 | 84 | chromeSetup.call(this); 85 | pageSetup.call(this); 86 | 87 | chrome.devtools.network.onNavigated.addListener(function(){ 88 | pageSetup.call(this); 89 | _.each(__pageReloadCallbacks, (prc) => prc && prc.call(this)); 90 | }); 91 | } 92 | } 93 | }; -------------------------------------------------------------------------------- /src/common/constants.js: -------------------------------------------------------------------------------- 1 | export const SET_TAB_INDEX = 'set_tab_index'; 2 | 3 | export const DDP_TAB_INDEX = 0; 4 | export const BLAZE_TAB_INDEX = 1; 5 | export const MINIMONGO_TAB_INDEX = 2; 6 | export const SECURITY_TAB_INDEX = 3; -------------------------------------------------------------------------------- /src/common/inject.js: -------------------------------------------------------------------------------- 1 | import ddpInject from '../plugins/ddp/inject'; 2 | import blazeInject from '../plugins/blaze/inject'; 3 | import miniMongoInject from '../plugins/minimongo/inject'; 4 | import securityInject from '../plugins/security/inject'; 5 | 6 | (() => { 7 | const talkToExtension = (eventType, data) => { 8 | window.postMessage({ 9 | eventType : eventType, 10 | data: data, 11 | source: 'ddp-monitor-extension' 12 | }, '*'); 13 | }; 14 | 15 | const readyStateCheckInterval = setInterval(function() { 16 | const isMeteorDefined = typeof Meteor !== 'undefined'; 17 | if (document.readyState === 'complete' || isMeteorDefined) { 18 | clearInterval(readyStateCheckInterval); 19 | if(isMeteorDefined){ 20 | const plugins = [ddpInject, blazeInject, miniMongoInject, securityInject]; 21 | for(var i=0; i { 15 | if (index === DDP_TAB_INDEX){ 16 | Analytics.trackPageView('meteor ddp monitor'); 17 | } 18 | if(index === BLAZE_TAB_INDEX){ 19 | Analytics.trackPageView('blaze inspector'); 20 | } 21 | if(index === MINIMONGO_TAB_INDEX){ 22 | Analytics.trackPageView('minimongo explorer'); 23 | } 24 | if(index === SECURITY_TAB_INDEX){ 25 | Analytics.trackPageView('security auditor'); 26 | } 27 | }; 28 | 29 | export default { 30 | tabIndex (state = 0, action) { 31 | switch(action.type){ 32 | case SET_TAB_INDEX: 33 | trackRoute(action.index); 34 | return action.index; 35 | case SET_MINIMONGO_COLLECTION_SELECTION: 36 | return MINIMONGO_TAB_INDEX; 37 | case SET_MINIMONGO_COLLECTION_AND_QUERY: 38 | return MINIMONGO_TAB_INDEX; 39 | default: 40 | return state; 41 | } 42 | }, 43 | }; -------------------------------------------------------------------------------- /src/common/styles/app.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color:#ffffff; 3 | padding:0; 4 | margin:0; 5 | height: 100%; 6 | font-family: 'Lucida Grande', sans-serif; 7 | font-size: 12px; 8 | } 9 | 10 | .app-container { 11 | height: 100%; 12 | } 13 | 14 | .tab-wrapper { 15 | height: 100%; 16 | } 17 | 18 | .app-tabs { 19 | &.react-tabs { 20 | height: 100%; 21 | overflow: hidden; 22 | margin: 0; 23 | position: relative; 24 | 25 | > ul { 26 | margin: 0; 27 | background-color: #f3f3f3; 28 | 29 | > li { 30 | margin-left: 0px !important; 31 | margin-top: 2px; 32 | bottom: 0; 33 | border-radius: 0 !important; 34 | } 35 | } 36 | } 37 | } 38 | 39 | .react-tabs { 40 | > ul { 41 | > li:first-child { 42 | margin-left: 5px !important; 43 | } 44 | } 45 | } 46 | 47 | .ReactTabs__Tab--selected { 48 | background-color: white !important; 49 | } 50 | 51 | .app-tab-panel { 52 | overflow-y: scroll; 53 | height: 100%; 54 | } 55 | 56 | .gh-link { 57 | @media (min-width: 380px) { 58 | display: block; 59 | } 60 | 61 | display: none; 62 | list-style: none; 63 | float: right; 64 | padding: 6px 6px 0 0; 65 | 66 | a { 67 | text-decoration: none; 68 | 69 | &:visited { 70 | color:rgb(0, 0, 238); 71 | } 72 | } 73 | 74 | .icon-baker-logo { 75 | float: left; 76 | height: 13px; 77 | width: 9px; 78 | margin-right: 6px; 79 | text-decoration: none; 80 | } 81 | } 82 | 83 | input[type="radio"], input[type="checkbox"] { 84 | margin: 3px 0.5ex; 85 | } 86 | 87 | .Resizer { 88 | background: #000; 89 | opacity: .2; 90 | z-index: 1; 91 | -moz-box-sizing: border-box; 92 | -webkit-box-sizing: border-box; 93 | box-sizing: border-box; 94 | -moz-background-clip: padding; 95 | -webkit-background-clip: padding; 96 | background-clip: padding-box; 97 | 98 | &:hover { 99 | -webkit-transition: all 2s ease; 100 | transition: all 2s ease; 101 | } 102 | 103 | &.vertical { 104 | width: 11px; 105 | margin: 0 -5px; 106 | border-left: 5px solid rgba(255, 255, 255, 0); 107 | border-right: 5px solid rgba(255, 255, 255, 0); 108 | cursor: col-resize; 109 | } 110 | 111 | &.vertical:hover { 112 | border-left: 5px solid rgba(0, 0, 0, 0.5); 113 | border-right: 5px solid rgba(0, 0, 0, 0.5); 114 | } 115 | 116 | &.disabled { 117 | cursor: not-allowed; 118 | } 119 | 120 | &.disabled:hover { 121 | border-color: transparent; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import { getStore, getPlugins } from './plugins' 3 | import { render } from 'react-dom' 4 | import { Provider, connect } from 'react-redux' 5 | import React, { Component, PropTypes } from 'react'; 6 | import NotificationSystem from 'react-notification-system' 7 | import {Tab, Tabs, TabList, TabPanel} from './patch/react-tabs' 8 | import _ from 'underscore'; 9 | import Analytics from './common/analytics'; 10 | import slugify from 'slugify'; 11 | import Bridge from './common/bridge'; 12 | import './common/styles/app.scss'; 13 | import { setTabIndex } from './common/actions'; 14 | 15 | class App extends Component { 16 | showGlobalError(msg) { 17 | this._notificationSystem.addNotification({ 18 | message: msg, 19 | level: 'error', 20 | position: 'br' 21 | }); 22 | } 23 | 24 | componentDidMount() { 25 | this._notificationSystem = this.refs.notificationSystem; 26 | window.onerror = (message) => { 27 | this.showGlobalError(message); 28 | }; 29 | Analytics.trackPageView('loaded devtools'); 30 | } 31 | 32 | render() { 33 | const { dispatch, tabIndex } = this.props; 34 | const plugins = getPlugins(); 35 | const tabs = _.map(plugins, (p) => { 36 | const keyName = `tab-${slugify(p.name)}`; 37 | return {p.name}; 38 | }); 39 | const tabPanels = _.map(plugins, (p) => { 40 | const keyName = `tab-panel-${slugify(p.name)}`; 41 | return {p.component}; 42 | }); 43 | 44 | const _handleSelect = (index, last) => { 45 | dispatch(setTabIndex(index)); 46 | }; 47 | 48 | const notificaitonStyle = { 49 | NotificationItem: { 50 | DefaultStyle: { 51 | margin: '10px 5px 40px 1px' 52 | } 53 | } 54 | }; 55 | return ( 56 |
57 | 62 | 63 | {tabs} 64 |
  • 65 | 66 | 67 | 68 | 69 | 77 | 78 | 79 | 80 | The Bakery 81 | 82 |  |  83 | 84 | 85 | 86 |
  • 87 |
    88 | {tabPanels} 89 |
    90 | 91 |
    92 | ) 93 | } 94 | } 95 | 96 | App.propTypes = { 97 | tabIndex : PropTypes.number, 98 | }; 99 | 100 | ((rootElement, AppContainer, store) => { 101 | Bridge.setup() 102 | Analytics.setup() 103 | rootElement.innerHTML = '' 104 | render(, rootElement) 105 | })(document.querySelector('.app-container'), connect((state) => { 106 | return { 107 | tabIndex: state.tabIndex 108 | } 109 | })(App), getStore()) 110 | -------------------------------------------------------------------------------- /src/patch/react-tabs/components/Tab.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import cx from 'classnames'; 4 | 5 | function syncNodeAttributes(node, props) { 6 | if (props.selected) { 7 | node.setAttribute('tabindex', 0); 8 | node.setAttribute('selected', 'selected'); 9 | if (props.focus) { 10 | node.focus(); 11 | } 12 | } else { 13 | node.removeAttribute('tabindex'); 14 | node.removeAttribute('selected'); 15 | } 16 | } 17 | 18 | module.exports = React.createClass({ 19 | displayName: 'Tab', 20 | 21 | propTypes: { 22 | className: PropTypes.string, 23 | id: PropTypes.string, 24 | selected: PropTypes.bool, 25 | disabled: PropTypes.bool, 26 | panelId: PropTypes.string, 27 | children: PropTypes.oneOfType([ 28 | PropTypes.array, 29 | PropTypes.object, 30 | PropTypes.string 31 | ]) 32 | }, 33 | 34 | getDefaultProps() { 35 | return { 36 | focus: false, 37 | selected: false, 38 | id: null, 39 | panelId: null 40 | }; 41 | }, 42 | 43 | componentDidMount() { 44 | syncNodeAttributes(findDOMNode(this), this.props); 45 | }, 46 | 47 | componentDidUpdate() { 48 | syncNodeAttributes(findDOMNode(this), this.props); 49 | }, 50 | 51 | render() { 52 | return ( 53 | 71 | ); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/patch/react-tabs/components/TabList.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import cx from 'classnames'; 3 | 4 | module.exports = React.createClass({ 5 | displayName: 'TabList', 6 | 7 | propTypes: { 8 | className: PropTypes.string, 9 | children: PropTypes.oneOfType([ 10 | PropTypes.object, 11 | PropTypes.array 12 | ]) 13 | }, 14 | 15 | render() { 16 | return ( 17 |
      24 | {this.props.children} 25 |
    26 | ); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/patch/react-tabs/components/TabPanel.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import cx from 'classnames'; 3 | 4 | module.exports = React.createClass({ 5 | displayName: 'TabPanel', 6 | 7 | propTypes: { 8 | className: PropTypes.string, 9 | selected: PropTypes.bool, 10 | id: PropTypes.string, 11 | tabId: PropTypes.string, 12 | children: PropTypes.oneOfType([ 13 | PropTypes.array, 14 | PropTypes.object, 15 | PropTypes.string 16 | ]) 17 | }, 18 | 19 | contextTypes: { 20 | forceRenderTabPanel: PropTypes.bool 21 | }, 22 | 23 | getDefaultProps() { 24 | return { 25 | selected: false, 26 | id: null, 27 | tabId: null 28 | }; 29 | }, 30 | 31 | render() { 32 | const children = (this.context.forceRenderTabPanel || this.props.selected) ? 33 | this.props.children : 34 | null; 35 | 36 | return ( 37 |
    50 | {children} 51 |
    52 | ); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /src/patch/react-tabs/components/Tabs.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes, cloneElement} from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import cx from 'classnames'; 4 | import jss from 'js-stylesheet'; 5 | import uuid from '../helpers/uuid'; 6 | import childrenPropType from '../helpers/childrenPropType'; 7 | 8 | // Determine if a tab node is disabled 9 | function isTabDisabled(node) { 10 | return node.getAttribute('aria-disabled') === 'true'; 11 | } 12 | 13 | let useDefaultStyles = true; 14 | 15 | module.exports = React.createClass({ 16 | displayName: 'Tabs', 17 | 18 | propTypes: { 19 | className: PropTypes.string, 20 | selectedIndex: PropTypes.number, 21 | onSelect: PropTypes.func, 22 | focus: PropTypes.bool, 23 | children: childrenPropType, 24 | forceRenderTabPanel: PropTypes.bool 25 | }, 26 | 27 | childContextTypes: { 28 | forceRenderTabPanel: PropTypes.bool 29 | }, 30 | 31 | statics: { 32 | setUseDefaultStyles(use) { 33 | useDefaultStyles = use; 34 | } 35 | }, 36 | 37 | getDefaultProps() { 38 | return { 39 | selectedIndex: -1, 40 | focus: false, 41 | forceRenderTabPanel: false 42 | }; 43 | }, 44 | 45 | getInitialState() { 46 | return this.copyPropsToState(this.props); 47 | }, 48 | 49 | getChildContext() { 50 | return { 51 | forceRenderTabPanel: this.props.forceRenderTabPanel 52 | }; 53 | }, 54 | 55 | componentDidMount() { 56 | if (useDefaultStyles) { 57 | jss(require('../helpers/styles.js')); 58 | } 59 | }, 60 | 61 | componentWillReceiveProps(newProps) { 62 | this.setState(this.copyPropsToState(newProps)); 63 | }, 64 | 65 | handleClick(e) { 66 | let node = e.target; 67 | do { 68 | if (this.isTabNode(node)) { 69 | if (isTabDisabled(node)) { 70 | return; 71 | } 72 | 73 | const index = [].slice.call(node.parentNode.children).indexOf(node); 74 | this.setSelected(index); 75 | return; 76 | } 77 | } while ((node = node.parentNode) !== null); 78 | }, 79 | 80 | handleKeyDown(e) { 81 | if (this.isTabNode(e.target)) { 82 | let index = this.state.selectedIndex; 83 | let preventDefault = false; 84 | 85 | // Select next tab to the left 86 | if (e.keyCode === 37 || e.keyCode === 38) { 87 | index = this.getPrevTab(index); 88 | preventDefault = true; 89 | } 90 | // Select next tab to the right 91 | /* eslint brace-style:0 */ 92 | else if (e.keyCode === 39 || e.keyCode === 40) { 93 | index = this.getNextTab(index); 94 | preventDefault = true; 95 | } 96 | 97 | // This prevents scrollbars from moving around 98 | if (preventDefault) { 99 | e.preventDefault(); 100 | } 101 | 102 | this.setSelected(index, true); 103 | } 104 | }, 105 | 106 | setSelected(index, focus) { 107 | // Don't do anything if nothing has changed 108 | if (index === this.state.selectedIndex) return; 109 | // Check index boundary 110 | if (index < 0 || index >= this.getTabsCount()) return; 111 | 112 | // Keep reference to last index for event handler 113 | const last = this.state.selectedIndex; 114 | 115 | // Update selected index 116 | this.setState({ selectedIndex: index, focus: focus === true }); 117 | 118 | // Call change event handler 119 | if (typeof this.props.onSelect === 'function') { 120 | this.props.onSelect(index, last); 121 | } 122 | }, 123 | 124 | getNextTab(index) { 125 | const count = this.getTabsCount(); 126 | 127 | // Look for non-disabled tab from index to the last tab on the right 128 | for (let i = index + 1; i < count; i++) { 129 | const tab = this.getTab(i); 130 | if (!isTabDisabled(findDOMNode(tab))) { 131 | return i; 132 | } 133 | } 134 | 135 | // If no tab found, continue searching from first on left to index 136 | for (let i = 0; i < index; i++) { 137 | const tab = this.getTab(i); 138 | if (!isTabDisabled(findDOMNode(tab))) { 139 | return i; 140 | } 141 | } 142 | 143 | // No tabs are disabled, return index 144 | return index; 145 | }, 146 | 147 | getPrevTab(index) { 148 | let i = index; 149 | 150 | // Look for non-disabled tab from index to first tab on the left 151 | while (i--) { 152 | const tab = this.getTab(i); 153 | if (!isTabDisabled(findDOMNode(tab))) { 154 | return i; 155 | } 156 | } 157 | 158 | // If no tab found, continue searching from last tab on right to index 159 | i = this.getTabsCount(); 160 | while (i-- > index) { 161 | const tab = this.getTab(i); 162 | if (!isTabDisabled(findDOMNode(tab))) { 163 | return i; 164 | } 165 | } 166 | 167 | // No tabs are disabled, return index 168 | return index; 169 | }, 170 | 171 | getTabsCount() { 172 | return this.props.children && this.props.children[0] ? 173 | React.Children.count(this.props.children[0].props.children) : 174 | 0; 175 | }, 176 | 177 | getPanelsCount() { 178 | return React.Children.count(this.props.children.slice(1)); 179 | }, 180 | 181 | getTabList() { 182 | return this.refs.tablist; 183 | }, 184 | 185 | getTab(index) { 186 | return this.refs['tabs-' + index]; 187 | }, 188 | 189 | getPanel(index) { 190 | return this.refs['panels-' + index]; 191 | }, 192 | 193 | getChildren() { 194 | let index = 0; 195 | let count = 0; 196 | const children = this.props.children; 197 | const state = this.state; 198 | const tabIds = this.tabIds = this.tabIds || []; 199 | const panelIds = this.panelIds = this.panelIds || []; 200 | let diff = this.tabIds.length - this.getTabsCount(); 201 | 202 | // Add ids if new tabs have been added 203 | // Don't bother removing ids, just keep them in case they are added again 204 | // This is more efficient, and keeps the uuid counter under control 205 | while (diff++ < 0) { 206 | tabIds.push(uuid()); 207 | panelIds.push(uuid()); 208 | } 209 | 210 | // Map children to dynamically setup refs 211 | return React.Children.map(children, (child) => { 212 | // null happens when conditionally rendering TabPanel/Tab 213 | // see https://github.com/rackt/react-tabs/issues/37 214 | if (child === null) { 215 | return null; 216 | } 217 | 218 | let result = null; 219 | 220 | // Clone TabList and Tab components to have refs 221 | if (count++ === 0) { 222 | // TODO try setting the uuid in the "constructor" for `Tab`/`TabPanel` 223 | result = cloneElement(child, { 224 | ref: 'tablist', 225 | children: React.Children.map(child.props.children, (tab) => { 226 | // null happens when conditionally rendering TabPanel/Tab 227 | // see https://github.com/rackt/react-tabs/issues/37 228 | if (tab === null) { 229 | return null; 230 | } 231 | 232 | const ref = 'tabs-' + index; 233 | const id = tabIds[index]; 234 | const panelId = panelIds[index]; 235 | const selected = state.selectedIndex === index; 236 | const focus = selected && state.focus; 237 | 238 | index++; 239 | 240 | return cloneElement(tab, { 241 | ref, 242 | id, 243 | panelId, 244 | selected, 245 | focus 246 | }); 247 | }) 248 | }); 249 | 250 | // Reset index for panels 251 | index = 0; 252 | } 253 | // Clone TabPanel components to have refs 254 | else { 255 | const ref = 'panels-' + index; 256 | const id = panelIds[index]; 257 | const tabId = tabIds[index]; 258 | const selected = state.selectedIndex === index; 259 | 260 | index++; 261 | 262 | result = cloneElement(child, { 263 | ref, 264 | id, 265 | tabId, 266 | selected 267 | }); 268 | } 269 | 270 | return result; 271 | }); 272 | }, 273 | 274 | render() { 275 | // This fixes an issue with focus management. 276 | // 277 | // Ultimately, when focus is true, and an input has focus, 278 | // and any change on that input causes a state change/re-render, 279 | // focus gets sent back to the active tab, and input loses focus. 280 | // 281 | // Since the focus state only needs to be remembered 282 | // for the current render, we can reset it once the 283 | // render has happened. 284 | // 285 | // Don't use setState, because we don't want to re-render. 286 | // 287 | // See https://github.com/rackt/react-tabs/pull/7 288 | if (this.state.focus) { 289 | setTimeout(() => { 290 | this.state.focus = false; 291 | }, 0); 292 | } 293 | 294 | return ( 295 |
    304 | {this.getChildren()} 305 |
    306 | ); 307 | }, 308 | 309 | // Determine if a node from event.target is a Tab element 310 | isTabNode(node) { 311 | return node.nodeName === 'LI' && node.getAttribute('role') === 'tab' && node.parentElement.parentElement === findDOMNode(this); 312 | }, 313 | 314 | // This is an anti-pattern, so sue me 315 | copyPropsToState(props) { 316 | let selectedIndex = props.selectedIndex; 317 | 318 | // If no selectedIndex prop was supplied, then try 319 | // preserving the existing selectedIndex from state. 320 | // If the state has not selectedIndex, default 321 | // to the first tab in the TabList. 322 | // 323 | // TODO: Need automation testing around this 324 | // Manual testing can be done using examples/focus 325 | // See 'should preserve selectedIndex when typing' in specs/Tabs.spec.js 326 | if (selectedIndex === -1) { 327 | if (this.state && this.state.selectedIndex) { 328 | selectedIndex = this.state.selectedIndex; 329 | } else { 330 | selectedIndex = 0; 331 | } 332 | } 333 | 334 | return { 335 | selectedIndex: selectedIndex, 336 | focus: props.focus 337 | }; 338 | } 339 | }); 340 | -------------------------------------------------------------------------------- /src/patch/react-tabs/helpers/childrenPropType.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tab from '../components/Tab'; 3 | import TabList from '../components/TabList'; 4 | 5 | module.exports = function childrenPropTypes(props, propName) { 6 | let error; 7 | let tabsCount = 0; 8 | let panelsCount = 0; 9 | const children = props[propName]; 10 | 11 | React.Children.forEach(children, (child) => { 12 | // null happens when conditionally rendering TabPanel/Tab 13 | // see https://github.com/rackt/react-tabs/issues/37 14 | if (child === null) { 15 | return; 16 | } 17 | 18 | if (child.type === TabList) { 19 | React.Children.forEach(child.props.children, (c) => { 20 | // null happens when conditionally rendering TabPanel/Tab 21 | // see https://github.com/rackt/react-tabs/issues/37 22 | if (c === null) { 23 | return; 24 | } 25 | 26 | if (c.type === Tab) { 27 | tabsCount++; 28 | } else { 29 | error = new Error( 30 | 'Expected `Tab` but found `' + (c.type.displayName || c.type) + '`' 31 | ); 32 | } 33 | }); 34 | } else if (child.type.displayName === 'TabPanel') { 35 | panelsCount++; 36 | } else { 37 | error = new Error( 38 | 'Expected `TabList` or `TabPanel` but found `' + (child.type.displayName || child.type) + '`' 39 | ); 40 | } 41 | }); 42 | 43 | if (tabsCount !== panelsCount) { 44 | error = new Error( 45 | 'There should be an equal number of `Tabs` and `TabPanels`. ' + 46 | 'Received ' + tabsCount + ' `Tabs` and ' + panelsCount + ' `TabPanels`.' 47 | ); 48 | } 49 | 50 | return error; 51 | }; 52 | -------------------------------------------------------------------------------- /src/patch/react-tabs/helpers/styles.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '.react-tabs [role=tablist]': { 3 | 'border-bottom': '1px solid #aaa', 4 | 'margin': '0 0 10px', 5 | 'padding': '0' 6 | }, 7 | 8 | '.react-tabs [role=tab]': { 9 | 'display': 'inline-block', 10 | 'border': '1px solid transparent', 11 | 'border-bottom': 'none', 12 | 'bottom': '-1px', 13 | 'position': 'relative', 14 | 'list-style': 'none', 15 | 'padding': '6px 12px', 16 | 'cursor': 'pointer' 17 | }, 18 | 19 | '.react-tabs [role=tab][aria-selected=true]': { 20 | 'background': '#fff', 21 | 'border-color': '#aaa', 22 | 'color': 'black', 23 | 'border-radius': '5px 5px 0 0', 24 | '-moz-border-radius': '5px 5px 0 0', 25 | '-webkit-border-radius': '5px 5px 0 0' 26 | }, 27 | 28 | '.react-tabs [role=tab][aria-disabled=true]': { 29 | 'color': 'GrayText', 30 | 'cursor': 'default' 31 | }, 32 | 33 | '.react-tabs [role=tab]:focus': { 34 | 'box-shadow': '0 0 5px hsl(208, 99%, 50%)', 35 | 'border-color': 'hsl(208, 99%, 50%)', 36 | 'outline': 'none' 37 | }, 38 | 39 | '.react-tabs [role=tab]:focus:after': { 40 | 'content': '""', 41 | 'position': 'absolute', 42 | 'height': '5px', 43 | 'left': '-4px', 44 | 'right': '-4px', 45 | 'bottom': '-5px', 46 | 'background': '#fff' 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/patch/react-tabs/helpers/uuid.js: -------------------------------------------------------------------------------- 1 | // Get a universally unique identifier 2 | let count = 0; 3 | module.exports = function uuid() { 4 | return 'react-tabs-' + count++; 5 | }; 6 | -------------------------------------------------------------------------------- /src/patch/react-tabs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Tabs: require('./components/Tabs'), 3 | TabList: require('./components/TabList'), 4 | Tab: require('./components/Tab'), 5 | TabPanel: require('./components/TabPanel') 6 | }; 7 | -------------------------------------------------------------------------------- /src/plugins/blaze/actions/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_BLAZE_TREE, 3 | TOGGLE_BLAZE_NODE_COLLAPSED, 4 | CHANGE_BLAZE_NODE_SELECTION, 5 | CHANGE_BLAZE_NODE_HOVER 6 | } from '../constants'; 7 | import _ from 'underscore'; 8 | import Bridge from '../../../common/bridge'; 9 | 10 | export function setBlazeTreeData(node) { 11 | // unwrap nodes into a list of components with 12 | // children and parent info 13 | 14 | if (!node) { 15 | Bridge.sendMessageToThePage({ 16 | source: 'blaze-inspector', 17 | event: 'shutdown' 18 | }); 19 | return { 20 | type: SET_BLAZE_TREE, 21 | data: [] 22 | } 23 | } 24 | 25 | let nodes = {}; 26 | 27 | const unwrapNode = (node, parentId) => { 28 | const nodeId = node._id; 29 | nodes[nodeId] = { 30 | _id: nodeId, 31 | name: node.name, 32 | data: node.data, 33 | events: node.events, 34 | helpers: node.helpers, 35 | isExpanded: false, 36 | isSelected: false, 37 | isHovered: true, 38 | children: [], 39 | parentId: parentId 40 | }; 41 | _.each(node.children, (node) => { 42 | var childId = unwrapNode(node, nodeId); 43 | nodes[nodeId].children.push(childId); 44 | }); 45 | return nodeId; 46 | }; 47 | 48 | unwrapNode(node); 49 | 50 | return { 51 | type: SET_BLAZE_TREE, 52 | data: nodes 53 | } 54 | } 55 | 56 | export function toggleNodeCollapse(nodeId) { 57 | return { 58 | type: TOGGLE_BLAZE_NODE_COLLAPSED, 59 | nodeId 60 | } 61 | } 62 | 63 | export function changeBlazeNodeSelection(nodeId) { 64 | return { 65 | type: CHANGE_BLAZE_NODE_SELECTION, 66 | nodeId 67 | } 68 | } 69 | 70 | export function changeNodeHover(nodeId, isHovered) { 71 | if (!isHovered) { 72 | Bridge.sendMessageToThePage({ 73 | source: 'blaze-inspector', 74 | event: 'hide-highlight' 75 | }); 76 | } else { 77 | Bridge.sendMessageToThePage({ 78 | source: 'blaze-inspector', 79 | event: 'highlight', 80 | nodeId: nodeId 81 | }); 82 | } 83 | 84 | return { 85 | type: CHANGE_BLAZE_NODE_HOVER, 86 | nodeId, 87 | isHovered 88 | } 89 | } -------------------------------------------------------------------------------- /src/plugins/blaze/blaze.css: -------------------------------------------------------------------------------- 1 | .blaze-inspector { 2 | display: flex; 3 | flex-direction: row; 4 | width: 100%; 5 | height: 100%; 6 | -webkit-user-select: none; 7 | cursor: default; 8 | } 9 | 10 | .blaze-inspector > section { 11 | flex:3; 12 | overflow: scroll; 13 | font-size: 11px !important; 14 | font-family: Menlo, monospace; 15 | line-height: 16px; 16 | overflow-y: scroll; 17 | } 18 | 19 | .blaze-inspector > section > header { 20 | font-family: 'Lucida Grande', sans-serif !important; 21 | font-size: 12px !important; 22 | } 23 | 24 | .blaze-inspector .blaze-tree { 25 | margin-top: 6px; 26 | } 27 | 28 | .blaze-inspector > section .collapse-toggler { 29 | margin-right: 3px; 30 | } 31 | 32 | .blaze-inspector > section .tag-wrap { 33 | color: rgb(168, 148, 166); 34 | padding: 1px 0 1px 14px; 35 | } 36 | 37 | .blaze-inspector > section .tag-wrap-closing { 38 | color: rgb(168, 148, 166); 39 | padding: 1px 0 1px 1px; 40 | } 41 | 42 | .blaze-inspector > section .tag-name { 43 | color: rgb(136, 18, 128); 44 | } 45 | 46 | .blaze-inspector > aside { 47 | font-family: 'Lucida Grande', sans-serif; 48 | font-size: 12px; 49 | flex:1; 50 | max-width: 25%; 51 | overflow: hidden; 52 | border-left: solid 1px #ccc; 53 | overflow-y: scroll; 54 | } 55 | 56 | .blaze-inspector > aside .treeview-wrapper { 57 | margin-left: -10px; 58 | margin-top: -5px; 59 | } 60 | 61 | .blaze-inspector > aside .section-separator { 62 | background-color: #ddd; 63 | padding: 2px 5px; 64 | border-top: 1px solid #ccc; 65 | border-bottom: 1px solid #ccc; 66 | color: rgb(50, 50, 50); 67 | white-space: nowrap; 68 | text-overflow: ellipsis; 69 | overflow: hidden; 70 | line-height: 16px; 71 | } 72 | 73 | .blaze-inspector > aside ul.stats { 74 | list-style: none; 75 | padding: 0; 76 | margin: 0; 77 | } 78 | 79 | .blaze-inspector > aside ul.stats li { 80 | color: rgb(200, 0, 0); 81 | padding: 5px; 82 | } 83 | 84 | .blaze-inspector > aside ul.stats li:nth-child(odd) { 85 | background-color: #F5F5F5; 86 | } 87 | 88 | .blaze-inspector > section .tag-name.selected-node { 89 | color: #fff; 90 | } 91 | 92 | .blaze-inspector .no-blaze { 93 | font-family: 'Lucida Grande', sans-serif !important; 94 | font-size: 12px !important; 95 | margin: 0.5em; 96 | } -------------------------------------------------------------------------------- /src/plugins/blaze/components/props/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import JSONTree from 'react-json-tree'; 4 | 5 | export default React.createClass({ 6 | render () { 7 | if(this.props.properties) { 8 | let getArrowStyle = (type, expanded) => ({ marginTop: 4 }); 9 | const data = this.props.properties.data; 10 | const events = this.props.properties.events; 11 | const helpers = this.props.properties.helpers; 12 | 13 | return
    14 | { data && data.size !== 0 ? ( 15 |
    16 |
    Data
    17 |
    18 | 19 |
    20 |
    21 | ) : ''} 22 | { events && events.size !== 0 ? ( 23 |
    24 |
    Events
    25 |
      {events.map((e) =>
    • {e}
    • )}
    26 |
    27 | ) : ''} 28 | { helpers && helpers.size !== 0 ? ( 29 |
    30 |
    Helpers
    31 |
      {helpers.map((h) =>
    • {h}
    • )}
    32 |
    33 | ) : ''} 34 |
    ; 35 | } else { 36 | return
    ; 37 | } 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/plugins/blaze/components/tree/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import _ from 'underscore'; 4 | import NodeType from './node-prop-type'; 5 | import Node from './node'; 6 | 7 | export default React.createClass({ 8 | render (){ 9 | if(this.props.rootNode) { 10 | return ( 11 |
    12 | 18 |
    19 | ); 20 | } else { 21 | return ( 22 |
    No nodes found
    23 | ); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /src/plugins/blaze/components/tree/node-prop-type.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NodeType = React.PropTypes.shape({ 4 | _id: React.PropTypes.string.isRequired, 5 | name: React.PropTypes.string.isRequired, 6 | data: React.PropTypes.object, 7 | isExpanded: React.PropTypes.bool.isRequired, 8 | children: React.PropTypes.arrayOf(NodeType), 9 | }).isRequired; 10 | 11 | export default NodeType; 12 | -------------------------------------------------------------------------------- /src/plugins/blaze/components/tree/node.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import _ from 'underscore'; 4 | import NodeType from './node-prop-type'; 5 | 6 | const Node = React.createClass({ 7 | selectedNodeStyle : { 8 | backgroundColor: 'rgb(56,121,217)' 9 | }, 10 | 11 | hoveredNodeStyle : { 12 | backgroundColor: 'rgba(56,121,217,0.1)' 13 | }, 14 | 15 | getPaddingStyle () { 16 | return {paddingLeft: `${3*this.props.depth}px`}; 17 | }, 18 | 19 | getStyles (openingTag) { 20 | return { 21 | display: this.props.node.get('isExpanded') ? 'block' : 'inline-block' 22 | }; 23 | }, 24 | 25 | shouldComponentUpdate (nextProps) { 26 | return nextProps !== this.props; 27 | }, 28 | 29 | isSelected () { 30 | return this.props.node.get('isSelected'); 31 | }, 32 | 33 | isHovered () { 34 | return this.props.node.get('isHovered'); 35 | }, 36 | 37 | isExpanded () { 38 | return this.props.node.get('isExpanded'); 39 | }, 40 | 41 | selectedNodeClassName () { 42 | return this.isSelected() ? 'selected-node' : ''; 43 | }, 44 | 45 | nodeOpeningTagContent () { 46 | return `${this.props.node.get('name')}`; 47 | }, 48 | 49 | nodeClosingTagContent () { 50 | return `/${this.props.node.get('name')}`; 51 | }, 52 | 53 | onHover (isHovered) { 54 | this.props.onHover(this.props.node.get('_id'), isHovered); 55 | }, 56 | 57 | changeSelection () { 58 | this.props.changeBlazeNodeSelection(this.props.node.get('_id')); 59 | }, 60 | 61 | renderEmptyNode () { 62 | let styles = this.getPaddingStyle(); 63 | if (this.isSelected()) { 64 | styles = _.extend({}, styles, this.selectedNodeStyle); 65 | } else { 66 | if (this.isHovered()) { 67 | styles = _.extend({}, styles, this.hoveredNodeStyle); 68 | } 69 | } 70 | 71 | return ( 72 |
    this.onHover(true)} 73 | onMouseOut={() => this.onHover(false)}> 74 |
    75 | < 76 | {this.nodeOpeningTagContent()}> 77 |
    78 |
    79 | < 80 | {this.nodeClosingTagContent()}> 81 |
    82 |
    83 | ); 84 | }, 85 | 86 | render () { 87 | const toggleCollapse = (e) => { 88 | e.stopPropagation(); 89 | this.props.onToggleCollapse(this.props.node.get('_id')); 90 | 91 | // XX: unhover all the children because they seem to have hover 92 | // stuck to them 93 | const kids = this.props.getChildNodes(this.props.node.get('_id')); 94 | kids.forEach((child) => { 95 | this.props.onHover(child.get('_id'), false); 96 | }); 97 | } 98 | 99 | const childNodes = this.props.getChildNodes(this.props.node.get('_id')); 100 | const hasChildren = childNodes.length !== 0; 101 | const expansionToggler = ( 102 | 103 | { this.isExpanded() ? 104 | : 105 | } 106 | 107 | ); 108 | 109 | // empty node 110 | if (!hasChildren) { 111 | return this.renderEmptyNode(); 112 | } else { 113 | let tagWrapperStyle = this.getPaddingStyle(); 114 | let openingTagStyles = this.getStyles(true); 115 | 116 | if (this.isSelected()) { 117 | if (!this.isExpanded()) { 118 | tagWrapperStyle = _.extend({}, tagWrapperStyle, this.selectedNodeStyle); 119 | } else { 120 | openingTagStyles = _.extend({}, openingTagStyles, this.selectedNodeStyle); 121 | } 122 | } else { 123 | if (this.isHovered()) { 124 | if (this.isExpanded()) { 125 | openingTagStyles = _.extend({}, openingTagStyles, this.hoveredNodeStyle); 126 | } else { 127 | tagWrapperStyle = _.extend({}, tagWrapperStyle, this.hoveredNodeStyle); 128 | } 129 | } 130 | } 131 | 132 | return ( 133 |
    { 134 | // XX: only handle this if the node is collapsed 135 | !this.isExpanded() && this.onHover(true); 136 | }} onMouseOut={() => { 137 | // XX: only handle this if the node is collapsed 138 | !this.isExpanded() && this.onHover(false); 139 | }}> 140 |
    this.onHover(true)} onMouseOut={() => this.onHover(false)} 142 | onClick={this.changeSelection} onDoubleClick={toggleCollapse}> 143 | {expansionToggler} 144 | < 145 | {this.nodeOpeningTagContent()}> 146 |
    147 | { 148 | this.isExpanded() ? childNodes.map(node => ( 149 | 154 | )) : 155 | } 156 |
    !this.isExpanded() && this.changeSelection() } 158 | onDoubleClick={(e) => !this.isExpanded() && toggleCollapse(e) }> 159 | < 160 | {this.nodeClosingTagContent()}> 161 |
    162 |
    163 | ); 164 | } 165 | } 166 | }); 167 | 168 | export default Node; -------------------------------------------------------------------------------- /src/plugins/blaze/constants/index.js: -------------------------------------------------------------------------------- 1 | export const SET_BLAZE_TREE = 'set_blaze_tree'; 2 | export const TOGGLE_BLAZE_NODE_COLLAPSED = 'toggle_blaze_node_collapsed'; 3 | export const CHANGE_BLAZE_NODE_SELECTION = 'change_blaze_node_selection'; 4 | export const CHANGE_BLAZE_NODE_HOVER = 'change_blaze_node_hover'; -------------------------------------------------------------------------------- /src/plugins/blaze/highlighter/index.js: -------------------------------------------------------------------------------- 1 | var Overlay = require('./overlay'); 2 | var MultiOverlay = require('./multi-overlay'); 3 | 4 | /** 5 | * Manages the highlighting of items on an html page, as well as 6 | * hover-to-inspect. 7 | */ 8 | class Highlighter { 9 | // _overlay: ?Overlay; 10 | // _multiOverlay: ?MultiOverlay; 11 | // _win: Object; 12 | // _onSelect: (node: DOMNode) => void; 13 | // _inspecting: boolean; 14 | // _subs: Array<() => void>; 15 | // _button: DOMNode; 16 | 17 | constructor(win, onSelect) { 18 | this._win = win; 19 | this._onSelect = onSelect; 20 | this._overlay = null; 21 | this._multiOverlay = null; 22 | this._subs = []; 23 | } 24 | 25 | startInspecting() { 26 | this._inspecting = true; 27 | this._subs = [ 28 | //captureSubscription(this._win, 'mouseover', this.onHover.bind(this)), 29 | captureSubscription(this._win, 'mousedown', this.onMouseDown.bind(this)), 30 | captureSubscription(this._win, 'click', this.onClick.bind(this)), 31 | ]; 32 | } 33 | 34 | stopInspecting() { 35 | this._subs.forEach(unsub => unsub()); 36 | this.hideHighlight(); 37 | } 38 | 39 | remove() { 40 | this.stopInspecting(); 41 | if (this._button && this._button.parentNode) { 42 | this._button.parentNode.removeChild(this._button); 43 | } 44 | } 45 | 46 | highlight(node, name) { 47 | this.removeMultiOverlay(); 48 | if (!this._overlay) { 49 | this._overlay = new Overlay(this._win); 50 | } 51 | this._overlay.inspect(node, name); 52 | } 53 | 54 | highlightMany(nodes) { 55 | this.removeOverlay(); 56 | if (!this._multiOverlay) { 57 | this._multiOverlay = new MultiOverlay(this._win); 58 | } 59 | this._multiOverlay.highlightMany(nodes); 60 | } 61 | 62 | hideHighlight() { 63 | this._inspecting = false; 64 | this.removeOverlay(); 65 | this.removeMultiOverlay(); 66 | } 67 | 68 | removeOverlay() { 69 | if (!this._overlay) { 70 | return; 71 | } 72 | this._overlay.remove(); 73 | this._overlay = null; 74 | } 75 | 76 | removeMultiOverlay() { 77 | if (!this._multiOverlay) { 78 | return; 79 | } 80 | this._multiOverlay.remove(); 81 | this._multiOverlay = null; 82 | } 83 | 84 | onMouseDown(evt) { 85 | if (!this._inspecting) { 86 | return; 87 | } 88 | evt.preventDefault(); 89 | evt.stopPropagation(); 90 | evt.cancelBubble = true; 91 | this._onSelect(evt.target); 92 | return; 93 | } 94 | 95 | onClick(evt) { 96 | if (!this._inspecting) { 97 | return; 98 | } 99 | this._subs.forEach(unsub => unsub()); 100 | evt.preventDefault(); 101 | evt.stopPropagation(); 102 | evt.cancelBubble = true; 103 | this.hideHighlight(); 104 | } 105 | 106 | onHover(evt) { 107 | if (!this._inspecting) { 108 | return; 109 | } 110 | evt.preventDefault(); 111 | evt.stopPropagation(); 112 | evt.cancelBubble = true; 113 | this.highlight(evt.target); 114 | } 115 | 116 | injectButton() { 117 | this._button = makeMagnifier(); 118 | this._button.onclick = this.startInspecting.bind(this); 119 | this._win.document.body.appendChild(this._button); 120 | } 121 | } 122 | 123 | function captureSubscription(obj, evt, cb) { 124 | obj.addEventListener(evt, cb, true); 125 | return () => obj.removeEventListener(evt, cb, true); 126 | } 127 | 128 | function makeMagnifier() { 129 | var button = window.document.createElement('button'); 130 | button.innerHTML = '🔍'; 131 | button.style.backgroundColor = 'transparent'; 132 | button.style.border = 'none'; 133 | button.style.outline = 'none'; 134 | button.style.cursor = 'pointer'; 135 | button.style.position = 'fixed'; 136 | button.style.bottom = '10px'; 137 | button.style.right = '10px'; 138 | button.style.fontSize = '30px'; 139 | button.style.zIndex = 10000000; 140 | return button; 141 | } 142 | 143 | module.exports = Highlighter; -------------------------------------------------------------------------------- /src/plugins/blaze/highlighter/multi-overlay.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign'); 2 | 3 | class MultiOverlay { 4 | constructor(window) { 5 | this.win = window; 6 | var doc = window.document; 7 | this.container = doc.createElement('div'); 8 | doc.body.appendChild(this.container); 9 | } 10 | 11 | highlightMany(nodes) { 12 | this.container.innerHTML = ''; 13 | nodes.forEach(node => { 14 | var div = this.win.document.createElement('div'); 15 | var box = node.getBoundingClientRect(); 16 | assign(div.style, { 17 | top: box.top + 'px', 18 | left: box.left + 'px', 19 | width: box.width + 'px', 20 | height: box.height + 'px', 21 | border: '2px dotted rgba(200, 100, 100, .8)', 22 | boxSizing: 'border-box', 23 | backgroundColor: 'rgba(200, 100, 100, .2)', 24 | position: 'fixed', 25 | zIndex: 10000000, 26 | pointerEvents: 'none', 27 | }); 28 | this.container.appendChild(div); 29 | }); 30 | } 31 | 32 | remove() { 33 | if (this.container.parentNode) { 34 | this.container.parentNode.removeChild(this.container); 35 | } 36 | } 37 | } 38 | 39 | module.exports = MultiOverlay; -------------------------------------------------------------------------------- /src/plugins/blaze/highlighter/overlay.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign'); 2 | 3 | class Overlay { 4 | constructor(window) { 5 | var doc = window.document; 6 | this.win = window; 7 | this.container = doc.createElement('div'); 8 | this.node = doc.createElement('div'); 9 | this.border = doc.createElement('div'); 10 | this.padding = doc.createElement('div'); 11 | this.content = doc.createElement('div'); 12 | 13 | this.border.style.borderColor = overlayStyles.border; 14 | this.padding.style.borderColor = overlayStyles.padding; 15 | this.content.style.backgroundColor = overlayStyles.background; 16 | 17 | assign(this.node.style, { 18 | borderColor: overlayStyles.margin, 19 | pointerEvents: 'none', 20 | position: 'fixed', 21 | }); 22 | 23 | this.tip = doc.createElement('div'); 24 | assign(this.tip.style, { 25 | border: '1px solid #aaa', 26 | backgroundColor: 'rgb(255, 255, 178)', 27 | fontFamily: 'sans-serif', 28 | color: 'orange', 29 | padding: '3px 5px', 30 | position: 'fixed', 31 | fontSize: '10px', 32 | }); 33 | 34 | this.nameSpan = doc.createElement('span'); 35 | this.tip.appendChild(this.nameSpan); 36 | assign(this.nameSpan.style, { 37 | color: 'rgb(136, 18, 128)', 38 | marginRight: '5px', 39 | }); 40 | this.dimSpan = doc.createElement('span'); 41 | this.tip.appendChild(this.dimSpan); 42 | assign(this.dimSpan.style, { 43 | color: '#888', 44 | }); 45 | 46 | this.container.style.zIndex = 10000000; 47 | this.node.style.zIndex = 10000000; 48 | this.tip.style.zIndex = 10000000; 49 | this.container.appendChild(this.node); 50 | this.container.appendChild(this.tip); 51 | this.node.appendChild(this.border); 52 | this.border.appendChild(this.padding); 53 | this.padding.appendChild(this.content); 54 | doc.body.appendChild(this.container); 55 | } 56 | 57 | remove() { 58 | if (this.container.parentNode) { 59 | this.container.parentNode.removeChild(this.container); 60 | } 61 | } 62 | 63 | inspect(node, name) { 64 | var box = node.getBoundingClientRect(); 65 | var dims = getElementDimensions(node); 66 | 67 | boxWrap(dims, 'margin', this.node); 68 | boxWrap(dims, 'border', this.border); 69 | boxWrap(dims, 'padding', this.padding); 70 | 71 | assign(this.content.style, { 72 | height: box.height - dims.borderTop - dims.borderBottom - dims.paddingTop - dims.paddingBottom + 'px', 73 | width: box.width - dims.borderLeft - dims.borderRight - dims.paddingLeft - dims.paddingRight + 'px', 74 | }); 75 | 76 | assign(this.node.style, { 77 | top: box.top - dims.marginTop + 'px', 78 | left: box.left - dims.marginLeft + 'px', 79 | }); 80 | 81 | this.nameSpan.textContent = (name || node.nodeName.toLowerCase()); 82 | this.dimSpan.textContent = box.width + 'px × ' + box.height + 'px'; 83 | 84 | var tipPos = findTipPos({ 85 | top: box.top - dims.marginTop, 86 | left: box.left - dims.marginLeft, 87 | height: box.height + dims.marginTop + dims.marginBottom, 88 | width: box.width + dims.marginLeft + dims.marginRight, 89 | }, this.win); 90 | assign(this.tip.style, tipPos); 91 | } 92 | } 93 | 94 | function findTipPos(dims, win) { 95 | var tipHeight = 20; 96 | var margin = 5; 97 | var top; 98 | if (dims.top + dims.height + tipHeight <= win.innerHeight) { 99 | if (dims.top + dims.height < 0) { 100 | top = margin; 101 | } else { 102 | top = dims.top + dims.height + margin; 103 | } 104 | } else if (dims.top - tipHeight <= win.innerHeight) { 105 | if (dims.top - tipHeight - margin < margin) { 106 | top = margin; 107 | } else { 108 | top = dims.top - tipHeight - margin; 109 | } 110 | } else { 111 | top = win.innerHeight - tipHeight - margin; 112 | } 113 | 114 | top += 'px'; 115 | 116 | if (dims.left < 0) { 117 | return {top, left: margin}; 118 | } 119 | if (dims.left + 200 > win.innerWidth) { 120 | return {top, right: margin}; 121 | } 122 | return {top, left: dims.left + margin + 'px'}; 123 | } 124 | 125 | function getElementDimensions(element) { 126 | var calculatedStyle = window.getComputedStyle(element); 127 | 128 | return { 129 | borderLeft: +calculatedStyle.borderLeftWidth.match(/[0-9]*/)[0], 130 | borderRight: +calculatedStyle.borderRightWidth.match(/[0-9]*/)[0], 131 | borderTop: +calculatedStyle.borderTopWidth.match(/[0-9]*/)[0], 132 | borderBottom: +calculatedStyle.borderBottomWidth.match(/[0-9]*/)[0], 133 | marginLeft: +calculatedStyle.marginLeft.match(/[0-9]*/)[0], 134 | marginRight: +calculatedStyle.marginRight.match(/[0-9]*/)[0], 135 | marginTop: +calculatedStyle.marginTop.match(/[0-9]*/)[0], 136 | marginBottom: +calculatedStyle.marginBottom.match(/[0-9]*/)[0], 137 | paddingLeft: +calculatedStyle.paddingLeft.match(/[0-9]*/)[0], 138 | paddingRight: +calculatedStyle.paddingRight.match(/[0-9]*/)[0], 139 | paddingTop: +calculatedStyle.paddingTop.match(/[0-9]*/)[0], 140 | paddingBottom: +calculatedStyle.paddingBottom.match(/[0-9]*/)[0], 141 | }; 142 | } 143 | 144 | function boxWrap(dims, what, node) { 145 | assign(node.style, { 146 | borderTopWidth: dims[what + 'Top'] + 'px', 147 | borderLeftWidth: dims[what + 'Left'] + 'px', 148 | borderRightWidth: dims[what + 'Right'] + 'px', 149 | borderBottomWidth: dims[what + 'Bottom'] + 'px', 150 | borderStyle: 'solid', 151 | }); 152 | } 153 | 154 | var overlayStyles = { 155 | background: 'rgba(120, 170, 210, 0.7)', 156 | padding: 'rgba(77, 200, 0, 0.3)', 157 | margin: 'rgba(255, 155, 0, 0.3)', 158 | border: 'rgba(255, 200, 50, 0.3)', 159 | }; 160 | 161 | module.exports = Overlay; -------------------------------------------------------------------------------- /src/plugins/blaze/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux' 3 | import Bridge from '../../common/bridge' 4 | import { 5 | setBlazeTreeData, 6 | toggleNodeCollapse, 7 | changeBlazeNodeSelection, 8 | changeNodeHover 9 | } from './actions' 10 | import BlazeTreeView from './components/tree' 11 | import PropertiesView from './components/props' 12 | import _ from 'underscore'; 13 | import './blaze.css'; 14 | 15 | let dispatch = null; 16 | 17 | const onNewMessage = (error, message) => { 18 | if(message && message.eventType === 'blaze-tree') { 19 | dispatch(setBlazeTreeData(JSON.parse(message.data))); 20 | Bridge.sendMessageToThePage({ 21 | source: 'blaze-inspector', 22 | event: 'start-inspecting' 23 | }); 24 | } 25 | }; 26 | 27 | const onPageReload = () => { 28 | dispatch(setBlazeTreeData(null)); 29 | }; 30 | 31 | class App extends Component { 32 | componentDidMount() { 33 | dispatch = this.props.dispatch; 34 | 35 | if(chrome && chrome.devtools) { 36 | Bridge.registerMessageCallback(onNewMessage); 37 | Bridge.registerPageReloadCallback(onPageReload); 38 | 39 | Bridge.sendMessageToThePage({ 40 | source: 'blaze-inspector', 41 | event: 'get-blaze-data' 42 | }); 43 | } else { 44 | var fakeBlazeTree = require('./fake'); 45 | onNewMessage.call(this, null, { 46 | eventType: 'blaze-tree', 47 | data: JSON.stringify(fakeBlazeTree) 48 | }); 49 | } 50 | } 51 | 52 | componentWillUnmount() { 53 | Bridge.removeMessageCallback(onNewMessage); 54 | Bridge.removePageReloadCallback(onPageReload); 55 | } 56 | 57 | render() { 58 | const { dispatch, filters, traces, stats } = this.props 59 | const changeNodeSelection = (nodeId) => { 60 | dispatch(changeBlazeNodeSelection(nodeId)); 61 | } 62 | const rootNode = this.props.getRootNode(); 63 | 64 | return ( 65 |
    66 |
    67 | { rootNode ? 68 | dispatch(toggleNodeCollapse(nodeId))} 72 | onHover={(nodeId, isHovered) => dispatch(changeNodeHover(nodeId, isHovered)) }/> 73 | :
    Looking for signs of Blaze...
    74 | } 75 |
    76 | 79 |
    80 | ) 81 | } 82 | } 83 | 84 | export default connect((state) => { 85 | return { 86 | blazeTree: state.blazeTree, 87 | getRootNode : () => { 88 | let rootNodeId = null; 89 | state.blazeTree.forEach((value, key) => { 90 | if (!value.get('parentId')) { 91 | rootNodeId = key; 92 | } 93 | }); 94 | return rootNodeId && state.blazeTree.get(rootNodeId); 95 | }, 96 | getChildNodes : (nodeId) => { 97 | let children = []; 98 | const theNode = state.blazeTree.get(nodeId); 99 | theNode.get('children').forEach((childId) => { 100 | children.push(state.blazeTree.get(childId)); 101 | }); 102 | return children; 103 | }, 104 | getSelectedNodeProps : () => { 105 | let selectedNode = null; 106 | state.blazeTree.forEach((value, key) => { 107 | if (value.get('isSelected')) { 108 | selectedNode = value; 109 | } 110 | }); 111 | return selectedNode && { 112 | data: selectedNode.get('data'), 113 | events: selectedNode.get('events'), 114 | helpers: selectedNode.get('helpers') 115 | }; 116 | } 117 | }; 118 | })(App) -------------------------------------------------------------------------------- /src/plugins/blaze/inject.js: -------------------------------------------------------------------------------- 1 | import Highlighter from './highlighter'; 2 | 3 | var hl; 4 | 5 | const __getBlazeData = (callback) => { 6 | if (typeof Blaze === 'undefined') { 7 | return; 8 | } 9 | 10 | var idCnt = 0; 11 | 12 | var generateId = function(){ 13 | idCnt++; 14 | return 'node-' + idCnt; 15 | }; 16 | 17 | var cleanupData = function(data){ 18 | if(!data){ 19 | return data; 20 | } 21 | 22 | var d = {}; 23 | var keys = Object.getOwnPropertyNames(data); 24 | 25 | for(var i=0; i { 110 | __talkToExtension = talkToExtension; 111 | // XX: wait a little bit to make sure all the jazz 112 | // has been rendered 113 | setTimeout(() => __getBlazeData(talkToExtension), 2000); 114 | }, 115 | 116 | onMessage: (message) => { 117 | if(message.source !== 'blaze-inspector'){ 118 | return; 119 | } 120 | 121 | switch(message.event){ 122 | case 'get-blaze-data': 123 | __talkToExtension && __getBlazeData(__talkToExtension); 124 | break; 125 | case 'shutdown': 126 | hl && hl.remove(); 127 | hl = null; 128 | break; 129 | case 'start-inspecting': 130 | hl = new Highlighter(window, node => { /*agent.selectFromDOMNode(node); */ }); 131 | hl && hl.startInspecting(); 132 | break; 133 | case 'hide-highlight': 134 | hl && hl.hideHighlight(); 135 | break; 136 | case 'highlight': 137 | hl && hl.highlight( 138 | document.querySelector('[data-blaze-inspector-id=' + message.nodeId + ']'), 139 | 'element' 140 | ); 141 | break; 142 | default: 143 | return; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/plugins/blaze/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_BLAZE_TREE, 3 | TOGGLE_BLAZE_NODE_COLLAPSED, 4 | CHANGE_BLAZE_NODE_SELECTION, 5 | CHANGE_BLAZE_NODE_HOVER 6 | } from '../constants' 7 | import Immutable from 'immutable' 8 | 9 | export default { 10 | blazeTree (state = Immutable.fromJS({}), action) { 11 | switch(action.type){ 12 | case SET_BLAZE_TREE: 13 | return Immutable.fromJS(action.data); 14 | case TOGGLE_BLAZE_NODE_COLLAPSED: 15 | return state.updateIn([action.nodeId, 'isExpanded'], ie => !ie); 16 | case CHANGE_BLAZE_NODE_SELECTION: 17 | // get currently selected blaze node 18 | let currentlySelectedNode = null; 19 | state.forEach((node, nodeId) => { 20 | if (node.get('isSelected')) { 21 | currentlySelectedNode = node; 22 | } 23 | }); 24 | return state.withMutations((nodes) => { 25 | if (currentlySelectedNode) { 26 | nodes.updateIn([currentlySelectedNode.get('_id'), 'isSelected'], 27 | selected => false); 28 | } 29 | nodes.updateIn([action.nodeId, 'isSelected'], 30 | selected => true); 31 | }); 32 | case CHANGE_BLAZE_NODE_HOVER: 33 | return state.updateIn([action.nodeId,'isHovered'], ih => action.isHovered); 34 | default: 35 | return state; 36 | } 37 | } 38 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/__tests__/ddp-message-generator.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/ddp-generator'); 2 | jest.dontMock('underscore'); 3 | 4 | const _ = require('underscore'); 5 | const DDPMessageGenerator = require('../lib/ddp-generator'); 6 | 7 | let checkMessageFormat = (msg, predicate, numberOfMessages) => { 8 | if(numberOfMessages){ 9 | expect(_.isArray(msg)).toBeTruthy(); 10 | expect(msg.length).toEqual(numberOfMessages); 11 | _.each(msg, (m) => { 12 | expect(predicate.call(this,m)).toBeTruthy(); 13 | }); 14 | } else { 15 | expect(msg).toBeDefined(); 16 | expect(predicate.call(this,msg)).toBeTruthy( 17 | `got ${msg}` 18 | ); 19 | } 20 | }; 21 | 22 | const messageChecks = { 23 | changed(m){ 24 | return m.msg === 'changed' && 25 | m.collection && _.isString(m.collection) && 26 | _.isString(m.id) && _.isObject(m.fields); 27 | }, 28 | added(m){ 29 | return m.msg === 'added' && 30 | _.isString(m.collection) && 31 | _.isString(m.id) && _.isObject(m.fields); 32 | }, 33 | removed(m){ 34 | return m.msg === 'removed' && 35 | _.isString(m.collection) && _.isString(m.id); 36 | }, 37 | sub(m){ 38 | return m.msg === 'sub' && 39 | _.isString(m.id) && _.isString(m.name) && 40 | _.isArray(m.params); 41 | }, 42 | ready(m){ 43 | return m.msg === 'ready' && _.isArray(m.subs); 44 | }, 45 | method(m){ 46 | return m.msg === 'method' && _.isString(m.method) && 47 | _.isArray(m.params) && _.isString(m.id); 48 | }, 49 | updated(m){ 50 | return m.msg === 'updated' && _.isArray(m.methods); 51 | }, 52 | connect(m){ 53 | return m.msg === 'connect' && _.isString(m.version) && 54 | _.isArray(m.support); 55 | }, 56 | result(m){ 57 | return m.msg === 'result' && _.isString(m.id) && 58 | _.isObject(m.result); 59 | }, 60 | resultWithError(m){ 61 | return m.msg === 'result' && _.isString(m.id) && 62 | _.isObject(m.error); 63 | } 64 | }; 65 | 66 | describe('DDPMessageGenerator', () => { 67 | it('is defined', () => { 68 | expect(DDPMessageGenerator).toBeDefined(); 69 | }); 70 | 71 | it('has a method generate', () => { 72 | expect(DDPMessageGenerator.generate).toBeDefined(); 73 | }); 74 | 75 | it('has an object DDPMessages', () => { 76 | expect(DDPMessageGenerator.DDPMessages).toBeDefined(); 77 | expect(_.isObject(DDPMessageGenerator.DDPMessages)).toBeTruthy(); 78 | }); 79 | }); 80 | 81 | describe('DDPMessageGenerator.generate', () => { 82 | it('returns a random json message string', () => { 83 | let msg = DDPMessageGenerator.generate(); 84 | expect(msg).toBeDefined(); 85 | expect(_.isObject(msg)).toBeTruthy(); 86 | }); 87 | 88 | it('can generate ping message', () => { 89 | let msg = DDPMessageGenerator.generate({ 90 | type : 'ping' 91 | }); 92 | expect(JSON.stringify(msg)).toMatch(/{"msg":"ping"}/ig); 93 | }); 94 | 95 | it('can generate pong message', () => { 96 | let msg = DDPMessageGenerator.generate({ 97 | type : 'pong' 98 | }); 99 | expect(JSON.stringify(msg)).toMatch(/{"msg":"pong"}/ig); 100 | }); 101 | 102 | it('can generate changed message', () => { 103 | let msg = DDPMessageGenerator.generate({ 104 | type : 'changed' 105 | }); 106 | checkMessageFormat(msg, messageChecks.changed); 107 | 108 | let numberOfMessages = _.random(2,5); 109 | let msgs = DDPMessageGenerator.generate({ 110 | type : 'changed', 111 | numberOfMessages : numberOfMessages 112 | }); 113 | 114 | checkMessageFormat(msgs, messageChecks.changed, numberOfMessages); 115 | expect( 116 | _.uniq(_.pluck(msgs,'collection')).length === 1 117 | ).toBeTruthy('collection name is not consistent'); 118 | }); 119 | 120 | it('can generate added message', () => { 121 | let msg = DDPMessageGenerator.generate({ 122 | type : 'added' 123 | }); 124 | checkMessageFormat(msg, messageChecks.added); 125 | 126 | let numberOfMessages = _.random(2,5); 127 | let msgs = DDPMessageGenerator.generate({ 128 | type : 'added', 129 | numberOfMessages : numberOfMessages 130 | }); 131 | 132 | checkMessageFormat(msgs, messageChecks.added, numberOfMessages); 133 | expect( 134 | _.uniq(_.pluck(msgs,'collection')).length === 1 135 | ).toBeTruthy('collection name is not consistent'); 136 | }); 137 | 138 | it('can generate removed message', () => { 139 | let msg = DDPMessageGenerator.generate({ 140 | type : 'removed' 141 | }); 142 | checkMessageFormat(msg, messageChecks.removed); 143 | 144 | let numberOfMessages = _.random(2,5); 145 | let msgs = DDPMessageGenerator.generate({ 146 | type : 'removed', 147 | numberOfMessages : numberOfMessages 148 | }); 149 | 150 | checkMessageFormat(msgs, messageChecks.removed, numberOfMessages); 151 | expect( 152 | _.uniq(_.pluck(msgs,'collection')).length === 1 153 | ).toBeTruthy('collection name is not consistent'); 154 | }); 155 | 156 | it('can generate sub message', () => { 157 | let msg = DDPMessageGenerator.generate({ 158 | type : 'sub' 159 | }); 160 | checkMessageFormat(msg, messageChecks.sub); 161 | }); 162 | 163 | it('can generate ready message', () => { 164 | let msg = DDPMessageGenerator.generate({ 165 | type : 'ready' 166 | }); 167 | checkMessageFormat(msg, messageChecks.ready); 168 | }); 169 | 170 | it('can generate method message', () => { 171 | let msg = DDPMessageGenerator.generate({ 172 | type : 'method' 173 | }); 174 | checkMessageFormat(msg, messageChecks.method); 175 | }); 176 | 177 | it('can generate updated message', () => { 178 | let msg = DDPMessageGenerator.generate({ 179 | type : 'updated' 180 | }); 181 | checkMessageFormat(msg, messageChecks.updated); 182 | }); 183 | 184 | it('can generate connect message', () => { 185 | let msg = DDPMessageGenerator.generate({ 186 | type : 'connect' 187 | }); 188 | checkMessageFormat(msg, messageChecks.connect); 189 | }); 190 | 191 | it('can generate result message', () => { 192 | let msg = DDPMessageGenerator.generate({ 193 | type : 'result' 194 | }); 195 | checkMessageFormat(msg, messageChecks.result); 196 | }); 197 | 198 | it('can generate resultWithError message', () => { 199 | let msg = DDPMessageGenerator.generate({ 200 | type : 'resultWithError' 201 | }); 202 | checkMessageFormat(msg, messageChecks.resultWithError); 203 | }); 204 | 205 | it('cannot generate unsupported message and complains about it', () => { 206 | expect(() => DDPMessageGenerator.generate({ type : 'not-a-valid-message' })).toThrow(); 207 | }); 208 | 209 | it('supports message attributes overrides', () => { 210 | let subs = ['1','2','3']; 211 | let id = '54'; 212 | let numberOfMessages = 5; 213 | let collectionName = 'my-collection'; 214 | 215 | let msg = DDPMessageGenerator.generate({ 216 | type : 'ready', 217 | overrides : { 218 | subs : subs 219 | } 220 | }); 221 | checkMessageFormat(msg, messageChecks.ready); 222 | expect(msg.subs).toEqual(subs); 223 | 224 | msg = DDPMessageGenerator.generate({ 225 | type : 'result', 226 | overrides : { 227 | id : id 228 | } 229 | }); 230 | checkMessageFormat(msg, messageChecks.result); 231 | expect(msg.id).toEqual(id); 232 | 233 | let msgs = DDPMessageGenerator.generate({ 234 | type : 'added', 235 | numberOfMessages : numberOfMessages, 236 | overrides : { 237 | collection : collectionName 238 | } 239 | }); 240 | 241 | checkMessageFormat(msgs, messageChecks.added, numberOfMessages); 242 | 243 | expect( 244 | _.pluck(msgs,'collection')[0] === collectionName 245 | ).toBeTruthy(); 246 | }); 247 | 248 | it('generates a random message when a spec is not given', () => { 249 | let supportedMessages = _.keys(DDPMessageGenerator.DDPMessages); 250 | let msg = DDPMessageGenerator.generate(); 251 | expect(_.contains(supportedMessages, msg.msg)).toBeTruthy(); 252 | }); 253 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/__tests__/processors.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const _ = require('underscore'); 4 | const DDPGenerator = require('../lib/ddp-generator'); 5 | const TraceProcessor = require('../lib/trace-processor'); 6 | 7 | let runProcessor = (traces, isOutbound=true) => { 8 | return TraceProcessor.processTraces( 9 | _.map(traces, (m) => { 10 | return { 11 | message : m, 12 | isOutbound : isOutbound, 13 | _id : _.uniqueId('trace'), 14 | _timestamp : _.now() 15 | }; 16 | }) 17 | ); 18 | }; 19 | 20 | let testLabel = (messages, expectedLabel) => { 21 | let actualLabel = runProcessor( 22 | _.isArray(messages) ? messages : [messages] 23 | )[0].label; 24 | 25 | expect(actualLabel).toEqual(expectedLabel); 26 | }; 27 | 28 | describe('Message Processor', () => { 29 | it('groups messages that belong to the same group', () => { 30 | let numberOfMessages = 4; 31 | let batch1 = DDPGenerator.generate({ 32 | type : 'added', numberOfMessages 33 | }); 34 | let batch2 = DDPGenerator.generate({ 35 | type : 'added', numberOfMessages 36 | }); 37 | let isOutbound = false; 38 | 39 | let runTestsOnBatch = (messages, processedTraces) => { 40 | expect(processedTraces.length).toEqual(1); 41 | expect(processedTraces[0].isOutbound).toEqual(isOutbound); 42 | expect(_.isArray(processedTraces[0].message)).toBeTruthy(); 43 | 44 | expect(processedTraces[0].message.length).toEqual(numberOfMessages); 45 | 46 | _.each(messages, (m) => { 47 | const cm = _.find(processedTraces[0].message, (i) => i.id === m.id); 48 | expect(cm).toEqual(m); 49 | }); 50 | }; 51 | 52 | _.each([batch1,batch2], (messages) => { 53 | let processedTraces = runProcessor(messages, isOutbound); 54 | runTestsOnBatch(messages, processedTraces); 55 | }); 56 | }); 57 | 58 | it('attaches correct label for **added/removed/changed** messages', () => { 59 | let numberOfMessages = 3; 60 | let labelsForMessageTypes = { 61 | added : { 62 | singleItemLabel : (i) => { 63 | return `item added to ${i.collection} collection` 64 | }, 65 | multipleItemsLabel : (is) => { 66 | return `${numberOfMessages} items added to ${is[0].collection} collection` 67 | } 68 | }, 69 | 70 | removed : { 71 | singleItemLabel : (i) => { 72 | return `item removed from ${i.collection} collection` 73 | }, 74 | multipleItemsLabel : (is) => { 75 | return `${numberOfMessages} items removed from ${is[0].collection} collection` 76 | } 77 | }, 78 | 79 | changed : { 80 | singleItemLabel : (i) => { 81 | return `item changed in ${i.collection} collection` 82 | }, 83 | multipleItemsLabel : (is) => { 84 | return `${numberOfMessages} items changed ${is[0].collection} collection` 85 | } 86 | } 87 | }; 88 | 89 | _.each(_.keys(labelsForMessageTypes), (type) => { 90 | let message = DDPGenerator.generate({ type }); 91 | let messages = DDPGenerator.generate({type, numberOfMessages}); 92 | 93 | testLabel(message, 94 | labelsForMessageTypes[type].singleItemLabel.call(this,message) 95 | ); 96 | testLabel(messages, 97 | labelsForMessageTypes[type].multipleItemsLabel.call(this,messages) 98 | ); 99 | }); 100 | }); 101 | 102 | it('attaches correct label for **ping/pong/connect/updated**', () => { 103 | _.each(['ping','pong','connect','updated'], (type) => { 104 | let message = DDPGenerator.generate({ type }); 105 | testLabel(message, type); 106 | }); 107 | }); 108 | 109 | it('attaches correct label for **sub**', () => { 110 | let message = DDPGenerator.generate({ type : 'sub' }); 111 | let params = message.params.join(', ') 112 | testLabel(message, 113 | `subscribing to ${message.name} with ${params}`); 114 | }); 115 | 116 | it('attaches correct label for **ready**', () => { 117 | let subMessage = DDPGenerator.generate({ type : 'sub' }); 118 | let message = DDPGenerator.generate({ 119 | type : 'ready', 120 | overrides : { 121 | subs : [subMessage.id] 122 | } 123 | }); 124 | testLabel([message, subMessage], 125 | `subscription ready for ${subMessage.name}`); 126 | }); 127 | 128 | it('attaches correct label for **result**', () => { 129 | let methodMessage = DDPGenerator.generate({ type : 'method' }); 130 | let message = DDPGenerator.generate({ 131 | type : 'result', 132 | overrides : { 133 | id : methodMessage.id 134 | } 135 | }); 136 | testLabel([message, methodMessage], 137 | `got result for method ${methodMessage.method}`); 138 | }); 139 | 140 | it('attaches correct label for **method**', () => { 141 | let message = DDPGenerator.generate({ type : 'method' }); 142 | let params = message.params.join(', ') 143 | testLabel(message, 144 | `calling method ${message.method} with ${params}`); 145 | }); 146 | 147 | it('attaches correct label for unknown messages', () => { 148 | let randomMessageType = 'randommessagetype'; 149 | testLabel({msg: randomMessageType}, randomMessageType); 150 | }); 151 | 152 | it('attaches operation attribute based on the msg type', () => { 153 | let keys = _.keys(_.omit(DDPGenerator.DDPMessages,'resultWithError')); 154 | _.each(keys, (type) => { 155 | let m = DDPGenerator.generate({type}); 156 | let r = runProcessor([m]); 157 | expect(r[0].operation).toEqual(type); 158 | }) 159 | }); 160 | 161 | it('attaches operation attribute correctly for grouped items', () => { 162 | let ms = DDPGenerator.generate({type: 'added', numberOfMessages: 4}); 163 | let rs = runProcessor(ms); 164 | expect(rs[0].operation).toEqual('added'); 165 | }); 166 | 167 | it('attaches request object for the ready message', () => { 168 | let subMessage = DDPGenerator.generate({ type : 'sub' }); 169 | let readyMessage = DDPGenerator.generate({ 170 | type : 'ready', 171 | overrides : { 172 | subs : [subMessage.id] 173 | } 174 | }); 175 | let rs = runProcessor([subMessage, readyMessage]); 176 | 177 | expect(rs[1].operation).toEqual('ready'); 178 | expect(rs[1].request).toBeDefined(); 179 | expect(rs[1].request).toEqual(rs[0]); 180 | }); 181 | 182 | it('attaches request object for the result message', () => { 183 | let methodMessage = DDPGenerator.generate({ type : 'method' }); 184 | let resultMessage = DDPGenerator.generate({ 185 | type : 'result', 186 | overrides : { 187 | id : methodMessage.id 188 | } 189 | }); 190 | 191 | let rs = runProcessor([methodMessage, resultMessage]); 192 | 193 | expect(rs[1].operation).toEqual('result'); 194 | expect(rs[1].request).toBeDefined(); 195 | expect(rs[1].request).toEqual(rs[0]); 196 | }); 197 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/__tests__/trace-filters.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const _ = require('underscore'); 4 | const DDPGenerator = require('../lib/ddp-generator'); 5 | const TraceProcessor = require('../lib/trace-processor'); 6 | const TraceFilter = require('../lib/trace-filter'); 7 | 8 | let runProcessor = (traces, isOutbound=true) => { 9 | return TraceProcessor.processTraces( 10 | _.map(traces, (m) => { 11 | return _.extend({ 12 | message : m, 13 | isOutbound : isOutbound, 14 | _id : _.uniqueId('trace'), 15 | _timestamp : _.now() 16 | }); 17 | }) 18 | ); 19 | }; 20 | 21 | describe('Trace Filter', () => { 22 | it('filters traces from the processed list', () => { 23 | let sub = DDPGenerator.generate({ type : 'sub' }); 24 | let ping = DDPGenerator.generate({ type : 'ping'}); 25 | // process traces before filtering 26 | let r = runProcessor([sub, ping]); 27 | 28 | let f = TraceFilter.filterTraces(r, { 29 | PingPong : { 30 | enabled : false, 31 | operations : ['ping','pong'] 32 | } 33 | }); 34 | expect(f).toContain(r[0]); 35 | expect(f).not.toContain(r[1]); 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/__tests__/warnings.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const _ = require('underscore'); 4 | const DDPGenerator = require('../lib/ddp-generator'); 5 | const TraceProcessor = require('../lib/trace-processor'); 6 | const Warnings = require('../lib/warnings'); 7 | 8 | let runProcessor = (traces, isOutbound=true) => { 9 | return TraceProcessor.processTraces( 10 | _.map(traces, (m) => { 11 | return { 12 | message : m, 13 | isOutbound : isOutbound, 14 | _id : _.uniqueId('trace'), 15 | _timestamp : _.now() 16 | }; 17 | }) 18 | ); 19 | }; 20 | 21 | describe('Warnings module', () => { 22 | it('exports checkForWarnings method', () => { 23 | expect(Warnings).toBeDefined(); 24 | expect(Warnings.checkForWarnings).toBeDefined(); 25 | expect(_.isFunction(Warnings.checkForWarnings)).toBeTruthy(); 26 | }); 27 | }); 28 | 29 | describe('User data overpublish warning', () => { 30 | it('is attached when overpublishing user data', () => { 31 | let traces = DDPGenerator.generate({ 32 | type : 'added', numberOfMessages : 5, 33 | overrides : { 34 | collection : 'users', 35 | fields : { 36 | services : { 37 | password : { 38 | bcrypt : 'bcrypt' 39 | }, 40 | resume : { 41 | loginTokens : [] 42 | } 43 | } 44 | } 45 | } 46 | }); 47 | 48 | let processedTraces = runProcessor(traces); 49 | Warnings.checkForWarnings(processedTraces); 50 | 51 | expect(processedTraces[0].warnings).toBeDefined(); 52 | expect(processedTraces[0].warnings).toEqual(['user-overpublish']); 53 | }); 54 | 55 | it('is not attached for ordinary collection subs', () => { 56 | let traces = DDPGenerator.generate({ 57 | type : 'added', numberOfMessages : 5 58 | }); 59 | 60 | let processedTraces = runProcessor(traces); 61 | Warnings.checkForWarnings(processedTraces); 62 | 63 | expect(processedTraces[0].warnings).toBeUndefined(); 64 | }); 65 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/actions/filters.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE_FILTER } from '../constants/action-types' 2 | import _ from 'underscore' 3 | import Analytics from '../../../common/analytics' 4 | 5 | export function toggleFilter(filter) { 6 | Analytics.trackEvent('ddp', 'filter:toggle', filter); 7 | return { 8 | type: TOGGLE_FILTER, 9 | filter: filter 10 | } 11 | } -------------------------------------------------------------------------------- /src/plugins/ddp/actions/traces.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/action-types' 2 | import _ from 'underscore' 3 | import Analytics from '../../../common/analytics' 4 | 5 | export function addTrace(trace){ 6 | return { 7 | type: types.NEW_TRACE, 8 | trace : { 9 | message : trace.message,//JSON.parse(trace.jsonString), 10 | isOutbound : trace.isOutbound, 11 | stackTrace : trace.stackTrace, 12 | _id : _.uniqueId('trace'), 13 | _timestamp : _.now() 14 | } 15 | } 16 | } 17 | 18 | export function clearLogs(){ 19 | Analytics.trackEvent('ddp', 'traces:clear'); 20 | return { type: types.CLEAR_LOGS } 21 | } -------------------------------------------------------------------------------- /src/plugins/ddp/components/clear-logs-button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | export default React.createClass({ 4 | propTypes : { 5 | onClearClick : PropTypes.func.isRequired 6 | }, 7 | onClick (){ 8 | this.props.onClearClick(); 9 | }, 10 | render (){ 11 | return ( 12 | 16 | ) 17 | } 18 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/components/filter.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export default React.createClass({ 5 | propTypes : { 6 | onToggle : PropTypes.func.isRequired, 7 | enabled : PropTypes.bool.isRequired, 8 | name : PropTypes.string 9 | }, 10 | onChange (event){ 11 | this.props.onToggle(this.props.name); 12 | }, 13 | render (){ 14 | let filterClass = classNames('filter', (this.props.name).toLowerCase()); 15 | let filterId = 'hide-'+(this.props.name).toLowerCase(); 16 | 17 | return ( 18 |
    19 | 20 | 21 |
    22 | ) 23 | } 24 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/components/json-tree-item.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { setCollectionSelection, setCollectionAndQuery } from '../../minimongo/actions'; 4 | import { setTabIndex } from '../../../common/actions'; 5 | import { MINIMONGO_TAB_INDEX } from '../../../common/constants'; 6 | 7 | class JSONTreeItem extends Component { 8 | 9 | _printValue () { 10 | // if data is an array, 11 | // check the first item to get collection name 12 | let data = this.props.data; 13 | if(Array.isArray(this.props.data) && this.props.data.length){ 14 | data = this.props.data[0]; 15 | } 16 | if((data.msg === 'added' || data.msg === 'changed') && 17 | (this.props.label === 'id' || this.props.label === 'collection')) { 18 | return ( 19 | 20 | "{this.props.raw}" 21 | ); 22 | } else { 23 | return ({this.props.value}); 24 | } 25 | } 26 | 27 | render () { 28 | return ({this._printValue()}); 29 | } 30 | }; 31 | 32 | JSONTreeItem.propTypes = { 33 | value: PropTypes.string.isRequired, 34 | label: PropTypes.string.isRequired, 35 | data: React.PropTypes.oneOfType([ 36 | React.PropTypes.object, 37 | React.PropTypes.array 38 | ]).isRequired, 39 | raw: PropTypes.string.isRequired, 40 | setMinimongoState: PropTypes.func.isRequired 41 | }; 42 | 43 | const mapDispatchToProps = (dispatch, ownProps) => { 44 | const query = `{ query : {"_id":"${ownProps.raw}"}, fields: {}, sort: {} }`; 45 | return { 46 | setMinimongoState: () => { 47 | // if data is an array, 48 | // check the first item to get collection name 49 | let data = ownProps.data; 50 | if(Array.isArray(ownProps.data) && ownProps.data.length){ 51 | data = ownProps.data[0]; 52 | } 53 | if(ownProps.label === 'id'){ 54 | dispatch(setCollectionAndQuery(data.collection, query)); 55 | } 56 | if(ownProps.label === 'collection'){ 57 | dispatch(setCollectionSelection(data.collection)); 58 | } 59 | } 60 | }; 61 | }; 62 | 63 | export default connect(null, mapDispatchToProps)(JSONTreeItem); 64 | -------------------------------------------------------------------------------- /src/plugins/ddp/components/stats.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import prettyBytes from 'pretty-bytes'; 3 | 4 | export default React.createClass({ 5 | propTypes : { 6 | stats: React.PropTypes.shape({ 7 | inboundMessages: React.PropTypes.number.isRequired, 8 | outboundMessages: React.PropTypes.number.isRequired, 9 | inboundMessagesSize: React.PropTypes.number.isRequired, 10 | outboundMessagesSize: React.PropTypes.number.isRequired, 11 | messageTypes: React.PropTypes.object.isRequired 12 | }).isRequired 13 | }, 14 | render (){ 15 | const inboundMessagesSize = prettyBytes(this.props.stats.inboundMessagesSize); 16 | const outboundMessagesSize = prettyBytes(this.props.stats.outboundMessagesSize); 17 | return ( 18 |
    19 | 20 |   21 | {inboundMessagesSize}  22 |   23 | 24 |   25 | {outboundMessagesSize} 26 |  |  27 | collection ops: {this.props.stats.messageTypes.collections} |  28 | method calls: {this.props.stats.messageTypes.methodCalls} |  29 | subs: {this.props.stats.messageTypes.subscriptions} 30 |
    31 | ) 32 | } 33 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/components/trace-item-prop-types.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default { 4 | data: React.PropTypes.oneOfType([ 5 | React.PropTypes.shape({ 6 | _id : React.PropTypes.string, 7 | isOutbound : React.PropTypes.bool.isRequired, 8 | stackTrace : React.PropTypes.array, 9 | message : React.PropTypes.oneOfType([ 10 | React.PropTypes.object, 11 | React.PropTypes.array 12 | ]).isRequired, 13 | label : React.PropTypes.string, 14 | operation : React.PropTypes.string, 15 | request : React.PropTypes.object, 16 | warnings : React.PropTypes.array, 17 | size : React.PropTypes.number 18 | }), 19 | React.PropTypes.array 20 | ]).isRequired 21 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/components/trace-item-tabs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import JSONTree from 'react-json-tree' 3 | import ItemPropTypes from './trace-item-prop-types' 4 | import JSONTreeItem from './json-tree-item' 5 | import {Tab, Tabs, TabList, TabPanel} from '../../../patch/react-tabs' 6 | import Bridge from '../../../common/bridge' 7 | import Warnings from './warnings' 8 | 9 | export default React.createClass({ 10 | propTypes: ItemPropTypes, 11 | 12 | _getRequestTabLabel () { 13 | const mappings = { 14 | ready : 'Subscription', 15 | result : 'Method' 16 | }; 17 | let label = mappings[this.props.data.operation]; 18 | return label ? label : 'Request' 19 | }, 20 | 21 | _renderMessage (data) { 22 | const theme = { 23 | tree: { 24 | backgroundColor: 'transparent', 25 | fontSize: '1em' 26 | }, 27 | arrow: ({ style }, type, expanded) => ({ 28 | style: Object.assign(style, { 29 | marginTop: 2, 30 | }) 31 | }), 32 | }; 33 | 34 | let valueRenderer = (value, raw, key) => { 35 | return (); 41 | }; 42 | 43 | return (); 48 | }, 49 | 50 | _renderTablist () { 51 | let l = [ 52 | Message, 53 | ]; 54 | 55 | if(this.props.data.request){ 56 | l.push({this._getRequestTabLabel()}); 57 | } 58 | 59 | if(this.props.data.stackTrace){ 60 | l.push(Stack Trace); 61 | } 62 | 63 | if(this.props.data.warnings){ 64 | l.push(⚠️ Warnings); 65 | } 66 | 67 | return l; 68 | }, 69 | 70 | _renderTabPanels () { 71 | let tb = [ 72 | 73 | {this._renderMessage(this.props.data.message)} 74 | 75 | ]; 76 | 77 | if(this.props.data.request){ 78 | tb.push( 79 | {this._renderMessage(this.props.data.request.message)} 80 | ); 81 | } 82 | 83 | if(this.props.data.stackTrace){ 84 | const stackTrace = this.props.data.stackTrace.map(function(st, i){ 85 | 86 | let shortUrl = st.fileName || ''; 87 | let functionName = st.functionName ? 88 | st.functionName : 89 | '(anonymous function)'; 90 | 91 | if(shortUrl !== ''){ 92 | let matches = shortUrl.match(/\/([^?\/]*)\?/) || 93 | shortUrl.match(/\/([^\/]*)$/); 94 | 95 | shortUrl = matches && matches.length === 2 ? 96 | matches[1] : 97 | shortUrl; 98 | } 99 | 100 | shortUrl = st.lineNumber ? `${shortUrl}:${st.lineNumber}` : shortUrl; 101 | 102 | return ( 103 | 104 | {functionName} 105 | 106 | @ { 108 | e.preventDefault(); 109 | Bridge.openResource(st.fileName, st.lineNumber); 110 | } }> 111 | {shortUrl} 112 | 113 | 114 | 115 | ); 116 | }); 117 | 118 | tb.push( 119 | 120 | 121 | 122 | {stackTrace} 123 | 124 |
    125 |
    126 | ); 127 | } 128 | 129 | if(this.props.data.warnings){ 130 | tb.push( 131 | 132 | 133 | 134 | ); 135 | } 136 | 137 | return tb; 138 | }, 139 | 140 | render () { 141 | return ( 142 | 143 | 144 | {this._renderTablist()} 145 | 146 | {this._renderTabPanels()} 147 | 148 | ); 149 | } 150 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/components/trace-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import prettyBytes from 'pretty-bytes'; 4 | import moment from 'moment'; 5 | import Helpers from '../lib/helpers'; 6 | import TraceItemTabs from './trace-item-tabs'; 7 | import ItemPropTypes from './trace-item-prop-types'; 8 | 9 | export default React.createClass({ 10 | 11 | propTypes: ItemPropTypes, 12 | 13 | getInitialState () { 14 | return { isExpanded : false }; 15 | }, 16 | 17 | onExpandToggle () { 18 | this.setState({isExpanded : !this.state.isExpanded}); 19 | }, 20 | 21 | render () { 22 | let itemClass = classNames({ 23 | outbound : this.props.data.isOutbound, 24 | inbound : !this.props.data.isOutbound, 25 | warning : typeof this.props.data.warnings !== 'undefined' 26 | }); 27 | let directionIconClass = classNames('fa', { 28 | 'fa-arrow-circle-o-up' : this.props.data.isOutbound, 29 | 'fa-arrow-circle-o-down' : !this.props.data.isOutbound 30 | }); 31 | let toggleIconClass = classNames('fa', { 32 | 'fa-minus-square-o' : this.state.isExpanded, 33 | 'fa-plus-square-o' : !this.state.isExpanded 34 | }); 35 | let tooltip = this.props.data.isOutbound ? 'Client says' : 'Server says'; 36 | let compactJSONString = Helpers.unescapeBackSlashes( 37 | Helpers.compactJSONString(JSON.stringify(this.props.data.message), 50)); 38 | let iconClass = classNames({ 39 | 'client' : this.props.data.isOutbound, 40 | 'server' : !this.props.data.isOutbound 41 | }, this.props.data.operation); 42 | let tabs = this.state.isExpanded 43 | ? 44 | : null; 45 | let timestamp = moment(this.props.data._timestamp).format('HH:mm:ss'); 46 | let prettyMessageSize = prettyBytes(this.props.data.size); 47 | 48 | return ( 49 |
  • 50 |
    51 | 52 | {this.props.data.label}  53 | {compactJSONString} 54 |   55 | 56 | 57 |  {timestamp} 58 | {prettyMessageSize} 59 | 60 |
    61 | {tabs} 62 |
  • 63 | ) 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /src/plugins/ddp/components/trace-list.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import TraceItem from './trace-item'; 3 | 4 | export default React.createClass({ 5 | propTypes : { 6 | traces : PropTypes.array.isRequired 7 | }, 8 | 9 | render () { 10 | 11 | const noData = this.props.traces.length === 0 ? 12 |
  • No traces yet...
  • : null; 13 | const items = this.props.traces.map(function(item, i){ 14 | return ; 15 | }); 16 | 17 | return ( 18 |
      19 | {items} 20 | {noData} 21 |
    22 | ) 23 | } 24 | }); -------------------------------------------------------------------------------- /src/plugins/ddp/components/warnings.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | export default React.createClass({ 4 | propTypes: { 5 | warnings : React.PropTypes.array.isRequired 6 | }, 7 | 8 | 9 | renderWarning : function(warning){ 10 | switch(warning){ 11 | case 'user-overpublish': 12 | return ( 13 |
  • 14 | This app is publishing too much data about people using it. To fix this, check out 'Don't over-publish your data' 15 | section in Security 101 by Josh Owens 16 |
  • 17 | ); 18 | case 'unknown-publication': 19 | return ( 20 |
  • 21 | You are trying to subscribe to a non-existent publication. Make sure it's been probably exposed through Meteor.publish 22 |
  • 23 | ); 24 | default: 25 | return
  • unknown warning
  • ; 26 | 27 | } 28 | }, 29 | 30 | render() { 31 | let warnings = this.props.warnings.map( (w) => { 32 | return this.renderWarning(w); 33 | }); 34 | 35 | return ( 36 |
      {warnings}
    37 | ) 38 | } 39 | }) -------------------------------------------------------------------------------- /src/plugins/ddp/constants/action-types.js: -------------------------------------------------------------------------------- 1 | export const NEW_TRACE = 'new_trace' 2 | export const CLEAR_LOGS= 'clear_logs' 3 | export const FILTER_ON= 'filter_on' 4 | export const FILTER_OFF= 'filter_off' 5 | export const TOGGLE_FILTER = 'toggle_filter' 6 | -------------------------------------------------------------------------------- /src/plugins/ddp/constants/operation-types.js: -------------------------------------------------------------------------------- 1 | const operationTypes = { 2 | 'PingPong' : ['ping','pong'], 3 | 'Subscriptions' : ['sub','unsub','nosub','ready'], 4 | 'Collections' : ['added','removed','changed'], 5 | 'Methods' : ['method','result','updated'], 6 | 'Connect' : ['connect','connected','failed'], 7 | }; 8 | 9 | export default operationTypes; -------------------------------------------------------------------------------- /src/plugins/ddp/ddp.css: -------------------------------------------------------------------------------- 1 | .ddp-monitor { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | header { 9 | background-color:#fff; 10 | border-bottom: solid 1px #ccc; 11 | } 12 | 13 | footer { 14 | height: 27px; 15 | line-height: 27px; 16 | padding-left: 5px; 17 | background-color: #eee; 18 | border-top: 1px solid #ccc; 19 | position: absolute; 20 | bottom: 0; 21 | left: 0; 22 | right: 0; 23 | } 24 | 25 | .ddp-monitor > section { 26 | flex:1; 27 | overflow: scroll; 28 | padding-bottom: 59px; 29 | } 30 | 31 | .ddp-monitor > header { 32 | display: flex; 33 | } 34 | 35 | ul.network-traces { 36 | margin:0; 37 | padding:0; 38 | font-size: 11px; 39 | } 40 | 41 | ul.network-traces > li { 42 | list-style-type:none; 43 | } 44 | 45 | ul.network-traces .trace-content, 46 | ul.network-traces .no-trace 47 | { 48 | padding: 0.5em 0.5em; 49 | } 50 | 51 | ul.network-traces > li:nth-child(even), 52 | ul.network-traces > li:nth-child(even) .ReactTabs__TabList { 53 | background-color: #f5f5f5; 54 | } 55 | 56 | ul.network-traces .react-tabs { 57 | background-color: white; 58 | } 59 | 60 | ul.network-traces > li .op-label { 61 | padding: 0.2em; 62 | word-break: break-all; 63 | color: gray; 64 | } 65 | 66 | ul.network-traces > li .op-label:hover { 67 | cursor: pointer; 68 | } 69 | 70 | ul.network-traces > li .op-label .fa { 71 | color: black; 72 | font-size: 12px; 73 | } 74 | 75 | ul.network-traces > li .message-inline-details { 76 | float: right; 77 | color: #8C8C8C; 78 | display: flex; 79 | flex-direction: column; 80 | text-align: right; 81 | font-size: 10px; 82 | line-height: 13px; 83 | } 84 | 85 | ul.network-traces > li.warning { 86 | background-color: #FFEFEF !important; 87 | border-color: #FFD8D7 !important; 88 | } 89 | 90 | .stack-trace { 91 | margin: 1em; 92 | font-size: inherit; 93 | } 94 | 95 | ul.warnings-list { 96 | margin: 0; 97 | padding: 0; 98 | } 99 | 100 | ul.warnings-list > li { 101 | list-style-type:none; 102 | padding: 0.5em; 103 | } 104 | 105 | .filter { 106 | margin: 0 5px; 107 | line-height: 26px; 108 | } 109 | 110 | .toolbar-item { 111 | position: relative; 112 | background-color: transparent; 113 | padding: 0; 114 | height: 26px; 115 | border: none; 116 | color: #5a5a5a; 117 | outline: none; 118 | } 119 | 120 | .toolbar-glyph { 121 | -webkit-mask-position: -64px 0; 122 | -webkit-mask-size: 352px 168px; 123 | background-color: #5a5a5a; 124 | width: 28px; 125 | height: 24px; 126 | } 127 | 128 | .toolbar-glyph:hover { 129 | background-color: #333; 130 | } 131 | 132 | .clear-traces > .tooltip { 133 | position: absolute; 134 | top: 28px; 135 | left: 2px; 136 | width: 137 | } 138 | 139 | .clear-traces:hover > .tooltip { 140 | opacity: 1; 141 | visibility: visible; 142 | } 143 | 144 | .tooltip { 145 | background: hsl(0, 0%, 95%); 146 | border-radius: 2px; 147 | color: hsl(0, 0%, 20%); 148 | padding: 5px 8px; 149 | font-size: 11px; 150 | line-height: 14px; 151 | align-items: center; 152 | -webkit-filter: drop-shadow(0 1px 2px hsla(0, 0%, 0%, 0.3)); 153 | border: 1px solid hsla(0, 0%, 0%, 0.1); 154 | background-clip: padding-box; 155 | box-sizing: border-box; 156 | visibility: hidden; 157 | transition: visibility 0s 100ms, opacity 150ms cubic-bezier(0, 0, .2, 1); 158 | z-index: 20001; 159 | opacity: 0; 160 | white-space: nowrap; 161 | } 162 | 163 | .collection-link { 164 | text-decoration: underline; 165 | } 166 | 167 | .collection-link:hover { 168 | cursor: pointer; 169 | } -------------------------------------------------------------------------------- /src/plugins/ddp/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux' 3 | import TraceList from './components/trace-list' 4 | import ClearLogsButton from './components/clear-logs-button' 5 | import Filter from './components/filter' 6 | import TraceFilter from './lib/trace-filter' 7 | import TraceProcessor from './lib/trace-processor' 8 | import Warnings from './lib/warnings' 9 | import {computeStats} from './lib/stats' 10 | import Stats from './components/stats' 11 | import Bridge from '../../common/bridge' 12 | import { addTrace, clearLogs } from './actions/traces' 13 | import { toggleFilter } from './actions/filters' 14 | import _ from 'underscore'; 15 | import DDPMessageGenerator from './lib/ddp-generator'; 16 | import './ddp.css'; 17 | 18 | let autoscrollToTheBottom = true; 19 | const _getScrollableSectionEl = () => { 20 | return document.querySelector('.ddp-monitor > section'); 21 | }; 22 | 23 | let dispatch = null; 24 | const onNewMessage = (error, message) => { 25 | if(message && message.eventType === 'ddp-trace'){ 26 | let data = message.data; 27 | let isValid = data && data.messageJSON && 28 | typeof data.isOutbound !== 'undefined'; 29 | 30 | if(!isValid){ 31 | return; 32 | } 33 | 34 | let d = JSON.parse(data.messageJSON); 35 | d = _.isArray(d) ? d : [d]; 36 | 37 | _.each(d, function(m){ 38 | m = _.isString(m) ? JSON.parse(m) : m; 39 | 40 | dispatch(addTrace({ 41 | message: m, 42 | isOutbound: data.isOutbound, 43 | stackTrace: data.stackTrace 44 | })); 45 | }); 46 | } 47 | }; 48 | 49 | const onPageReload = () => { 50 | dispatch(clearLogs()); 51 | }; 52 | 53 | class App extends Component { 54 | componentDidMount() { 55 | dispatch = this.props.dispatch; 56 | 57 | if(chrome && chrome.devtools) { 58 | Bridge.registerMessageCallback(onNewMessage); 59 | Bridge.registerPageReloadCallback(onPageReload); 60 | } else { 61 | // inside standalone web app 62 | var counter = 0; 63 | var loop = setInterval(function(){ 64 | counter++; 65 | onNewMessage.call(this, null, { 66 | eventType: 'ddp-trace', 67 | data: { 68 | isOutbound: true, 69 | messageJSON: JSON.stringify(DDPMessageGenerator.generate()) 70 | } 71 | }); 72 | if(counter > 100) { 73 | clearInterval(loop); 74 | } 75 | },1000); 76 | } 77 | } 78 | 79 | componentWillUnmount() { 80 | Bridge.removeMessageCallback(onNewMessage); 81 | Bridge.removePageReloadCallback(onPageReload); 82 | } 83 | 84 | onScroll () { 85 | const section = _getScrollableSectionEl(); 86 | autoscrollToTheBottom = (section.scrollTop + section.clientHeight) === section.scrollHeight; 87 | } 88 | 89 | componentDidUpdate () { 90 | if (autoscrollToTheBottom) { 91 | const section = _getScrollableSectionEl(); 92 | section.scrollTop = section.scrollHeight - section.clientHeight; 93 | } 94 | } 95 | 96 | render() { 97 | const { dispatch, filters, traces, stats } = this.props 98 | return ( 99 |
    100 |
    101 | dispatch(clearLogs())} /> 102 | dispatch(toggleFilter(filter)) } /> 103 | dispatch(toggleFilter(filter)) } /> 104 | dispatch(toggleFilter(filter)) } /> 105 | dispatch(toggleFilter(filter)) } /> 106 | dispatch(toggleFilter(filter)) } /> 107 |
    108 |
    109 | 110 |
    111 |
    112 | 113 |
    114 |
    115 | ) 116 | } 117 | } 118 | 119 | App.propTypes = { 120 | traces : PropTypes.array.isRequired 121 | } 122 | 123 | export default connect((state) => { 124 | const traces = TraceProcessor.processTraces(state.traces); 125 | const filteredTraces = TraceFilter.filterTraces( 126 | Warnings.checkForWarnings(traces), 127 | state.filters 128 | ); 129 | return { 130 | traces : filteredTraces, 131 | filters : state.filters, 132 | stats: computeStats(traces) 133 | }; 134 | })(App) -------------------------------------------------------------------------------- /src/plugins/ddp/inject.js: -------------------------------------------------------------------------------- 1 | import ErrorStackParser from 'error-stack-parser'; 2 | 3 | module.exports = { 4 | setup : (talkToExtension) => { 5 | var getStackTrace = function(stackTraceLimit){ 6 | var originalStackTraceLimit = Error.stackTraceLimit; 7 | try { 8 | Error.stackTraceLimit = stackTraceLimit || 15; 9 | return ErrorStackParser.parse(new Error); 10 | } finally { 11 | Error.stackTraceLimit = originalStackTraceLimit; 12 | } 13 | }; 14 | 15 | var grabStackAndTalkToExtension = function(message){ 16 | var stackTrace = getStackTrace(15); 17 | 18 | if(stackTrace && stackTrace.length !== 0){ 19 | // XX: clean up first 2 traces since they refer to 20 | // account for getStackTrace and grabStackAndTalkToExtension calls 21 | stackTrace.splice(0,2); 22 | } 23 | 24 | message.stackTrace = stackTrace; 25 | talkToExtension('ddp-trace', message); 26 | }; 27 | 28 | var oldSend = Meteor.connection._stream.send; 29 | Meteor.connection._stream.send = function(){ 30 | oldSend.apply(this, arguments); 31 | grabStackAndTalkToExtension({ 32 | messageJSON : arguments[0], 33 | isOutbound : true 34 | }); 35 | }; 36 | 37 | Meteor.connection._stream.on('message', function(){ 38 | grabStackAndTalkToExtension({ 39 | messageJSON : arguments[0], 40 | isOutbound : false 41 | }); 42 | }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/plugins/ddp/lib/ddp-generator.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | let randomObject = () => { 4 | let result = {}; 5 | _.each(_.range(0,_.random(1, 5)), () => { 6 | result[_.uniqueId('field')] = 7 | _.uniqueId('value'); 8 | }); 9 | return result; 10 | }; 11 | 12 | let randomArray = () => { 13 | return _.map(_.range(0,_.random(1, 5)), () => { 14 | return _.uniqueId('item') 15 | }); 16 | }; 17 | 18 | let generateMessages = (g, spec = {}) => { 19 | return _.map(_.range(spec.numberOfMessages || 1), function(){ 20 | return _.extend({}, g.call(this), spec.overrides || {}); 21 | }); 22 | }; 23 | 24 | module.exports = { 25 | 26 | DDPMessages : { 27 | ping : () => { 28 | return {msg:'ping'}; 29 | }, 30 | pong : () => { 31 | return {msg:'pong'}; 32 | }, 33 | changed : (spec) => { 34 | let collectionName = _.uniqueId('collection'); 35 | return generateMessages(() => { 36 | return { 37 | msg : 'changed', 38 | collection : collectionName, 39 | id : _.uniqueId('id'), 40 | fields : randomObject() 41 | }; 42 | }, spec); 43 | }, 44 | added : (spec) => { 45 | const collectionName = _.uniqueId('collection'); 46 | return generateMessages(() => { 47 | return { 48 | msg : 'added', 49 | collection : collectionName, 50 | id : _.uniqueId('id'), 51 | fields : randomObject() 52 | }; 53 | }, spec); 54 | }, 55 | removed : (spec) => { 56 | const collectionName = _.uniqueId('collection'); 57 | return generateMessages(() => { 58 | return { 59 | msg : 'removed', 60 | collection : collectionName, 61 | id : _.uniqueId('id') 62 | }; 63 | }, spec); 64 | }, 65 | sub : (spec) => { 66 | return generateMessages(() => { 67 | return { 68 | msg : 'sub', 69 | id : _.uniqueId('id'), 70 | name : _.uniqueId('sub-name'), 71 | params : randomArray() 72 | }; 73 | }, spec); 74 | }, 75 | ready : (spec) => { 76 | return generateMessages(() => { 77 | return { 78 | msg : 'ready', 79 | subs : randomArray() 80 | }; 81 | }, spec); 82 | }, 83 | method : (spec) => { 84 | return generateMessages(() => { 85 | return { 86 | msg : 'method', 87 | method : _.uniqueId('method'), 88 | params : randomArray(), 89 | id : _.uniqueId('id') 90 | }; 91 | }, spec); 92 | }, 93 | updated : (spec) => { 94 | return generateMessages(() => { 95 | return { 96 | msg : 'updated', 97 | methods : randomArray() 98 | }; 99 | }, spec); 100 | }, 101 | connect : (spec) => { 102 | return generateMessages(() => { 103 | return { 104 | msg : 'connect', 105 | version : _.uniqueId('version'), 106 | support : randomArray() 107 | }; 108 | }, spec); 109 | }, 110 | result : (spec) => { 111 | return generateMessages(() => { 112 | return { 113 | msg : 'result', 114 | id : _.uniqueId('version'), 115 | result : randomObject() 116 | }; 117 | }, spec); 118 | }, 119 | resultWithError : (spec) => { 120 | return generateMessages(() => { 121 | return { 122 | msg : 'result', 123 | id : _.uniqueId('version'), 124 | error : randomObject() 125 | }; 126 | }, spec); 127 | } 128 | }, 129 | 130 | generate : function(spec){ 131 | // message spec 132 | // { 133 | // type: , 134 | // numberOfMessages: 135 | // overrides: 136 | // } 137 | 138 | const supportedMessages = _.keys(this.DDPMessages); 139 | const type = (spec && spec.type) || 140 | supportedMessages[_.random(0,supportedMessages.length-1)]; 141 | 142 | return this.runGenerator(type, spec); 143 | }, 144 | 145 | runGenerator : function(type, spec){ 146 | const data = this.DDPMessages[type].call(this, spec); 147 | return data.length === 1 ? data[0] : data; 148 | } 149 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/helpers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | unescapeBackSlashes(str) { 3 | return str.replace(/\\"/g, '"'); 4 | }, 5 | compactJSONString(d, maxLength) { 6 | var str = JSON.stringify(d); 7 | if(str.length <= maxLength){ 8 | return str; 9 | } else { 10 | var postFix = ' ... }'; 11 | return [str.substr(0,maxLength - postFix.length), 12 | postFix].join(''); 13 | } 14 | } 15 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/processors/associations.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | class TraceAssociations { 4 | run(traces) { 5 | return _.map(traces, (t) => { 6 | if(t.operation === 'ready'){ 7 | let sub = _.find(traces, (tt) => { 8 | return tt.operation === 'sub' && 9 | _.contains(t.message.subs, tt.message.id) 10 | }); 11 | if(sub){ 12 | _.extend(t, { 13 | request : sub 14 | }); 15 | } 16 | } 17 | 18 | if(t.operation === 'result'){ 19 | let method = _.find(traces, (tt) => { 20 | return tt.operation === 'method' && 21 | tt.message.msg.id === t.message.msg.id 22 | }); 23 | 24 | if(method){ 25 | _.extend(t, { 26 | request : method 27 | }); 28 | } 29 | } 30 | 31 | return t; 32 | }); 33 | } 34 | }; 35 | 36 | export default TraceAssociations; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/processors/groups.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | class TraceGroups { 4 | run(traces) { 5 | const targetMsgs = ['added','changed','removed','updated']; 6 | const unrelated = 'unrelated'; 7 | 8 | // group traces using 9 | // traces that are not related to collection ops, go 10 | // to the 'unrelated' group 11 | let groups = _.groupBy(traces, (t) => { 12 | if(t.message.collection && _.contains(targetMsgs, t.message.msg)){ 13 | return [ 14 | t.message.collection, 15 | t.message.msg, 16 | t.isOutbound, 17 | // XX: group messages in 10 seconds batches 18 | Math.floor(t._timestamp/10000), 19 | ].join('-'); 20 | } else { 21 | return unrelated; 22 | } 23 | }); 24 | 25 | // for grouping, we are only iterested in collection 26 | // related ops with group size > 1 27 | let groupsToOmit = _.filter(_.keys(groups), (k) => { 28 | return groups[k].length === 1; 29 | }); 30 | groupsToOmit.push(unrelated); 31 | groups = _.omit(groups, groupsToOmit); 32 | 33 | _.each(_.values(groups), (g) => { 34 | // merge traces together 35 | // jsonString is a serialized list of all items 36 | // _timestamp should be set to that of the latest entry in the group 37 | let mergedTrace = _.reduce(g, (memo, item) => { 38 | let currentMessage = memo.message; 39 | currentMessage.push(item.message); 40 | return _.extend(memo, { message : currentMessage }); 41 | }, { 42 | isOutbound : _.first(g).isOutbound, 43 | _timestamp : _.first(_.sortBy(g, (t) => -t._timestamp))._timestamp, 44 | message : [] 45 | }); 46 | 47 | // remove group members from the original traces 48 | // and add the resulting merged trace to the original traces 49 | const ids = _.pluck(g,'_id') 50 | 51 | traces = _.filter(traces, (t) => { 52 | return !_.contains(ids,t._id); 53 | }); 54 | traces.push(mergedTrace); 55 | }); 56 | 57 | return traces; 58 | } 59 | }; 60 | 61 | export default TraceGroups; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/processors/labels.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | const _labels = { 4 | added : (message) => { 5 | return _.isArray(message) ? 6 | `${message.length} items added to ${message[0].collection} collection` : 7 | `item added to ${message.collection} collection`; 8 | }, 9 | 10 | removed : (message) => { 11 | return _.isArray(message) ? 12 | `${message.length} items removed from ${message[0].collection} collection` : 13 | `item removed from ${message.collection} collection`; 14 | }, 15 | 16 | changed : (message) => { 17 | return _.isArray(message) ? 18 | `${message.length} items changed ${message[0].collection} collection` : 19 | `item changed in ${message.collection} collection`; 20 | }, 21 | 22 | ping : () => 'ping', 23 | pong : () => 'pong', 24 | connect : () => 'connect', 25 | updated : () => 'updated', 26 | 27 | result : (message, traces) => { 28 | let method = _.find(traces, (t) => { 29 | return t.operation === 'method' && 30 | message.id === t.message.id; 31 | }); 32 | let methodName = method && method.message.method; 33 | return `got result for method ${methodName}`; 34 | }, 35 | 36 | sub : (message) => { 37 | let params = (message.params || []).join(', ') 38 | if (params) { 39 | return `subscribing to ${message.name} with ${params}`; 40 | } 41 | return `subscribing to ${message.name}`; 42 | 43 | }, 44 | 45 | ready : (message, traces) => { 46 | let sub = _.find(traces, (t) => { 47 | return t.operation === 'sub' && 48 | _.contains(message.subs, t.message.id); 49 | }); 50 | let subName = (sub && sub.message.name) || 'unknown subscription'; 51 | return `subscription ready for ${subName}`; 52 | }, 53 | 54 | method : (message) => { 55 | let params = (message.params || []).join(', ') 56 | if (params) { 57 | return `calling method ${message.method} with ${params}`; 58 | } 59 | return `calling method ${message.method}`; 60 | }, 61 | 62 | unsub : (message, traces) => { 63 | let sub = _.find(traces, (t) => { 64 | return t.operation === 'sub' && 65 | (message.id === t.message.id); 66 | }); 67 | let subName = (sub && sub.message.name) || 'unknown subscription'; 68 | 69 | return `unsubscribing from ${subName}`; 70 | }, 71 | 72 | nosub : (message, traces) => { 73 | let sub = _.find(traces, (t) => { 74 | return t.operation === 'sub' && 75 | (message.id === t.message.id); 76 | }); 77 | let subName = (sub && sub.message.name); 78 | let label = 'nosub'; 79 | 80 | let unsub = _.find(traces, (t) => { 81 | return t.operation === 'unsub' && 82 | (message.id === t.message.id); 83 | }); 84 | 85 | if (subName) { 86 | label = `unsubscribed from ${subName}`; 87 | } 88 | if (! unsub) { 89 | // no unsubscribe request, hence the subscription is unknown 90 | label = `${label} (unrecognized subscription)`; 91 | } 92 | return label; 93 | }, 94 | 95 | }; 96 | 97 | class TraceLabels { 98 | run(traces) { 99 | return _.map(traces, (t) => { 100 | let messageType = _.isArray(t.message) ? 101 | t.message[0].msg : t.message.msg; 102 | let label = _labels[messageType] ? 103 | _labels[messageType].call(this,t.message,traces) : 104 | messageType; 105 | 106 | return _.extend(t, { label }); 107 | }); 108 | } 109 | }; 110 | 111 | export default TraceLabels; 112 | -------------------------------------------------------------------------------- /src/plugins/ddp/lib/processors/message-size.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | class TraceMessageSize { 4 | run(traces) { 5 | return _.map(traces, (t) => { 6 | return _.extend(t, { size: JSON.stringify(t.message).length }); 7 | }); 8 | } 9 | }; 10 | 11 | export default TraceMessageSize; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/processors/operation-types.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import operationTypes from '../../constants/operation-types' 3 | 4 | class TraceOperationTypes { 5 | run(traces) { 6 | return _.map(traces, (t) => { 7 | const operationType = _.find(_.keys(operationTypes), (ot) => { 8 | return _.contains(operationTypes[ot], t.operation); 9 | }); 10 | return _.extend(t, { 11 | operationType : operationType 12 | }); 13 | }); 14 | } 15 | }; 16 | 17 | export default TraceOperationTypes; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/processors/operations.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | class TraceOperations { 4 | run(traces) { 5 | return _.map(traces, (t) => { 6 | return _.extend(t, { 7 | operation : _.isArray(t.message) ? 8 | t.message[0].msg : t.message.msg 9 | }); 10 | }); 11 | } 12 | }; 13 | 14 | export default TraceOperations; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/stats.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import OperationTypes from '../constants/operation-types'; 3 | 4 | module.exports = { 5 | computeStats (traces) { 6 | return { 7 | inboundMessages: _.reduce(traces, (memo, trace) => { 8 | return memo + (!trace.isOutbound ? 1 : 0); 9 | }, 0), 10 | outboundMessages: _.reduce(traces, (memo, trace) => { 11 | return memo + (trace.isOutbound ? 1 : 0); 12 | }, 0), 13 | inboundMessagesSize : _.reduce(traces, (memo, trace) => { 14 | return memo + (!trace.isOutbound ? trace.size : 0); 15 | }, 0), 16 | outboundMessagesSize: _.reduce(traces, (memo, trace) => { 17 | return memo + (trace.isOutbound ? trace.size : 0); 18 | }, 0), 19 | messageTypes: { 20 | methodCalls : _.reduce(traces, (cnt, trace) => { 21 | return cnt + (trace.operation === 'method' ? 1 : 0); 22 | }, 0), 23 | collections : _.reduce(traces, (cnt, trace) => { 24 | return cnt + (trace.operationType === 'Collections' ? 1 : 0); 25 | }, 0), 26 | subscriptions : _.reduce(traces, (cnt, trace) => { 27 | return cnt + (trace.operation === 'sub' ? 1 : 0); 28 | }, 0), 29 | } 30 | }; 31 | } 32 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/trace-filter.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | module.exports = { 4 | filterTraces(traces, filters) { 5 | 6 | let disabledFilters = _.reduce(_.keys(filters), (memo, filterName) => { 7 | let filter = filters[filterName]; 8 | return filter.enabled ? 9 | memo : 10 | [...memo, ...filter.operations]; 11 | }, []); 12 | 13 | return _.filter(traces, function(trace){ 14 | return !_.contains(disabledFilters, trace.operation); 15 | }); 16 | } 17 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/trace-processor.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Groups from './processors/groups'; 3 | import Labels from './processors/labels'; 4 | import Operations from './processors/operations'; 5 | import OperationTypes from './processors/operation-types'; 6 | import Associations from './processors/associations'; 7 | import MessageSize from './processors/message-size'; 8 | 9 | module.exports = { 10 | processTraces : function(traces){ 11 | // XX: deep copy 12 | let copyOfTraces = JSON.parse(JSON.stringify(traces)); 13 | return _.sortBy( 14 | _.reduce(this.processors, (ts, processor) => processor.run(ts), copyOfTraces), 15 | (t) => t._timestamp 16 | ); 17 | }, 18 | 19 | processors : [ 20 | new Groups(), 21 | new Operations(), 22 | new OperationTypes(), 23 | new Associations(), 24 | new Labels(), 25 | new MessageSize(), 26 | ] 27 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/warnings/index.js: -------------------------------------------------------------------------------- 1 | import UserOverpublish from './user-overpublish'; 2 | import UknownPublication from './unknown-publication'; 3 | 4 | import _ from 'underscore'; 5 | 6 | var checks = [ 7 | new UserOverpublish(), 8 | new UknownPublication() 9 | ]; 10 | 11 | module.exports = { 12 | checkForWarnings : function(traces){ 13 | return _.reduce(checks, (ts, c) => c.check(ts), traces); 14 | } 15 | }; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/warnings/unknown-publication.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | class UknownPublicationWarning { 4 | check(traces) { 5 | return _.map(traces, (trace) => { 6 | if(trace.operation !== 'nosub'){ 7 | return trace; 8 | } 9 | 10 | const isUknown = trace.message.error && 11 | (trace.message.error.error === 404); 12 | 13 | return isUknown ? 14 | _.extend(trace, { 15 | warnings : ['unknown-publication'] 16 | }) : 17 | trace; 18 | }); 19 | } 20 | }; 21 | 22 | module.exports = UknownPublicationWarning; -------------------------------------------------------------------------------- /src/plugins/ddp/lib/warnings/user-overpublish.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | class UserOverpublishWarning { 4 | check(traces) { 5 | return _.map(traces, (trace) => { 6 | if(trace.operation !== 'added'){ 7 | return trace; 8 | } 9 | 10 | let collectionNames = _.uniq(_.pluck(trace.message, 'collection')); 11 | 12 | if(collectionNames[0] !== 'users'){ 13 | return trace; 14 | } 15 | 16 | let message = trace.message[0]; 17 | let flds = message.fields; 18 | let isOverpublishing = flds && flds.services && 19 | (flds.services.password || flds.services.resume); 20 | 21 | return isOverpublishing ? 22 | _.extend(trace, { 23 | warnings : ['user-overpublish'] 24 | }) : 25 | trace; 26 | }); 27 | } 28 | }; 29 | 30 | module.exports = UserOverpublishWarning; -------------------------------------------------------------------------------- /src/plugins/ddp/reducers/filters.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE_FILTER } from '../constants/action-types' 2 | 3 | const initialState = { 4 | 'PingPong' : { 5 | operations: ['ping','pong'], 6 | enabled: false 7 | }, 8 | 'Subscriptions' : { 9 | operations: ['sub','unsub','nosub','ready'], 10 | enabled: true 11 | }, 12 | 'Collections' : { 13 | operations: ['added','removed','changed'], 14 | enabled: true 15 | }, 16 | 'Methods' : { 17 | operations: ['method','result','updated'], 18 | enabled: true 19 | }, 20 | 'Connect' : { 21 | operations: ['connect','connected','failed'], 22 | enabled: false 23 | } 24 | }; 25 | 26 | export default function filters(state = initialState, action){ 27 | switch(action.type){ 28 | case TOGGLE_FILTER: 29 | return Object.assign({}, state, { 30 | [action.filter] : Object.assign({}, state[action.filter], { 31 | enabled : !state[action.filter].enabled 32 | }) 33 | }) 34 | default: 35 | return state 36 | } 37 | } -------------------------------------------------------------------------------- /src/plugins/ddp/reducers/index.js: -------------------------------------------------------------------------------- 1 | import traces from './traces' 2 | import filters from './filters' 3 | 4 | export default {traces, filters}; 5 | -------------------------------------------------------------------------------- /src/plugins/ddp/reducers/traces.js: -------------------------------------------------------------------------------- 1 | import { NEW_TRACE, CLEAR_LOGS } from '../constants/action-types'; 2 | import Immutable from 'immutable'; 3 | 4 | 5 | export default function traces(state = Immutable.List(), action){ 6 | switch(action.type){ 7 | case NEW_TRACE: 8 | return state.push(action.trace); 9 | case CLEAR_LOGS: 10 | return Immutable.List(); 11 | default: 12 | return state; 13 | } 14 | } -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore, compose, combineReducers } from 'redux'; 3 | import _ from 'underscore'; 4 | import DDPReducers from './ddp/reducers'; 5 | import DDPMonitor from './ddp'; 6 | import BlazeReducers from './blaze/reducers'; 7 | import BlazeInspector from './blaze'; 8 | import MiniMongoExplorer from './minimongo'; 9 | import SecurityAuditor from './security'; 10 | import SecurityReducers from './security/reducers'; 11 | import MiniMongoReducers from './minimongo/reducers'; 12 | import CommonReducers from '../common/reducers'; 13 | 14 | let __store = null; 15 | const plugins = [ 16 | { 17 | name: 'DDP', 18 | reducers: DDPReducers, 19 | component: , 20 | }, 21 | { 22 | name: 'Blaze', 23 | reducers: BlazeReducers, 24 | component: , 25 | }, 26 | { 27 | name: 'MiniMongo', 28 | reducers: MiniMongoReducers, 29 | component: 30 | }, 31 | { 32 | name: 'Security', 33 | reducers: SecurityReducers, 34 | component: 35 | } 36 | ]; 37 | 38 | module.exports = { 39 | getStore() { 40 | if (!__store) { 41 | const finalCreateStore = compose( 42 | // XX: work with Redux Devtools Chrome extension 43 | window.devToolsExtension ? window.devToolsExtension() : f => f 44 | )(createStore); 45 | 46 | const configureStore = function(initialState){ 47 | let rootReducer = CommonReducers; 48 | _.each(plugins , (p) => _.extend(rootReducer, p.reducers)); 49 | return finalCreateStore(combineReducers(rootReducer), initialState); 50 | } 51 | 52 | __store = configureStore(); 53 | } 54 | 55 | return __store; 56 | }, 57 | 58 | getPlugins() { 59 | return plugins; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/plugins/minimongo/actions/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_MINIMONGO_COLLECTION_DATA, 3 | SET_MINIMONGO_COLLECTION_SELECTION, 4 | SET_MINIMONGO_COLLECTION_QUERY, 5 | SET_MINIMONGO_COLLECTION_AND_QUERY 6 | } from '../constants'; 7 | 8 | export function setCollectionData(data) { 9 | return { 10 | type: SET_MINIMONGO_COLLECTION_DATA, 11 | data: data 12 | } 13 | } 14 | 15 | export function setCollectionSelection(collectionName) { 16 | return { 17 | type: SET_MINIMONGO_COLLECTION_SELECTION, 18 | collectionName: collectionName 19 | } 20 | } 21 | 22 | export function setCollectionQuery(collectionName, query) { 23 | return { 24 | type: SET_MINIMONGO_COLLECTION_QUERY, 25 | collectionName: collectionName, 26 | query: query 27 | } 28 | } 29 | 30 | export function setCollectionAndQuery(collectionName, query) { 31 | return { 32 | type: SET_MINIMONGO_COLLECTION_AND_QUERY, 33 | collectionName: collectionName, 34 | query: query 35 | } 36 | } -------------------------------------------------------------------------------- /src/plugins/minimongo/components/collection-input.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export default React.createClass({ 5 | propTypes : { 6 | collectionName : PropTypes.string.isRequired, 7 | query: PropTypes.string.isRequired, 8 | onChange: PropTypes.func.isRequired, 9 | error: PropTypes.bool.isRequired 10 | }, 11 | 12 | handleChange: function(e) { 13 | this.props.onChange(this.props.collectionName, e.target.value); 14 | }, 15 | 16 | render () { 17 | let inputClass = classNames('minimongo-input', 18 | {'error' : this.props.error} 19 | ); 20 | 21 | return ( 22 |