├── .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 | 
2 | # Meteor Dev Tools Chrome Extension
3 | [](https://travis-ci.org/thebakeryio/meteor-devtools)
4 | [](https://david-dm.org/thebakeryio/meteor-devtools)
5 | [](https://david-dm.org/thebakeryio/meteor-devtools#info=devDependencies)
6 | [](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 |
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 |
69 | {this.props.children}
70 |
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 |
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 |
13 |
14 | Clear Traces
15 |
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 | Show {this.props.name}
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 |
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 |
111 |
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 |
29 | )
30 | }
31 | });
--------------------------------------------------------------------------------
/src/plugins/minimongo/components/collection-item.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | export default React.createClass({
4 | propTypes : {
5 | name : PropTypes.string.isRequired,
6 | size: PropTypes.number.isRequired,
7 | isSelected: PropTypes.bool.isRequired,
8 | changeCollectionSelection: PropTypes.func.isRequired
9 | },
10 |
11 | changeSelection () {
12 | this.props.changeCollectionSelection(this.props.name);
13 | },
14 |
15 | render () {
16 | return (
17 |
19 | {this.props.name} ({this.props.size})
20 |
21 | )
22 | }
23 | });
--------------------------------------------------------------------------------
/src/plugins/minimongo/components/collection-list.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import CollectionItem from './collection-item';
3 |
4 | export default React.createClass({
5 | propTypes : {
6 | collections : PropTypes.object.isRequired,
7 | changeCollectionSelection: PropTypes.func.isRequired,
8 | currentSelection: PropTypes.string
9 | },
10 |
11 | isSelected (itemName) {
12 | return this.props.currentSelection === itemName;
13 | },
14 |
15 | render () {
16 | const noData = this.props.collections.count() === 0 ?
17 | No collections yet... : null;
18 |
19 | const items = this.props.collections.entrySeq()
20 | .sortBy(([k, v]) => k).map( ([k, v]) => {
21 | return (
22 |
27 | )
28 | });
29 |
30 | return (
31 |
32 | {noData}
33 | {items}
34 |
35 | )
36 | }
37 | });
--------------------------------------------------------------------------------
/src/plugins/minimongo/components/data-tree.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import JSONTree from 'react-json-tree';
3 |
4 | export default React.createClass({
5 | propTypes : {
6 | data : PropTypes.array.isRequired
7 | },
8 |
9 | render () {
10 |
11 | const theme = {
12 | tree: {
13 | backgroundColor: 'transparent',
14 | fontSize: '1em'
15 | },
16 | arrow: ({ style }, type, expanded) => ({
17 | style: Object.assign(style, {
18 | marginTop: 2
19 | })
20 | }),
21 | };
22 |
23 | const getItemString = (type, data, itemType, itemString) => {
24 | let id = (typeof data._id) === 'string' ? data._id :
25 | data._id && data._id._str;
26 | return ( {id} {itemType} {itemString} );
27 | };
28 |
29 | // expand the first node if its an only child
30 | const shouldExpandNode = (keyPath, data, level) => {
31 | return (level === 1) && (this.props.data.length === 1);
32 | };
33 |
34 | if(this.props.data.length === 0){
35 | return No items in this collection.
36 | } else {
37 | return (
38 | );
45 | }
46 | }
47 | });
--------------------------------------------------------------------------------
/src/plugins/minimongo/constants/index.js:
--------------------------------------------------------------------------------
1 | export const SET_MINIMONGO_COLLECTION_DATA = 'set_minimongo_collection_data';
2 | export const SET_MINIMONGO_COLLECTION_SELECTION = 'set_minimongo_collection_selection';
3 | export const SET_MINIMONGO_COLLECTION_QUERY = 'set_minimongo_collection_query';
4 | export const SET_MINIMONGO_COLLECTION_AND_QUERY = 'set_minimongo_collection_and_query';
--------------------------------------------------------------------------------
/src/plugins/minimongo/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import Bridge from '../../common/bridge';
4 | import {
5 | setCollectionData,
6 | setCollectionSelection,
7 | setCollectionQuery
8 | } from './actions';
9 | import Immutable from 'immutable';
10 | import Analytics from '../../common/analytics';
11 | import CollectionList from './components/collection-list';
12 | import CollectionInput from './components/collection-input';
13 | import DataTree from './components/data-tree';
14 | import safeDocumentQuery from './lib/doc-matcher';
15 | import safeDocumentSorter from './lib/doc-sorter';
16 | import safeDocumentProjector from './lib/doc-projector';
17 | import SplitPane from 'react-split-pane';
18 | import './minimongo.scss';
19 |
20 | let dispatch = null;
21 |
22 | const onNewMessage = (error, message) => {
23 | if(message && message.eventType === 'minimongo-explorer') {
24 | dispatch(setCollectionData(message.data));
25 | }
26 | };
27 |
28 | const onPageReload = () => {
29 | dispatch(setCollectionData());
30 | Bridge.sendMessageToThePage({
31 | source: 'security-auditor',
32 | event: 'get-minimongo-collections'
33 | });
34 | };
35 |
36 | class App extends Component {
37 |
38 | componentDidMount() {
39 | dispatch = this.props.dispatch;
40 |
41 | if(chrome && chrome.devtools) {
42 | Bridge.registerMessageCallback(onNewMessage);
43 | Bridge.registerPageReloadCallback(onPageReload);
44 | Bridge.sendMessageToThePage({
45 | source: 'minimongo-explorer',
46 | event: 'get-minimongo-collections'
47 | });
48 | } else {
49 | var fakeCollections = require('./fake');
50 | onNewMessage.call(this, null, {
51 | eventType: 'minimongo-explorer',
52 | data: fakeCollections
53 | });
54 | }
55 | }
56 |
57 | componentWillUnmount() {
58 | Bridge.removeMessageCallback(onNewMessage);
59 | }
60 |
61 | _collectionPanel (currentSelection) {
62 | if(currentSelection){
63 |
64 | const changeQuery = (collectionName, query) => {
65 | dispatch(setCollectionQuery(collectionName, query));
66 | }
67 |
68 | const query = this.props.minimongoCollectionQuery.
69 | get(this.props.minimongoCurrentSelection) ||
70 | '{ query: { }, fields: { }, sort: { } }';
71 | const matcher = safeDocumentQuery(query);
72 | const projector = safeDocumentProjector(query);
73 | const sorter = safeDocumentSorter(query);
74 | const error = matcher.error || projector.error || sorter.error;
75 | const collection = this.props.minimongoCollections.get(this.props.minimongoCurrentSelection) || [];
76 | const queryResult = collection.filter(matcher.action).map(projector.action).sort(sorter.action);
77 |
78 | return (
79 |
80 |
{currentSelection}
81 |
87 |
88 |
89 | )
90 | } else {
91 | return No collections yet...
92 | }
93 | }
94 |
95 | render() {
96 | const data = this.props.minimongoCollections;
97 | const noData = Immutable.is(data, Immutable.fromJS({}));
98 | const changeSelection = (collectionName) => {
99 | dispatch(setCollectionSelection(collectionName));
100 | }
101 |
102 | return (
103 |
104 |
105 |
106 |
Collections
107 |
112 |
113 |
114 | { this._collectionPanel(this.props.minimongoCurrentSelection) }
115 |
116 |
117 |
118 | )
119 | }
120 | }
121 |
122 | App.propTypes = {
123 | minimongoCurrentSelection : PropTypes.string,
124 | minimongoCollectionQuery: PropTypes.object,
125 | minimongoCollections: PropTypes.object.isRequired
126 | }
127 |
128 | export default connect((state) => {
129 | return {
130 | minimongoCollections: state.minimongoCollectionData,
131 | minimongoCurrentSelection: state.minimongoCollectionSelection,
132 | minimongoCollectionQuery: state.minimongoCollectionQuery
133 | };
134 | })(App)
--------------------------------------------------------------------------------
/src/plugins/minimongo/inject.js:
--------------------------------------------------------------------------------
1 | const __cleanUpObjectProps = (obj) => {
2 | Object.keys(obj).forEach((k) => {
3 | if (obj[k] instanceof Date) {
4 | obj[k] = obj[k].toString();
5 | }
6 | });
7 | return obj;
8 | };
9 |
10 | const __getMinimongoCollections = (callback) => {
11 | let data = {};
12 | const collections = Meteor.connection._mongo_livedata_collections;
13 | for(let i in collections) {
14 | if(collections[i].name){
15 | data[collections[i].name] = collections[i].find().fetch().map(__cleanUpObjectProps);
16 | }
17 | }
18 | callback && callback('minimongo-explorer', data);
19 | };
20 |
21 | let __talkToExtension = null;
22 |
23 | module.exports = {
24 | setup : (talkToExtension) => {
25 | __talkToExtension = talkToExtension;
26 | Tracker.autorun(function(){
27 | const collections = Meteor.connection._mongo_livedata_collections;
28 | for(let i in collections) {
29 | collections[i].find();
30 | }
31 | __getMinimongoCollections(talkToExtension)
32 | });
33 | },
34 | onMessage: (message) => {
35 | if(message.source !== 'minimongo-explorer'){
36 | return;
37 | }
38 | if(message.event === 'get-minimongo-collections'){
39 | __talkToExtension && __getMinimongoCollections(__talkToExtension);
40 | }
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/plugins/minimongo/lib/doc-matcher.js:
--------------------------------------------------------------------------------
1 | import DocumentMatcher from 'marsdb/dist/DocumentMatcher';
2 |
3 | const defaultAction = () => true;
4 |
5 | export default query => {
6 | try {
7 | let parsed = eval(`(${query})`);
8 | if (parsed && parsed.query) {
9 | const helper = new DocumentMatcher(parsed.query);
10 | const action = doc => helper.documentMatches(doc).result;
11 | return {action, error: false};
12 | }
13 | return { action: defaultAction, error: false };
14 | } catch (_) {
15 | return { action: defaultAction, error: true };
16 | }
17 | };
--------------------------------------------------------------------------------
/src/plugins/minimongo/lib/doc-projector.js:
--------------------------------------------------------------------------------
1 | import DocumentProjector from 'marsdb/dist/DocumentProjector';
2 |
3 | const defaultAction = doc => doc;
4 |
5 | export default query => {
6 | try {
7 | let parsed = eval(`(${query})`);
8 | if (parsed && parsed.fields) {
9 | const helper = new DocumentProjector(parsed.fields);
10 | const action = doc => helper.project(doc);
11 | return {action, error: false};
12 | }
13 | return {action: defaultAction, error: false};
14 | } catch (_) {
15 | return {action: defaultAction, error: true};
16 | }
17 | };
--------------------------------------------------------------------------------
/src/plugins/minimongo/lib/doc-sorter.js:
--------------------------------------------------------------------------------
1 | import DocumentSorter from 'marsdb/dist/DocumentSorter';
2 |
3 | const defaultAction = () => 0;
4 |
5 | export default query => {
6 | try {
7 | let parsed = eval(`(${query})`);
8 | if (parsed && parsed.sort) {
9 | const helper = new DocumentSorter(parsed.sort);
10 | const action = helper.getComparator()
11 | return {action, error: false};
12 | }
13 | return {action: defaultAction, error: false};
14 | } catch (_) {
15 | return {action: defaultAction, error: true};
16 | }
17 | };
--------------------------------------------------------------------------------
/src/plugins/minimongo/minimongo.scss:
--------------------------------------------------------------------------------
1 | .no-minimongo {
2 | margin: 0.5em;
3 | }
4 |
5 | .minimongo {
6 | display: flex;
7 | flex-direction: row;
8 | height: 100%;
9 |
10 | > aside, > section {
11 | overflow-y: scroll;
12 | padding-bottom: 40px;
13 | }
14 |
15 | > aside {
16 | flex: 1;
17 | max-width: 25%;
18 | border-right: solid 1px #ccc;
19 | }
20 |
21 | > section {
22 | flex: 3;
23 | }
24 |
25 | > section ul {
26 | margin: 4px 0;
27 | }
28 |
29 | .collection-panel {
30 | padding-bottom: 31px;
31 | }
32 |
33 | .Pane1, .Pane2 {
34 | overflow-y: scroll;
35 | }
36 | }
37 |
38 | .minimongo-header {
39 | text-align: center;
40 | background-color: #f3f3f3;
41 | padding: 5px 0;
42 | font-weight: bold;
43 | border-bottom: solid 1px #ccc;
44 | }
45 |
46 | .minimongo-collections {
47 | list-style: none;
48 | padding: 0;
49 | margin: 0;
50 | overflow: hidden;
51 |
52 | li {
53 | padding: 5px 10px;
54 |
55 | &:nth-child(even) {
56 | background-color: #f5f5f5;
57 | }
58 |
59 | &:hover {
60 | cursor: pointer;
61 | }
62 |
63 | &.selected {
64 | color: white;
65 | background-color: rgb(56, 121, 217)
66 | }
67 | }
68 | }
69 |
70 | .minimongo-input {
71 | width: 100%;
72 | padding: 5px 2px;
73 | outline: none;
74 | font-size: 12px;
75 | box-sizing: border-box;
76 | border: none;
77 | border-bottom: 1px solid #ccc;
78 |
79 | &.error {
80 | background-color: #f2dede;
81 | border-color: #ebccd1;
82 | color: #a94442;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/plugins/minimongo/reducers/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 | import Immutable from 'immutable'
8 |
9 | export default {
10 | minimongoCollectionData (state = Immutable.Map(), action) {
11 | switch(action.type){
12 | case SET_MINIMONGO_COLLECTION_DATA:
13 | return Immutable.Map(action.data);
14 | default:
15 | return state;
16 | }
17 | },
18 | minimongoCollectionSelection (state = Immutable.fromJS(null), action) {
19 | switch(action.type){
20 | case SET_MINIMONGO_COLLECTION_SELECTION:
21 | return Immutable.fromJS(action.collectionName);
22 | case SET_MINIMONGO_COLLECTION_AND_QUERY:
23 | return Immutable.fromJS(action.collectionName);
24 | default:
25 | return state;
26 | }
27 | },
28 | minimongoCollectionQuery (state = Immutable.Map(), action) {
29 | switch(action.type){
30 | case SET_MINIMONGO_COLLECTION_QUERY:
31 | return state.set(action.collectionName, action.query);
32 | case SET_MINIMONGO_COLLECTION_AND_QUERY:
33 | return state.set(action.collectionName, action.query);
34 | default:
35 | return state;
36 | }
37 | },
38 | };
--------------------------------------------------------------------------------
/src/plugins/security/actions/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_PACKAGE_LIST,
3 | SET_COLLECTION_SECURITY,
4 | CLEAR_METHOD_SECURITY,
5 | SET_SECURITY_TAB
6 | } from '../constants';
7 |
8 | export function setPackageList(data) {
9 | return {
10 | type: SET_PACKAGE_LIST,
11 | data: data
12 | }
13 | }
14 |
15 | export function clearMethodSecurity() {
16 | return {
17 | type: CLEAR_METHOD_SECURITY
18 | }
19 | }
20 |
21 | export function setSecurityTab(tab) {
22 | return {
23 | type: SET_SECURITY_TAB,
24 | tab: tab
25 | }
26 | }
--------------------------------------------------------------------------------
/src/plugins/security/components/collection-audit.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import classNames from 'classnames';
4 | import {
5 | buildDDPMessage,
6 | } from '../lib';
7 | import Analytics from '../../../common/analytics';
8 | import Bridge from '../../../common/bridge';
9 |
10 | const getResultForOperation = (traces, collection, operation) => {
11 | const id = `/audit/${collection}/${operation}`;
12 | const res = traces.find((trace) => {
13 | return trace.message.id === id;
14 | });
15 | if(!res){
16 | return false;
17 | } else {
18 | return res.message && res.message.error &&
19 | res.message.error.error === 403 ? 'secure' : 'insecure';
20 | }
21 | };
22 |
23 | export default React.createClass({
24 | propTypes : {
25 | traces: PropTypes.object.isRequired,
26 | name: PropTypes.string.isRequired
27 | },
28 |
29 | shouldComponentUpdate: function(nextProps, nextState) {
30 | return !this.props.traces.equals(nextProps.traces);
31 | },
32 |
33 | _auditCollection() {
34 | const operations = ['insert', 'update', 'remove'];
35 |
36 | operations.forEach((operation) => {
37 | // send a DDP probe
38 | Bridge.sendMessageToThePage({
39 | source: 'security-auditor',
40 | event: 'test-collection-security',
41 | ddpMessage: buildDDPMessage(this.props.name, operation),
42 | });
43 | });
44 |
45 | Analytics.trackEvent('security', 'collection:audit');
46 | },
47 |
48 | _showResults(result, operation) {
49 | return result && {operation}: {result} ;
50 | },
51 |
52 | render () {
53 | const insertResult = getResultForOperation(this.props.traces, this.props.name, 'insert');
54 | const updateResult = getResultForOperation(this.props.traces, this.props.name, 'update');
55 | const removeResult = getResultForOperation(this.props.traces, this.props.name, 'remove');
56 |
57 | const statusClass = classNames('status', {
58 | 'secure' : insertResult === 'secure' && updateResult === 'secure' && removeResult === 'secure',
59 | 'insecure' : (insertResult && updateResult && removeResult) &&
60 | (insertResult !== 'secure' || updateResult !== 'secure' || removeResult !== 'secure')
61 | });
62 | const resultClass = classNames('operation-results', {
63 | 'show': insertResult || updateResult || removeResult
64 | });
65 |
66 | return (
67 |
68 | ⬤
69 |
70 |
{this.props.name}
71 |
72 | {this._showResults(insertResult, 'Insert')}
73 | {this._showResults(updateResult, 'Update')}
74 | {this._showResults(removeResult, 'Remove')}
75 |
76 |
77 | Audit collection
78 |
79 |
80 |
81 | )
82 | }
83 | });
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/plugins/security/components/collections-panel.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import _ from 'underscore';
3 | import CollectionAudit from './collection-audit';
4 |
5 | export default React.createClass({
6 | propTypes : {
7 | collectionData: PropTypes.object.isRequired,
8 | traces: PropTypes.object.isRequired
9 | },
10 |
11 | shouldComponentUpdate: function(nextProps, nextState) {
12 | return !this.props.traces.equals(nextProps.traces);
13 | },
14 |
15 | // only send relevant traces to child components
16 | _filterTraces : (traces, name) => {
17 | return traces.filter((trace) => {
18 | return trace.message.id && trace.message.id.startsWith(`/audit/${name}`);
19 | });
20 | },
21 |
22 | render () {
23 | let collections = this.props.collectionData.entrySeq().sort(([k,v]) => k).map(([k, v]) => {
24 | return ( );
25 | });
26 |
27 | let noCollections = () => {
28 | if(!this.props.collectionData.size){
29 | return (
30 | No collections detected.
31 | );
32 | }
33 | };
34 |
35 | return (
36 |
37 |
42 |
43 | {collections}
44 | {noCollections()}
45 |
46 |
47 | )
48 | }
49 | });
--------------------------------------------------------------------------------
/src/plugins/security/components/method-audit.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import classNames from 'classnames';
4 | import {
5 | buildDDPMethodTester,
6 | testCollectionSecurity,
7 | } from '../lib';
8 | import {
9 | setCollectionSecurity
10 | } from '../actions';
11 | import Analytics from '../../../common/analytics';
12 | import Bridge from '../../../common/bridge';
13 | import JSONTree from 'react-json-tree';
14 | import MethodStatus from './method-audit';
15 |
16 | export default React.createClass({
17 | propTypes : {
18 | traces: PropTypes.object.isRequired,
19 | name: PropTypes.string.isRequired,
20 | params : PropTypes.array,
21 | },
22 |
23 | shouldComponentUpdate: function(nextProps, nextState) {
24 | return !this.props.traces.equals(nextProps.traces);
25 | },
26 |
27 | _auditMethod(argType, argument) {
28 | // send a DDP probe
29 | Bridge.sendMessageToThePage({
30 | source: 'security-auditor',
31 | event: 'test-method-params',
32 | ddpMessage: buildDDPMethodTester(this.props.name, argType, argument),
33 | });
34 | this.setState({'testing': true});
35 |
36 | Analytics.trackEvent('security', 'method:audit');
37 | },
38 |
39 | _showResult(argType) {
40 | const response = this.props.traces.find((trace) => {
41 | return trace.message.id === `/audit/${this.props.name}/${argType}`;
42 | });
43 | if (!response) { return; }
44 | if(response.message && response.message.error) {
45 | if(response.message.error.reason === 'Match failed'){
46 | return ⬤ Blocked by check
;
47 | } else {
48 | return ⬤ Unknown error
;
49 | }
50 | } else {
51 | return ⬤ Method called
;
52 | }
53 | },
54 |
55 | render () {
56 | const theme = {
57 | tree: {
58 | backgroundColor: 'transparent',
59 | fontSize: '1em'
60 | },
61 | arrow: ({ style }, type, expanded) => ({
62 | style: Object.assign(style, {
63 | marginTop: 2
64 | })
65 | }),
66 | };
67 |
68 | const valueRenderer = (raw) => {
69 | const type = typeof raw;
70 | return {raw} // {type}
71 | };
72 |
73 | const checkMethod = ['string', 'number', 'object'].map((argType) => {
74 | return (
75 |
this._auditMethod(argType)}>Call with {argType}
76 |
{this._showResult(argType)}
77 |
);
78 | });
79 |
80 | return (
81 |
82 | {this.props.name}
83 |
84 |
Called with args:
85 |
91 |
92 | {checkMethod}
93 |
94 |
95 |
96 | )
97 | }
98 | });
99 |
--------------------------------------------------------------------------------
/src/plugins/security/components/methods-panel.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import MethodAudit from './method-audit';
3 |
4 | export default React.createClass({
5 | propTypes : {
6 | methodsSecurity: PropTypes.object.isRequired,
7 | traces: PropTypes.object.isRequired
8 | },
9 |
10 | // only send relevant traces to child components
11 | _filterTraces : (traces, name) => {
12 | return traces.filter((trace) => {
13 | return trace.message.id && trace.message.id.startsWith(`/audit/${name}`);
14 | });
15 | },
16 |
17 | render () {
18 | let methods = this.props.methodsSecurity.entrySeq().map( ([k, v]) => {
19 | return ;
20 | });
21 |
22 | let noMethod = () => {
23 | if(!this.props.methodsSecurity.size){
24 | return (
25 | No methods recorded yet. Browse around your application and outgoing method calls will show up here.
26 | );
27 | }
28 | };
29 |
30 | return (
31 |
32 |
38 |
39 | {methods}
40 | {noMethod()}
41 |
42 |
43 | )
44 | }
45 | });
--------------------------------------------------------------------------------
/src/plugins/security/components/package-audit.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import _ from 'underscore';
3 |
4 | export default React.createClass({
5 | propTypes : {
6 | packages: PropTypes.object.isRequired
7 | },
8 |
9 | _checkPackageIsRemoved (packageName) {
10 | if(this.props.packages.includes(packageName)){
11 | return (
12 |
13 | ⬤
14 | You should consider removing the {packageName} package.
15 | );
16 | } else {
17 | return (
18 |
19 | ⬤
20 | {packageName} package was removed.
21 | );
22 | }
23 | },
24 |
25 | _checkPackageIsIncluded (packageName) {
26 | if(!this.props.packages.includes(packageName)){
27 | return (
28 |
29 | ⬤
30 | Consider using the {packageName} package.
31 | );
32 | } else {
33 | return (
34 |
35 | ⬤
36 | {packageName} package is included.
37 |
38 | );
39 | }
40 | },
41 |
42 | _listPackages () {
43 | if(this.props.packages.size){
44 | return (
45 | {this._checkPackageIsRemoved('insecure')}
46 | {this._checkPackageIsRemoved('autopublish')}
47 | {this._checkPackageIsIncluded('aldeed:simple-schema')}
48 | {this._checkPackageIsIncluded('audit-argument-checks')}
49 | {this._checkPackageIsIncluded('mdg:validated-method')}
50 | );
51 | } else {
52 | return (
53 |
54 | No packages detected.
55 |
56 | );
57 | }
58 | },
59 |
60 | render () {
61 | return (
62 |
63 |
67 | {this._listPackages()}
68 |
69 | )
70 | }
71 | });
72 |
--------------------------------------------------------------------------------
/src/plugins/security/constants/index.js:
--------------------------------------------------------------------------------
1 | export const SET_PACKAGE_LIST = 'set_package_list';
2 | export const SET_SECURITY_TAB = 'set_security_tab';
3 | export const CLEAR_METHOD_SECURITY = 'clear_method_security';
--------------------------------------------------------------------------------
/src/plugins/security/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux'
3 | import Bridge from '../../common/bridge'
4 | import _ from 'underscore';
5 | import {
6 | setPackageList,
7 | setCollectionSecurity,
8 | setSecurityTab,
9 | clearMethodSecurity
10 | } from './actions';
11 | import SplitPane from 'react-split-pane';
12 | import CollectionPanel from './components/collections-panel';
13 | import PackageAudit from './components/package-audit';
14 | import MethodsPanel from './components/methods-panel';
15 | import Analytics from '../../common/analytics';
16 | import './security.scss';
17 |
18 |
19 | let dispatch = null;
20 |
21 | const onNewMessage = (error, message) => {
22 | if(message && message.eventType === 'security-auditor'){
23 | dispatch(setPackageList(message.data));
24 | }
25 | };
26 |
27 | const onPageReload = () => {
28 | dispatch(clearMethodSecurity());
29 | dispatch(setPackageList());
30 | Bridge.sendMessageToThePage({
31 | source: 'security-auditor',
32 | event: 'get-package-list'
33 | });
34 | };
35 |
36 | class App extends Component {
37 | componentDidMount() {
38 | dispatch = this.props.dispatch;
39 | if(chrome && chrome.devtools) {
40 | Bridge.registerMessageCallback(onNewMessage);
41 | Bridge.registerPageReloadCallback(onPageReload);
42 | Bridge.sendMessageToThePage({
43 | source: 'security-auditor',
44 | event: 'get-package-list'
45 | });
46 | } else {
47 | // inside standalone web app
48 | onNewMessage.call(this, null, {
49 | eventType: 'security-auditor',
50 | data: [
51 | 'autopublish',
52 | 'insecure',
53 | 'something-else'
54 | ]
55 | });
56 | }
57 | Analytics.trackPageView('security audit');
58 | }
59 |
60 | componentWillUnmount() {
61 | Bridge.removeMessageCallback(onNewMessage);
62 | }
63 |
64 | _handleClick(index) {
65 | dispatch(setSecurityTab(index));
66 | }
67 |
68 | _showTabs(tabs, selectedIndex) {
69 | return tabs.map((t, i) => {
70 | const selected = (i === selectedIndex) && 'selected';
71 | const boundClick = this._handleClick.bind(this, i);
72 | return (
73 |
76 | {t.name}
77 |
78 | );
79 | });
80 | }
81 |
82 | _showSelectedComponent(tabs, selectedIndex) {
83 | return tabs[selectedIndex] && tabs[selectedIndex].component;
84 | }
85 |
86 | render() {
87 | const { dispatch } = this.props;
88 | const Tabs = [
89 | {
90 | name: 'Packages',
91 | component:
92 | },
93 | {
94 | name: 'Collections',
95 | component:
96 | },
97 | {
98 | name: 'Methods',
99 | component:
100 | },
101 | ];
102 |
103 | return (
104 |
105 |
106 |
107 |
108 | {this._showTabs(Tabs, this.props.securityTabsIndex)}
109 |
110 |
111 |
112 |
113 | {this._showSelectedComponent(Tabs, this.props.securityTabsIndex)}
114 |
115 |
116 |
117 |
118 |
119 | )
120 | }
121 | }
122 |
123 | App.propTypes = {
124 | collectionData: PropTypes.object,
125 | packageList : PropTypes.object,
126 | securityTabsIndex: PropTypes.number,
127 | methodsSecurity: PropTypes.object,
128 | resultTraces: PropTypes.object.isRequired
129 | }
130 |
131 | export default connect((state) => {
132 | return {
133 | packageList: state.packageList,
134 | collectionData : state.minimongoCollectionData,
135 | securityTabsIndex: state.securityTabsIndex,
136 | methodsSecurity: state.methodsSecurity,
137 | resultTraces: state.resultTraces,
138 | }
139 | })(App)
--------------------------------------------------------------------------------
/src/plugins/security/inject.js:
--------------------------------------------------------------------------------
1 | const __getPackageList = (callback) => {
2 | const collections = Package && Object.keys(Package);
3 | callback && callback('security-auditor', collections);
4 | };
5 |
6 | const __testDDPMethod = (ddpMessage) => {
7 | // add empty methodInvoker to avoid error message
8 | Meteor.connection._methodInvokers[ddpMessage.id] = { dataVisible : () => {} };
9 | Meteor.connection._stream.send(JSON.stringify(ddpMessage));
10 | };
11 |
12 | let __talkToExtension = null;
13 |
14 | module.exports = {
15 | setup : (talkToExtension) => {
16 | __talkToExtension = talkToExtension;
17 | __getPackageList(talkToExtension);
18 | },
19 | onMessage: (message) => {
20 | if(message.source !== 'security-auditor'){
21 | return;
22 | }
23 | if(message.event === 'get-package-list'){
24 | __talkToExtension && __getPackageList(__talkToExtension);
25 | }
26 | if(message.event === 'test-collection-security') {
27 | __talkToExtension && __testDDPMethod(message.ddpMessage);
28 | }
29 | if(message.event === 'test-method-params') {
30 | __talkToExtension && __testDDPMethod(message.ddpMessage);
31 | }
32 | }
33 | };
--------------------------------------------------------------------------------
/src/plugins/security/lib/index.js:
--------------------------------------------------------------------------------
1 | import _ from 'underscore';
2 |
3 | module.exports = {
4 |
5 | buildDDPMessage(collection, operation){
6 | // use empty object for inserts to avoid inserting
7 | let params = [{}];
8 | // but _id must be defined for updates and removals
9 | if(operation !== 'insert'){
10 | params = [{
11 | '_id' : 'an_invalid_id'
12 | }];
13 | }
14 |
15 | const ddpMessage = {
16 | msg: 'method',
17 | method: `/${collection}/${operation}`,
18 | params: params,
19 | id: `/audit/${collection}/${operation}`
20 | };
21 | return ddpMessage;
22 | },
23 |
24 | buildDDPMethodTester(method, paramType){
25 | let params = ['String'];
26 | if(paramType === 'number'){
27 | params = [3.14159];
28 | }
29 | if(paramType === 'object'){
30 | params = [{}];
31 | }
32 |
33 | const ddpMessage = {
34 | msg: 'method',
35 | method: method,
36 | params: params,
37 | id: `/audit/${method}/${paramType}`
38 | };
39 | return ddpMessage;
40 | }
41 | };
--------------------------------------------------------------------------------
/src/plugins/security/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_PACKAGE_LIST,
3 | SET_SECURITY_TAB,
4 | CLEAR_METHOD_SECURITY
5 | } from '../constants'
6 | import {
7 | NEW_TRACE, CLEAR_LOGS
8 | } from '../../ddp/constants/action-types';
9 | import Immutable from 'immutable'
10 |
11 |
12 | export default {
13 | packageList (state = Immutable.List(), action) {
14 | switch(action.type){
15 | case SET_PACKAGE_LIST:
16 | return Immutable.List(action.data);
17 | default:
18 | return state;
19 | }
20 | },
21 | securityTabsIndex (state = 0, action) {
22 | switch(action.type){
23 | case SET_SECURITY_TAB:
24 | return action.tab;
25 | default:
26 | return state;
27 | }
28 | },
29 | methodsSecurity (state = Immutable.Map(), action) {
30 | switch(action.type){
31 | case NEW_TRACE:
32 | if(action.trace.message && action.trace.message.msg === 'method' &&
33 | !action.trace.message.id.startsWith('/audit')){
34 | return state.set(action.trace.message.method, action.trace.message.params);
35 | } else {
36 | return state;
37 | }
38 | case CLEAR_METHOD_SECURITY:
39 | return Immutable.Map();
40 | default:
41 | return state;
42 | }
43 | },
44 | resultTraces (state = Immutable.List(), action) {
45 | switch(action.type){
46 | case NEW_TRACE:
47 | if(action.trace.message && action.trace.message.msg === 'result'){
48 | return state.push(action.trace);
49 | } else {
50 | return state;
51 | }
52 | case CLEAR_LOGS:
53 | return Immutable.List();
54 | default:
55 | return state;
56 | }
57 | }
58 | };
--------------------------------------------------------------------------------
/src/plugins/security/security.scss:
--------------------------------------------------------------------------------
1 | .security {
2 | min-height: 100%;
3 | padding-bottom: 31px;
4 |
5 | .sidebar {
6 | flex: 1;
7 | background-color: #f3f3f3;
8 |
9 | > ul {
10 | padding: 0;
11 | margin: 0;
12 | list-style: none;
13 | }
14 |
15 | li {
16 | border-bottom: 1px solid rgb(230, 230, 230);
17 | padding: 16px 0 16px 16px;
18 |
19 | &.selected {
20 | background-color: #ddd;
21 | }
22 |
23 | &:hover {
24 | cursor: pointer;
25 | }
26 | }
27 | }
28 |
29 | .Pane2 {
30 | overflow-y: scroll;
31 | }
32 |
33 | .main-panel {
34 | background-color: #f9f9f9;
35 | padding-bottom: 31px;
36 | min-height: calc(100vh - 31px);
37 |
38 |
39 | .panel-header {
40 | padding: 12px 24px;
41 | background-color: white;
42 | }
43 |
44 | h3 {
45 | font-size: 14px;
46 | margin: 0 0 10px 0;
47 | padding: 0;
48 | }
49 |
50 | .panel-header p {
51 | margin: 0 0 5px 0;
52 | padding: 0;
53 | font-size: 10px;
54 | }
55 |
56 | }
57 |
58 | .package-status, .collection-status, .methods-status {
59 | list-style: none;
60 | padding: 0;
61 | margin: 0;
62 |
63 | > li {
64 | padding: 12px;
65 | border-bottom: 1px solid rgb(230, 230, 230);
66 | background-color: #fff;
67 | display: flex;
68 |
69 | &:first-child {
70 | border-top: 1px solid rgb(230, 230, 230);
71 | }
72 | }
73 |
74 | .desc {
75 | flex: auto;
76 | margin: 0;
77 | }
78 |
79 | button {
80 | background-image: linear-gradient(hsl(0, 0%, 93%), hsl(0, 0%, 93%) 38%, hsl(0, 0%, 87%));
81 | border: 1px solid hsla(0, 0%, 0%, 0.25);
82 | border-radius: 2px;
83 | box-shadow: 0 1px 0 hsla(0, 0%, 0%, 0.08), inset 0 1px 2px hsla(0, 100%, 100%, 0.75);
84 | color: hsl(0, 0%, 27%);
85 | margin: 8px 1px 0 0;
86 | text-shadow: 0 1px 0 hsl(0, 0%, 94%);
87 | min-height: 2em !important;
88 | padding: 0 10px;
89 | -webkit-user-select: none;
90 | flex: none;
91 |
92 | &:focus, &:active {
93 | outline: none;
94 | }
95 |
96 | &:hover {
97 | background-image: linear-gradient(hsl(0, 0%, 94%), hsl(0, 0%, 94%) 38%, hsl(0, 0%, 88%));
98 | border-color: hsla(0, 0%, 0%, 0.3);
99 | box-shadow: 0 1px 0 hsla(0, 0%, 0%, 0.12), inset 0 1px 2px hsla(0, 100%, 100%, 0.95);
100 | color: hsl(0, 0%, 0%);
101 | }
102 |
103 | &:active {
104 | background-image: linear-gradient(hsl(0, 0%, 91%), hsl(0, 0%, 91%) 38%, hsl(0, 0%, 84%));
105 | box-shadow: none;
106 | text-shadow: none;
107 | }
108 | }
109 | }
110 |
111 | .operation-results {
112 | max-height: 0;
113 | overflow: hidden;
114 | transition: all ease-in 400ms;
115 | padding: 0;
116 |
117 | &.show {
118 | max-height: 200px;
119 |
120 | }
121 |
122 | li {
123 | opacity: 0;
124 | margin: 5px 10px 0 0;
125 | padding-right: 10px;
126 | display: inline-block;
127 | border-right: 1px solid #ccc;
128 | transition: all ease-in 300ms;
129 |
130 | &:last-child {
131 | border: none;
132 | margin-right: 0;
133 | }
134 |
135 | &.secure, &.insecure, &.timeout {
136 | opacity: 1;
137 | }
138 | }
139 | }
140 |
141 | .valid, .secure {
142 | color: rgb(42, 194, 57) !important;
143 | }
144 |
145 | .warning, .insecure {
146 | color: red !important;
147 | }
148 |
149 | .timeout, .caution {
150 | color: rgb(253, 177, 48) !important;
151 | }
152 |
153 | .gray {
154 | color: gray;
155 | }
156 |
157 | .arg-type {
158 | font-style: italic;
159 | color: gray;
160 | }
161 |
162 | .status {
163 | margin-left: 10px;
164 | margin-right: 15px;
165 | font-size: 8px;
166 | line-height: 14px;
167 | color: #ccc;
168 | }
169 |
170 | .check-status {
171 | font-size: 8px;
172 | line-height: 14px;
173 | vertical-align: top;
174 | margin-right: 10px;
175 | }
176 |
177 |
178 | .methods-status {
179 |
180 | > li {
181 | padding: 12px 24px;
182 | display: block;
183 | }
184 |
185 | .args {
186 | margin-top: 10px;
187 | color: gray;
188 | }
189 |
190 | .method-audit {
191 | display: flex;
192 | }
193 |
194 | .check-method {
195 | margin-right: 25px;
196 | }
197 |
198 | button {
199 | display: block;
200 | width: 130px;
201 | margin-bottom: 10px;
202 | }
203 | }
204 | }
205 |
206 |
--------------------------------------------------------------------------------
/webpack/app.dev.js:
--------------------------------------------------------------------------------
1 | // This config is used when running the monitor locally
2 | // as a standalone web application
3 |
4 | var baseConfig = require('./base');
5 | var path = require('path');
6 | var webpack = require('webpack');
7 |
8 | var srcPath =
9 |
10 | module.exports = baseConfig({
11 | entry: [
12 | 'webpack-dev-server/client?http://localhost:3000',
13 | 'webpack/hot/only-dev-server',
14 | path.join(__dirname, '../src/main.jsx')
15 | ],
16 | output: {
17 | filename: 'bundle.js',
18 | path: path.join(__dirname, 'build'),
19 | publicPath: '/build/'
20 | },
21 | devtool: '#eval-source-map',
22 | plugins: [
23 | new webpack.HotModuleReplacementPlugin(),
24 | new webpack.NoErrorsPlugin(),
25 | new webpack.DefinePlugin({
26 | 'process.env': {
27 | 'NODE_ENV': JSON.stringify('development'),
28 | }
29 | })
30 | ]
31 | });
--------------------------------------------------------------------------------
/webpack/base.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var _ = require('underscore');
3 |
4 | const srcPath = path.join(__dirname, '../src/');
5 |
6 | module.exports = function(overrides){
7 | return _.extend({
8 | entry: [
9 | srcPath + 'main.jsx'
10 | ],
11 | module: {
12 | loaders: [
13 | {
14 | test: /\.js/,
15 | loaders: ['babel'],
16 | include: srcPath
17 | },
18 | {
19 | test: /\.css$/,
20 | loader: 'style-loader!css-loader!postcss-loader'
21 | },
22 | {
23 | test: /\.scss$/,
24 | loaders: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
25 | }
26 | ]
27 | },
28 | postcss: () => [require('precss')],
29 | resolve: {
30 | extensions: ['', '.js', '.jsx']
31 | },
32 | }, overrides);
33 | };
--------------------------------------------------------------------------------
/webpack/chrome.dev.js:
--------------------------------------------------------------------------------
1 | // This config is used when testing a local build
2 | // of the extension in Chrome
3 |
4 | var baseConfig = require('./base');
5 | var path = require('path');
6 | var webpack = require('webpack');
7 |
8 | module.exports = baseConfig({
9 | output: {
10 | filename: 'bundle.js',
11 | path: path.join(__dirname, '../chrome/build/')
12 | },
13 | plugins : [
14 | new webpack.DefinePlugin({
15 | 'process.env': {
16 | 'NODE_ENV': JSON.stringify('development'),
17 | }
18 | })
19 | ],
20 | devtool: 'inline-source-map'
21 | });
--------------------------------------------------------------------------------
/webpack/chrome.inject.js:
--------------------------------------------------------------------------------
1 | // This generates an inject.js bundle for Chrome
2 |
3 |
4 | var baseConfig = require('./base');
5 | var path = require('path');
6 | var webpack = require('webpack');
7 |
8 | const srcPath = path.join(__dirname, '../src/');
9 |
10 | module.exports = baseConfig({
11 | entry: [
12 | srcPath + 'common/inject.js'
13 | ],
14 | output: {
15 | filename: 'inject.js',
16 | path: path.join(__dirname, '../chrome/build/')
17 | },
18 | plugins : [
19 | // new webpack.DefinePlugin({
20 | // 'process.env': {
21 | // 'NODE_ENV': JSON.stringify('production'),
22 | // }
23 | // }),
24 | // new webpack.optimize.DedupePlugin(),
25 | // new webpack.optimize.UglifyJsPlugin({
26 | // comments: false,
27 | // compressor: {
28 | // warnings: false
29 | // }
30 | // })
31 | ],
32 | });
--------------------------------------------------------------------------------
/webpack/chrome.prod.js:
--------------------------------------------------------------------------------
1 | // This config is used in production Chrome extension
2 |
3 | var path = require('path');
4 | var webpack = require('webpack');
5 | var baseConfig = require('./base');
6 |
7 | module.exports = baseConfig({
8 | output: {
9 | filename: 'bundle.js',
10 | path: path.join(__dirname, '../chrome/build/')
11 | },
12 | plugins : [
13 | new webpack.DefinePlugin({
14 | 'process.env': {
15 | 'NODE_ENV': JSON.stringify('production'),
16 | }
17 | }),
18 | new webpack.optimize.DedupePlugin(),
19 | new webpack.optimize.UglifyJsPlugin({
20 | comments: false,
21 | compressor: {
22 | warnings: false
23 | }
24 | })
25 | ]
26 | });
--------------------------------------------------------------------------------