├── data ├── orgs.json ├── search-index.json └── screenshot.png ├── .eslintignore ├── gulpfile.js ├── .dockerignore ├── .babelrc ├── config ├── production.json ├── development.json └── default.json ├── server ├── utils │ ├── content-to-string.js │ ├── github-auth-object.js │ ├── check-version.js │ └── ensure-directory-exists.js ├── index.js ├── app.js ├── webapp │ ├── index.html │ └── index.js ├── search-api │ ├── partial-handler.js │ ├── create-search-strings.js │ ├── index.js │ └── term-handler.js ├── component-api │ ├── fetch-module-demo.js │ ├── fetch-doc.js │ ├── update-search-index.js │ ├── index.js │ ├── process-submodules.js │ ├── fetch-repo.js │ ├── check-dependencies.js │ ├── update-handler.js │ └── fetch-usage.js ├── poll.js └── images │ └── electrode.svg ├── Dockerfile ├── scripts ├── info-module.sh ├── install-module.sh ├── prepare_nodejs.sh └── post-install-module.sh ├── client ├── components │ ├── header.jsx │ ├── page.jsx │ ├── body.jsx │ ├── home.jsx │ ├── search-bar.jsx │ ├── search.jsx │ ├── menu.jsx │ ├── revealer │ │ ├── collapsable.jsx │ │ └── index.jsx │ └── component.jsx ├── styles │ ├── base.styl │ ├── usage.styl │ ├── menu.styl │ ├── search.styl │ ├── component.styl │ └── explorer.styl ├── routes.jsx ├── store │ └── index.jsx └── app.jsx ├── component-webpack.config.js ├── package.json ├── .gitignore └── README.md /data/orgs.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /data/search-index.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | server/data 2 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require("electrode-archetype-react-app")(); 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .git 4 | .isomorphic-loader-config.json 5 | .idea 6 | 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/electrode-archetype-react-app/config/babel/.babelrc" 3 | } 4 | -------------------------------------------------------------------------------- /data/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrode-io/electrode-explorer/HEAD/data/screenshot.png -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "env": "prod" 4 | }, 5 | 6 | "ui": { 7 | "env": "production" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/utils/content-to-string.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = (base64encodedString) => 4 | new Buffer(base64encodedString, "base64").toString("ascii"); 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4.5 2 | RUN npm i -g npm@3 3 | EXPOSE 3000 4 | ENV DIR /usr/src/app 5 | RUN mkdir -p $DIR 6 | WORKDIR $DIR 7 | COPY . $DIR 8 | RUN npm install 9 | RUN $DIR/node_modules/.bin/gulp build 10 | CMD node server 11 | 12 | -------------------------------------------------------------------------------- /server/utils/github-auth-object.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Config = require("electrode-confippet").config; 4 | const ghToken = process.env[Config.GHACCESS_TOKEN_NAME]; 5 | 6 | module.exports = { 7 | type: "oauth", 8 | token: ghToken 9 | }; 10 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "env": "qa" 4 | }, 5 | "plugins": { 6 | "./server/poll": { 7 | "options": { 8 | "enable": false 9 | } 10 | } 11 | }, 12 | "ui": { 13 | "env": "development" 14 | } 15 | } -------------------------------------------------------------------------------- /server/utils/check-version.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const semver = require("semver"); 3 | 4 | const checkVersion = (wanted, got) => { 5 | return { 6 | status: semver.satisfies(wanted, got) ? "ok" : "bad", 7 | str: got 8 | }; 9 | }; 10 | 11 | module.exports = checkVersion; 12 | -------------------------------------------------------------------------------- /scripts/info-module.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "No module name specified. Exiting." 5 | exit 1 6 | fi 7 | 8 | if which npm; then 9 | npm info $1 10 | else 11 | source `pwd`/scripts/prepare_nodejs.sh 12 | prepare_nodejs 13 | npm info $1 14 | fi 15 | -------------------------------------------------------------------------------- /client/components/header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const explorerHeader = () => { 4 | return ( 5 |
6 |

7 | 8 |

9 |
10 | ); 11 | }; 12 | 13 | export default explorerHeader; 14 | -------------------------------------------------------------------------------- /server/utils/ensure-directory-exists.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Fs = require("fs"); 4 | 5 | module.exports = (path) => { 6 | 7 | try { 8 | 9 | // Try to stat the directory 10 | Fs.statSync(path); 11 | 12 | } catch (err) { 13 | 14 | // Error indicates it doesn't exist 15 | // So we create it 16 | Fs.mkdirSync(path); 17 | 18 | } 19 | 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /client/components/page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Body from "./body"; 4 | 5 | export const Page = (props) => { 6 | return ( 7 | 8 | {props.children} 9 | 10 | ); 11 | }; 12 | 13 | Page.propTypes = { 14 | children: React.PropTypes.oneOfType([ 15 | React.PropTypes.arrayOf(React.PropTypes.node), 16 | React.PropTypes.node 17 | ]) 18 | }; 19 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var extendRequire = require("isomorphic-loader/lib/extend-require"); 3 | extendRequire() 4 | .then(function () { 5 | /*eslint-disable*/ 6 | require("babel-core/register"); 7 | require("electrode-server")(require("electrode-confippet").config, [require("electrode-static-paths")()]); 8 | /*eslint-enable*/ 9 | }) 10 | .catch(function (err) { 11 | console.log("extendRequire failed", err.stack); // eslint-disable-line no-console 12 | }); 13 | -------------------------------------------------------------------------------- /client/styles/base.styl: -------------------------------------------------------------------------------- 1 | @require "./component"; 2 | @require "./explorer"; 3 | @require "./menu"; 4 | @require "./search"; 5 | @require "./usage"; 6 | 7 | body { 8 | background:#efefef; 9 | margin-top: -20px; 10 | } 11 | 12 | a { 13 | color: #007dc6; 14 | text-decoration: none; 15 | } 16 | 17 | button, html, input, select, textarea { 18 | color: #444; 19 | font-family: myriad-pro,Helvetica Neue,Helvetica,Arial,sans-serif; 20 | } 21 | 22 | em { 23 | font-style: normal; 24 | font-weight: 700; 25 | } 26 | -------------------------------------------------------------------------------- /client/components/body.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./header"; 3 | import SearchBar from "./search-bar"; 4 | import Menu from "./menu"; 5 | 6 | const Body = (props) => { 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 | {props.children} 14 |
15 |
16 | ); 17 | }; 18 | 19 | Body.propTypes = { 20 | children: React.PropTypes.node 21 | }; 22 | 23 | export default Body; 24 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Promise = require("bluebird"); 4 | const ReduxRouterEngine = require("electrode-redux-router-engine"); 5 | const routes = require("../client/routes").routes; 6 | const configureStore = require("../client/store").configureStore; 7 | 8 | const createReduxStore = () => { 9 | const store = configureStore(); 10 | return Promise.resolve(store); 11 | }; 12 | 13 | const engine = new ReduxRouterEngine({routes, createReduxStore}); 14 | 15 | module.exports = (req) => { 16 | return engine.render(req); 17 | }; 18 | -------------------------------------------------------------------------------- /server/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electrode explorer 6 | 7 | 8 | {{PREFETCH_BUNDLES}} 9 | {{WEBAPP_BUNDLES}} 10 | 11 | 12 |
{{SSR_CONTENT}}
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /scripts/install-module.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "No module name specified. Exiting." 5 | exit 1 6 | fi 7 | 8 | function npm_install() { 9 | echo "installing $1" 10 | if which npm; then 11 | npm i $1 12 | npm i --only=dev $1 13 | else 14 | source `pwd`/scripts/prepare_nodejs.sh 15 | prepare_nodejs 16 | npm i $1 17 | npm i --only=dev $1 18 | fi 19 | } 20 | 21 | if [ -z "$2" ]; then 22 | version="latest" 23 | else 24 | version=^$2 25 | fi 26 | 27 | npm_install $1@$version 28 | `pwd`/scripts/post-install-module.sh $1 $2 29 | -------------------------------------------------------------------------------- /client/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, IndexRoute} from "react-router"; 3 | 4 | import { Page } from "./components/page"; 5 | import { Home } from "./components/home"; 6 | import Component from "./components/component"; 7 | import Search from "./components/search"; 8 | 9 | export const routes = ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /component-webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const config = require("electrode-archetype-react-component/config/webpack/webpack.config"); 4 | 5 | module.exports = Object.assign(config, { 6 | externals: [ 7 | { 8 | "react": { 9 | root: "React", 10 | commonjs2: "react", 11 | commonjs: "react", 12 | amd: "react" 13 | } 14 | }, 15 | { 16 | "react-dom": { 17 | root: "ReactDOM", 18 | commonjs2: "react-dom", 19 | commonjs: "react-dom", 20 | amd: "react-dom" 21 | } 22 | } 23 | ], 24 | plugins: [] 25 | }); 26 | -------------------------------------------------------------------------------- /server/search-api/partial-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function PartialHandler (request, reply) { 4 | 5 | const results = []; 6 | const part = request.params.part; 7 | const matches = request.server.settings.app.searchStrings || {}; 8 | 9 | const subMatch = matches[part.substr(0,2)]; 10 | 11 | if (subMatch) { 12 | const pattern = new RegExp(`^${part}`); 13 | const returned = {}; 14 | subMatch.map((str) => { 15 | if (!returned[str] && pattern.test(str)) { 16 | results.push(str); 17 | returned[str] = 1; 18 | } 19 | }); 20 | } 21 | 22 | return reply(results); 23 | }; 24 | -------------------------------------------------------------------------------- /client/store/index.jsx: -------------------------------------------------------------------------------- 1 | import ExEnv from "exenv"; 2 | import thunkMiddleware from "redux-thunk"; 3 | import { createStore, applyMiddleware } from "redux"; 4 | 5 | const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore); 6 | 7 | const reducer = () => {}; 8 | 9 | export const configureStore = (initialState) => { 10 | if (process.env.NODE_ENV !== "production" && ExEnv.canUseDOM) { 11 | return createStoreWithMiddleware( 12 | reducer, 13 | initialState, 14 | window.devToolsExtension ? window.devToolsExtension() : (f) => f 15 | ); 16 | } 17 | return createStoreWithMiddleware(reducer, initialState); 18 | }; 19 | -------------------------------------------------------------------------------- /client/styles/usage.styl: -------------------------------------------------------------------------------- 1 | .usage-detail { 2 | margin:10px 0; 3 | } 4 | 5 | .detail-uri { 6 | font-size:0.9em; 7 | } 8 | 9 | .detail-version { 10 | font-family:monospace; 11 | padding:0 20px; 12 | } 13 | 14 | .detail-description { 15 | color:#666; 16 | font-size:0.9em; 17 | } 18 | 19 | .component-consumption { 20 | position: absolute; 21 | top: 175px; 22 | right: 0; 23 | width: 24%; 24 | } 25 | 26 | .component-consumption .table { 27 | border-bottom:0; 28 | } 29 | 30 | .component-consumption .table td:first-child { 31 | padding-left: 0; 32 | } 33 | 34 | .version-status-ok { 35 | color:green; 36 | } 37 | 38 | .version-status-bad { 39 | color:red; 40 | } 41 | -------------------------------------------------------------------------------- /server/search-api/create-search-strings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const strings = {}; 4 | 5 | const indexString = (str) => { 6 | 7 | const key = str.substr(0,2); 8 | 9 | if (!strings[key]) { 10 | strings[key] = []; 11 | } 12 | 13 | strings[key].push(str); 14 | 15 | }; 16 | 17 | const CreateSearchStrings = (index) => { 18 | 19 | Object.keys(index).map((module) => { 20 | 21 | const parts = module.split("/"); 22 | const unNamespacedModule = parts[parts.length - 1]; 23 | 24 | indexString(unNamespacedModule); 25 | index[module].map((str) => indexString(str)); 26 | 27 | }); 28 | 29 | return strings; 30 | }; 31 | 32 | module.exports = CreateSearchStrings; 33 | 34 | -------------------------------------------------------------------------------- /scripts/prepare_nodejs.sh: -------------------------------------------------------------------------------- 1 | function prepare_nodejs() { 2 | local version; 3 | if [ -n "$NODE_VERSION" ]; then 4 | version=$NODE_VERSION 5 | else 6 | version=4 7 | fi 8 | 9 | if [ -z `command -v nvm` ]; then 10 | local NVM_DIR="/usr/local/nvm" 11 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.7/install.sh | NVM_DIR=$NVM_DIR bash 12 | [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" 13 | fi 14 | 15 | nvm install $version 16 | 17 | npm set progress false 18 | npm set strict-ssl false 19 | 20 | local NPM_MAJOR=$(npm --version | cut -f1 -d.) 21 | if [ $version -ge 4 ]; then 22 | if [ $NPM_MAJOR -lt 3 ]; then 23 | echo "NPM major version is $NPM_MAJOR, upgrading to npm@3" 24 | npm install -g npm@^3 25 | fi 26 | fi 27 | } 28 | -------------------------------------------------------------------------------- /server/component-api/fetch-module-demo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Path = require("path"); 4 | const execFile = require("child_process").execFile; 5 | const processSubModules = require("./process-submodules"); 6 | 7 | const saveModuleDemo = (meta, majorVersion, server, keywords) => { 8 | 9 | const moduleName = meta.name; 10 | const command = [Path.join(__dirname, "../../scripts/install-module.sh"), moduleName]; 11 | 12 | if (majorVersion) { 13 | command.push(majorVersion); 14 | } 15 | 16 | execFile("bash", command, (error) => { 17 | if (error) { 18 | console.log(`npm install failed for this module, error:\n${error}`); 19 | throw error; 20 | } 21 | 22 | console.log(`${moduleName}: npm install finished.`); 23 | 24 | processSubModules(moduleName, meta.github, server, keywords); 25 | }); 26 | 27 | }; 28 | 29 | module.exports = saveModuleDemo; 30 | -------------------------------------------------------------------------------- /client/app.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Client entry point. 3 | */ 4 | 5 | /* globals document global */ 6 | 7 | import React from "react"; 8 | 9 | import { routes } from "./routes"; 10 | import { Router } from "react-router"; 11 | import ReactDOM from "react-dom"; 12 | import { Provider } from "react-redux"; 13 | import { Resolver } from "react-resolver"; 14 | import { createHistory } from "history"; 15 | import { configureStore } from "./store"; 16 | 17 | import "./styles/base.styl"; 18 | 19 | const store = configureStore({}); 20 | 21 | global.React = React; 22 | global.ReactDOM = ReactDOM; 23 | 24 | global.webappStart = function () { 25 | const rootEl = document.querySelector(".js-content"); 26 | Resolver.render( 27 | () => ( 28 | 29 | {routes} 30 | 31 | ), 32 | rootEl 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /server/component-api/fetch-doc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const GitHubApi = require("github"); 4 | const Config = require("electrode-confippet").config; 5 | const github = new GitHubApi(Config.githubApi); 6 | const githubAuthObject = require("../utils/github-auth-object"); 7 | const contentToString = require("../utils/content-to-string"); 8 | 9 | const fetchDoc = (request, reply) => { 10 | 11 | github.authenticate(githubAuthObject); 12 | 13 | const { org, repoName } = request.params; 14 | 15 | const opts = { 16 | user: org, 17 | repo: repoName, 18 | path: "components.md" 19 | }; 20 | 21 | return github.repos.getContent(opts, (err, response) => { 22 | 23 | if (err) { 24 | return reply("An error occured").code(err.code || 500); 25 | } 26 | 27 | const doc = contentToString(response.content); 28 | 29 | reply({doc}); 30 | }); 31 | }; 32 | 33 | module.exports = fetchDoc; 34 | -------------------------------------------------------------------------------- /client/styles/menu.styl: -------------------------------------------------------------------------------- 1 | .explorer-menu { 2 | background-color: transparent; 3 | position: absolute; 4 | width: 15%; 5 | margin-top: 2px; 6 | border-right: 1px solid #e5e5e5; 7 | } 8 | 9 | .explorer-menu ul { 10 | list-style-type: none; 11 | margin: 10px 0; 12 | padding-left: 20px; 13 | } 14 | 15 | .explorer-menu li { 16 | padding: 0 0 5px 10px; 17 | text-transform: capitalize; 18 | font-size: 0.9em; 19 | } 20 | 21 | .explorer-menu li a { 22 | color: #606060; 23 | } 24 | 25 | .explorer-menu li a:hover { 26 | text-decoration: underline; 27 | } 28 | 29 | .menu-link { 30 | color: white; 31 | margin: 0 10%; 32 | } 33 | 34 | .explorer-menu h4 { 35 | margin-left: 20px; 36 | text-transform: capitalize; 37 | } 38 | 39 | .explorer-menu ul.menu-submodules { 40 | margin: 5px 0 0 0; 41 | padding: 0; 42 | 43 | li { 44 | margin: 0; 45 | padding: 2px 10px; 46 | font-size:0.9em; 47 | 48 | a { 49 | color: #999; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /client/components/home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { canUseDOM } from "exenv"; 4 | import fetch from "isomorphic-fetch"; 5 | 6 | const random = (arr) => arr[Math.floor(Math.random() * arr.length)]; 7 | 8 | export class Home extends React.Component { 9 | render() { 10 | if (canUseDOM) { 11 | const host = window.location.origin; 12 | const url = `${host}/data/orgs.json`; 13 | 14 | fetch(url) 15 | .then((res) => { 16 | if (res.status >= 400) { 17 | throw res; 18 | } 19 | return res.json(); 20 | }) 21 | .then((res) => { 22 | const org = random(Object.keys(res.allOrgs || {})); 23 | if (org) { 24 | const repo = random(Object.keys(res.allOrgs[org].repos)); 25 | return window.location.pathname = `/${org}/${repo}`; 26 | } 27 | }); 28 | } 29 | 30 | return ( 31 | Explorer! 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "inert": { 4 | "enable": true 5 | }, 6 | "./server/search-api": {}, 7 | "./server/component-api": {}, 8 | "./server/poll": { 9 | "options": { 10 | "enable": true 11 | } 12 | }, 13 | "./server/webapp": { 14 | "options": { 15 | "pageTitle": "Electrode Explorer", 16 | "devServer": { 17 | "port": "2992" 18 | }, 19 | "paths": { 20 | "/{args*}": { 21 | "view": "index", 22 | "content": { 23 | "module": "./server/app" 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | 31 | "githubApi": { 32 | "version": "3.0.0", 33 | "pathPrefix": "", 34 | "protocol": "https", 35 | "host": "api.github.com" 36 | }, 37 | 38 | "ORGS": [ 39 | 40 | ], 41 | 42 | "REPOS_USAGE_INCLUDE": [ 43 | 44 | ], 45 | 46 | "REPOS_USAGE_EXCLUDE": [ 47 | 48 | ], 49 | 50 | "MODULE_PREFIXES_INCLUDE": [ 51 | 52 | ], 53 | 54 | "NPM_WAITING_TIME": 300000, 55 | 56 | "GHACCESS_TOKEN_NAME": "GHACCESS_TOKEN" 57 | } 58 | -------------------------------------------------------------------------------- /client/styles/search.styl: -------------------------------------------------------------------------------- 1 | 2 | .explorer-search-bar { 3 | background: #093252; 4 | padding: 1px 15px 11px; 5 | } 6 | 7 | .tt-dropdown-menu { 8 | background: #fff; 9 | box-shadow: 2px 2px 2px #555; 10 | } 11 | 12 | .tt-suggestion { 13 | padding: 20px; 14 | border-bottom: 1px solid #eee; 15 | } 16 | 17 | .tt-suggestion:last-of-type { 18 | border-bottom: 0; 19 | } 20 | 21 | .tt-suggestion.tt-cursor { 22 | background: #ffc120; 23 | } 24 | 25 | .search-results { 26 | padding: 0 20px; 27 | } 28 | 29 | .search-results h2 em { 30 | font-style: italic; 31 | } 32 | 33 | .search-results h3 { 34 | font-weight: 200; 35 | } 36 | 37 | .search-results h3 em { 38 | font-style: normal; 39 | font-weight: 700; 40 | } 41 | 42 | .results-list { 43 | margin-top: 20px; 44 | } 45 | 46 | .results-list a { 47 | pointer:hand; 48 | } 49 | 50 | .search-result { 51 | padding: 20px 40px; 52 | border-bottom: 1px solid #ccc; 53 | font-size: 1.2em; 54 | } 55 | 56 | .search-input { 57 | margin-top: 8px; 58 | width: 100%; 59 | padding: 8px; 60 | border-radius: 4px; 61 | background-color: #fff; 62 | border-color: #c2cfd6; 63 | border: 1px solid; 64 | } 65 | -------------------------------------------------------------------------------- /scripts/post-install-module.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "No module name specified. Exiting." 5 | exit 1 6 | fi 7 | 8 | function update_content() { 9 | files=`find $1 -type f -name "$2"` 10 | for f in $files; do 11 | sed -i "" $3 $f 12 | sed -i $3 $f 13 | done 14 | } 15 | 16 | function add_global() { 17 | file=node_modules/$1/demo/EXPLORER.js 18 | rm $file 19 | echo "var demo=require(\"./demo\").default;global._COMPONENTS=global._COMPONENTS||{};global._COMPONENTS[\"$1\"]=demo;" >> $file 20 | } 21 | 22 | function run_babel() { 23 | rm node_modules/$1/.babelrc 24 | `pwd`/node_modules/.bin/babel node_modules/$1/demo -d node_modules/$1/demo 25 | } 26 | 27 | function build() { 28 | run_babel $1 29 | 30 | update_content node_modules/$1/demo "*.js" 's/\.jsx//g' 31 | 32 | cp -r node_modules/$1/lib/* node_modules/$1/src 33 | 34 | add_global $1 35 | 36 | outputPath="data/demo-modules/$1/v$2" 37 | echo "Webpack running for $1"; 38 | `pwd`/node_modules/.bin/webpack --config ./component-webpack.config.js --colors --entry node_modules/$1/demo/EXPLORER.js --output-path $outputPath 39 | echo "Webpack finished for $1"; 40 | } 41 | 42 | build $1 $2 43 | -------------------------------------------------------------------------------- /client/styles/component.styl: -------------------------------------------------------------------------------- 1 | .component-documentation { 2 | background: white; 3 | padding: 1px 40px 20px 40px; 4 | margin-top: 40px; 5 | position: relative; 6 | } 7 | 8 | .component-documentation>div { 9 | border-bottom:1px solid #ececec; 10 | padding: 40px 0 80px 0; 11 | 12 | &:last-of-type {border-bottom:0;} 13 | } 14 | 15 | .component-documentation>div>h3 { 16 | margin-bottom:20px; 17 | } 18 | 19 | .component-version { 20 | font-weight: 300; 21 | color: #999; 22 | font-size:0.9em; 23 | } 24 | 25 | .component-description { 26 | color: #888; 27 | display: block; 28 | font-size: 0.8em; 29 | font-weight: 200; 30 | } 31 | 32 | .component-usage { 33 | margin: 0; 34 | } 35 | 36 | .switch-version { 37 | font-size: 16px; 38 | float: right; 39 | } 40 | 41 | .switch-version-text { 42 | margin-right: 6px; 43 | color: #444; 44 | } 45 | 46 | .previewArea { 47 | padding-top:40px; 48 | } 49 | 50 | .code-well { 51 | font-family: monospace; 52 | background: #ccc; 53 | border-color: #aaa; 54 | padding: 20px; 55 | margin: 20px 0; 56 | border: 2px solid #dfebed; 57 | border-radius: 10px; 58 | } 59 | 60 | .demo { 61 | margin-top: 20px; 62 | margin-left: 1%; 63 | } 64 | -------------------------------------------------------------------------------- /server/component-api/update-search-index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Path = require("path"); 4 | const fs = require("fs"); 5 | const Promise = require("bluebird"); 6 | const readFile = Promise.promisify(fs.readFile); 7 | const Chalk = require("chalk"); 8 | 9 | const CreateSearchStrings = require("../search-api/create-search-strings"); 10 | 11 | const IndexPath = Path.join(__dirname, "../../data/search-index.json"); 12 | 13 | const UpdateSearchIndex = (moduleName, subModules, server, keywords) => { 14 | 15 | let searchIndex = {}; 16 | 17 | return readFile(IndexPath) 18 | .then((data) => { 19 | searchIndex = JSON.parse(data); 20 | 21 | searchIndex[moduleName] = (keywords || []).concat(subModules); 22 | 23 | // Directly update server app stored value so that it's available immediately 24 | server.settings.app.searchIndex = searchIndex; 25 | server.settings.app.searchStrings = CreateSearchStrings(searchIndex); 26 | 27 | // Write to file to persist data 28 | fs.writeFileSync(IndexPath, JSON.stringify(searchIndex)); 29 | console.log(Chalk.green(`Successfully wrote search index to ${IndexPath}`)); 30 | }) 31 | .catch(() => {}); 32 | }; 33 | 34 | module.exports = UpdateSearchIndex; 35 | -------------------------------------------------------------------------------- /server/component-api/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* Handles updating (or creating) the component 4 | * data for demoing. Implemented in the server 5 | * as we want to be able to flash updates after 6 | * the server is up and running, without requiring 7 | * a restart */ 8 | const Path = require("path"); 9 | const updateHandler = require("./update-handler"); 10 | const fetchDocHandler = require("./fetch-doc"); 11 | 12 | const ComponentData = {}; 13 | 14 | ComponentData.register = (server, options, next) => { 15 | 16 | server.route({ 17 | path: "/api/update/{org}/{repoName}", 18 | method: "POST", 19 | handler: updateHandler 20 | }); 21 | 22 | server.route({ 23 | path: "/api/doc/{org}/{repoName}", 24 | method: "GET", 25 | handler: fetchDocHandler 26 | }); 27 | 28 | server.route({ 29 | method: "GET", 30 | path: "/data/{param*}", 31 | handler: { 32 | directory: { 33 | path: Path.join(__dirname, "../../data"), 34 | listing: true 35 | } 36 | } 37 | }); 38 | 39 | server.route({ 40 | method: "GET", 41 | path: "/img/electrode.svg", 42 | handler: function (request, reply) { 43 | reply.file(Path.join(__dirname, "../images/electrode.svg")); 44 | } 45 | }); 46 | 47 | return next(); 48 | 49 | }; 50 | 51 | ComponentData.register.attributes = { 52 | name: "componentData", 53 | version: "1.0.0" 54 | }; 55 | 56 | module.exports = ComponentData; 57 | -------------------------------------------------------------------------------- /server/search-api/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Path = require("path"); 4 | const Chalk = require("chalk"); 5 | const Promise = require("bluebird"); 6 | const readFile = Promise.promisify(require("fs").readFile); 7 | 8 | const TermHandler = require("./term-handler"); 9 | const PartialHandler = require("./partial-handler"); 10 | const CreateSearchStrings = require("./create-search-strings"); 11 | 12 | const searchIndexPath = Path.join(__dirname, "../../data/search-index.json"); 13 | 14 | const SearchApi = {}; 15 | 16 | SearchApi.register = (server, options, next) => { 17 | 18 | readFile(searchIndexPath) 19 | .then((res) => { 20 | const searchIndex = JSON.parse(res); 21 | server.settings.app.searchIndex = searchIndex; 22 | server.settings.app.searchStrings = CreateSearchStrings(searchIndex); 23 | }) 24 | .catch((err) => { 25 | console.log("search Index loading error:", err); 26 | console.log(Chalk.red("WARNING: No search index file exists. Search will be non-functional")); 27 | console.log(Chalk.red(`Expected search index file path: ${searchIndexPath}`)); 28 | 29 | server.settings.app.searchIndex = {}; 30 | }); 31 | 32 | server.route({ 33 | path: "/api/search/partial/{part}", 34 | method: "GET", 35 | handler: PartialHandler 36 | }); 37 | 38 | server.route({ 39 | path: "/api/search/term/{term}", 40 | method: "GET", 41 | handler: TermHandler 42 | }); 43 | 44 | return next(); 45 | 46 | }; 47 | 48 | SearchApi.register.attributes = { 49 | name: "portalSearchApi", 50 | version: "1.0.0" 51 | }; 52 | 53 | module.exports = SearchApi; 54 | 55 | -------------------------------------------------------------------------------- /server/search-api/term-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const doSearch = (term, key, pool, omitMatches) => { 4 | 5 | const pattern = new RegExp(`^(${pool.join("|")})$`, "g"); 6 | const m = term.match(pattern); 7 | 8 | const matchesObj = { "module": key }; 9 | 10 | if (!omitMatches) { 11 | matchesObj.matches = m; 12 | } else { 13 | matchesObj.isModule = true; 14 | } 15 | 16 | return m && matchesObj; 17 | 18 | }; 19 | 20 | module.exports = function TermHandler (request, reply) { 21 | 22 | const term = request.params.term; 23 | 24 | if (!term) { 25 | return reply({ err: "No search term specified" }); 26 | } 27 | 28 | const results = { term }; 29 | 30 | const searchIndex = request.server.settings.app.searchIndex || {}; 31 | 32 | const moduleKeys = Object.keys(searchIndex); 33 | const mkLength = moduleKeys.length; 34 | let leftPointer = 0; 35 | let rightPointer = mkLength - 1; 36 | 37 | results.matched = []; 38 | 39 | while (leftPointer < mkLength && rightPointer > leftPointer) { 40 | const leftKey = moduleKeys[leftPointer]; 41 | const rightKey = moduleKeys[rightPointer]; 42 | 43 | [leftKey, rightKey].map((key) => { 44 | const keywordMatches = doSearch(term, key, searchIndex[key]); 45 | const moduleMatches = doSearch(term, key, key.split('/'), true); 46 | 47 | [ 48 | doSearch(term, key, searchIndex[key]), 49 | doSearch(term, key, key.split('/'), true) 50 | ].map((matches) => matches && results.matched.push(matches)); 51 | 52 | }); 53 | 54 | ++leftPointer; 55 | --rightPointer; 56 | } 57 | 58 | results.count = results.matched.length; 59 | 60 | return reply(results); 61 | }; 62 | 63 | -------------------------------------------------------------------------------- /server/component-api/process-submodules.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Path = require("path"); 4 | const fs = require("fs"); 5 | const Promise = require("bluebird"); 6 | const readFile = Promise.promisify(fs.readFile); 7 | const Babel = require("babel-core"); 8 | const traverse = require("babel-traverse").default; 9 | const UpdateSearchIndex = require("./update-search-index"); 10 | 11 | const ProcessSubModules = (moduleName, github, server, keywords) => { 12 | const subModules = []; 13 | 14 | const demoFile = Path.join(__dirname, `../../node_modules/${moduleName}/demo/demo.jsx`); 15 | const orgFile = Path.join(__dirname, "../../data/orgs.json"); 16 | 17 | const parts = github.split("/"); 18 | const moduleOrg = parts[3]; 19 | const moduleRepo = parts[4]; 20 | 21 | Babel.transformFile(demoFile, {presets: ["es2015", "react"]}, (err, result) => { 22 | if (err) { 23 | console.log("error processing submodules for ", moduleName); 24 | return console.log(err); 25 | } 26 | 27 | const visitor = { 28 | enter(path) { 29 | if (path.node.key && path.node.key.name === "title") { 30 | subModules.push(path.node.value.value); 31 | } 32 | } 33 | }; 34 | 35 | traverse(result.ast, visitor); 36 | 37 | UpdateSearchIndex(`${moduleOrg}/${moduleRepo}`, subModules, server, keywords); 38 | 39 | if (subModules.length < 3) { 40 | // Denotes no actual submodules 41 | return; 42 | } 43 | 44 | readFile(orgFile) 45 | .then((data) => { 46 | const orgs = JSON.parse(data); 47 | orgs.allOrgs[moduleOrg].repos[moduleRepo].submodules = 48 | subModules.filter((sm, i)=>i); 49 | 50 | fs.writeFileSync(orgFile, JSON.stringify(orgs)); 51 | }) 52 | .catch((e) => { 53 | console.error("Problem checking org map", e); 54 | }); 55 | }); 56 | 57 | }; 58 | 59 | module.exports = ProcessSubModules; 60 | -------------------------------------------------------------------------------- /client/components/search-bar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ExecutionEnvironment from "exenv"; 3 | import Typeahead from "radon-typeahead"; 4 | import fetch from "isomorphic-fetch"; 5 | 6 | export default class Component extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | list: [], 11 | host: ExecutionEnvironment.canUseDOM ? 12 | window.location.origin : 13 | "http://localhost:3000" 14 | }; 15 | 16 | this._fetchSuggestions = this._fetchSuggestions.bind(this); 17 | } 18 | 19 | _fetchSuggestions(part) { 20 | if (part.length < 2) { 21 | return; 22 | } 23 | 24 | const { host } = this.state; 25 | 26 | return fetch(`${host}/api/search/partial/${part}`) 27 | .then((res) => { 28 | if (res.status >= 400) { 29 | throw res; 30 | } 31 | return res.json(); 32 | }) 33 | .then((results) => { 34 | this.setState({list: results}); 35 | }).catch((err) => { 36 | console.error(err); 37 | }); 38 | } 39 | 40 | _performSearch(term) { 41 | if (ExecutionEnvironment.canUseDOM) { 42 | window.location = "/search/" + term; 43 | } 44 | } 45 | 46 | render() { 47 | return ( 48 |
49 | 62 | } 63 | onChange={this._fetchSuggestions} 64 | onSelectOption={this._performSearch} 65 | isRequiredField={false}/> 66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electrode-explorer", 3 | "version": "1.0.0", 4 | "main": "server/index.js", 5 | "description": "Showcase/Explorer for Electrode components", 6 | "engine": { 7 | "node": ">=4.0.0", 8 | "npm": ">=3.0.0" 9 | }, 10 | "scripts": { 11 | "build": "gulp build", 12 | "dev": "gulp dev", 13 | "help": "gulp help", 14 | "hot": "gulp hot" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/electrode-io/electrode-explorer.git" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "webpack", 23 | "electrode", 24 | "showcase", 25 | "catalog", 26 | "example", 27 | "explore", 28 | "demo" 29 | ], 30 | "author": "Caoyang Shi ", 31 | "contributors": [ 32 | { 33 | "name": "Caoyang Shi", 34 | "email": "cshi@walmartlabs.com" 35 | }, 36 | { 37 | "name": "Dave Stevens", 38 | "email": "dstevens@walmartlabs.com" 39 | } 40 | ], 41 | "license": "Apache-2.0", 42 | "bugs": { 43 | "url": "https://github.com/electrode-io/electrode-explorer/issues" 44 | }, 45 | "dependencies": { 46 | "babel-traverse": "^6.10.4", 47 | "bluebird": "^3", 48 | "classnames": "^2.2.5", 49 | "electrode-archetype-react-component": "^1.0.0", 50 | "electrode-archetype-react-component-dev": "^1.0.0", 51 | "electrode-confippet": "^1.5.0", 52 | "electrode-redux-router-engine": "^1.0.0", 53 | "electrode-server": "^1.0.0", 54 | "electrode-static-paths": "^1.0.0", 55 | "exenv": "^1.2.0", 56 | "github": "^1.1.2", 57 | "history": "~1.13.1", 58 | "inert": "^4.0.1", 59 | "isomorphic-fetch": "^2.2.1", 60 | "marked": "^0.3.6", 61 | "mkdirp": "^0.5.1", 62 | "radon-typeahead": "^0.3.1", 63 | "react": "^15.3.1", 64 | "react-dom": "^15.3.1", 65 | "react-tween-state": "^0.1.5", 66 | "redux-thunk": "^2.1.0", 67 | "semver": "^5.2.0" 68 | }, 69 | "devDependencies": { 70 | "electrode-archetype-react-app": "^1.0.0", 71 | "electrode-archetype-react-app-dev": "^1.0.0", 72 | "gulp": "^3.9.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/component-api/fetch-repo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const GitHubApi = require("github"); 4 | const Promise = require("bluebird"); 5 | const Config = require("electrode-confippet").config; 6 | const github = new GitHubApi(Config.githubApi); 7 | const githubAuthObject = require("../utils/github-auth-object"); 8 | const contentToString = require("../utils/content-to-string"); 9 | 10 | const fetchUsage = require("./fetch-usage"); 11 | const checkDependencies = require("./check-dependencies"); 12 | 13 | const extractMetaData = (pkg, repoUrl) => { 14 | return { 15 | name: pkg.name, 16 | title: pkg.title, 17 | description: pkg.description, 18 | github: repoUrl, 19 | version: pkg.version 20 | }; 21 | }; 22 | 23 | const fetchRepo = (org, repoName) => { 24 | 25 | github.authenticate(githubAuthObject); 26 | 27 | const opts = { 28 | user: org, 29 | repo: repoName, 30 | path: "package.json" 31 | }; 32 | 33 | return new Promise((resolve, reject) => { 34 | github.repos.getContent(opts, (err, response) => { 35 | 36 | if (err) { 37 | console.log("error fetchRepo", err); 38 | return reject(err); 39 | } 40 | 41 | const packageContent = contentToString(response.content); 42 | 43 | let meta; 44 | let pkg = {}; 45 | 46 | try { 47 | pkg = JSON.parse(packageContent); 48 | meta = extractMetaData(pkg, response.html_url.replace("blob/master/package.json", "")); 49 | } catch (err) { 50 | console.error("Error parsing package.json", err); 51 | return reject(new Error("Could not get package.json as JSON")); 52 | } 53 | 54 | return Promise.all([ 55 | fetchUsage(meta), 56 | checkDependencies(`${org}/${repoName}`, pkg.dependencies, pkg.devDependencies) 57 | ]) 58 | .spread((usage) => { 59 | return resolve({ meta, usage, pkg }); 60 | }).catch((err) => { 61 | console.error(`Error fetching demo index for ${org}/${repoName}`, err); 62 | return reject(err); 63 | }); 64 | }); 65 | }); 66 | }; 67 | 68 | module.exports = fetchRepo; 69 | -------------------------------------------------------------------------------- /client/styles/explorer.styl: -------------------------------------------------------------------------------- 1 | .explorer-header { 2 | background: white; 3 | padding-top: 20px; 4 | } 5 | 6 | .explorer-title { 7 | margin-top: 20px; 8 | margin-left: 1%; 9 | } 10 | 11 | .explorer-header>h1 { 12 | text-align: center; 13 | 14 | img { 15 | height: 89px; 16 | } 17 | } 18 | 19 | .explorer-body { 20 | background: #efefef; 21 | padding: 0 25.5% 100px 15%; 22 | min-height: 700px; 23 | } 24 | 25 | .explorer-body span.component-info { 26 | color: #808080; 27 | font-weight: normal; 28 | font-size: 0.6em; 29 | display: block; 30 | } 31 | 32 | .nav-link { 33 | display: block; 34 | position: relative; 35 | font-size: 14px; 36 | font-weight: 400; 37 | text-decoration: none; 38 | cursor: pointer; 39 | text-transform: capitalize; 40 | } 41 | 42 | .nav-link:hover { 43 | color: #fff; 44 | background-color: #0070b2; 45 | } 46 | 47 | .explorer-footer { 48 | background-color: #f2f8fd; 49 | } 50 | 51 | .footer-container { 52 | padding-left: 32px; 53 | padding-right: 32px; 54 | } 55 | 56 | .footer-text { 57 | color: #888; 58 | font-size: 13px; 59 | line-height: 40px; 60 | padding-left: 20px; 61 | } 62 | 63 | .revealer-footer { 64 | text-align:left; 65 | } 66 | 67 | .module { 68 | font-weight: 200; 69 | } 70 | 71 | .module em { 72 | font-weight: 700; 73 | } 74 | 75 | .matches { 76 | font-style: italic; 77 | } 78 | 79 | .location { 80 | font-weight: 700; 81 | } 82 | 83 | .chooser { 84 | display: inline-block; 85 | vertical-align: middle; 86 | height: 30px; 87 | position: relative; 88 | border: 1px solid #c2cfd6; 89 | } 90 | 91 | .button { 92 | background-color: #fff; 93 | color: #007dc6; 94 | border: 1px solid #d9d9d9; 95 | font-size: 15px; 96 | font-weight: 600; 97 | border-radius: 4px; 98 | outline: none; 99 | -webkit-font-smoothing: antialiased; 100 | display: inline-block; 101 | height: 38px; 102 | padding: 0 34px; 103 | line-height: 1; 104 | text-align: center; 105 | vertical-align: middle; 106 | white-space: nowrap; 107 | transition-property: background-color,opacity,border-color,color; 108 | transition-duration: .12s; 109 | transition-timing-function: ease-out; 110 | } 111 | -------------------------------------------------------------------------------- /client/components/search.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ExecutionEnvironment from "exenv"; 3 | import fetch from "isomorphic-fetch"; 4 | 5 | const Results = (props) => { 6 | const { 7 | term, 8 | count, 9 | matched 10 | } = props; 11 | 12 | return ( 13 |
14 |

You searched for {term}

15 |

16 | There 17 | {count > 1 ? " were " : " was "} 18 | {count ? count : "no" } 19 | result{count > 1 ? "s" : ""}:

20 | {count && 21 | } 34 |
35 | ); 36 | }; 37 | 38 | export default class Search extends React.Component { 39 | 40 | constructor(props) { 41 | super(props); 42 | 43 | this.state = { 44 | completed: false, 45 | results: {} 46 | }; 47 | } 48 | 49 | componentWillMount() { 50 | if (!ExecutionEnvironment.canUseDOM) { 51 | return; 52 | } 53 | 54 | const { term } = this.props.params; 55 | 56 | const host = window.location.origin; 57 | 58 | return fetch(`${host}/api/search/term/${term}`) 59 | .then((res) => { 60 | if (res.status >= 400) { 61 | throw res; 62 | } 63 | return res.json(); 64 | }) 65 | .then((results) => { 66 | this.setState({ results, completed: true }); 67 | }).catch((err) => { 68 | console.error(err); 69 | }); 70 | } 71 | 72 | render() { 73 | const results = this.state.results; 74 | 75 | return ( 76 |
77 | {this.state.completed ? 78 | "" : Searching...} 79 | {this.state.completed && 80 | } 81 |
82 | ); 83 | } 84 | 85 | } 86 | 87 | -------------------------------------------------------------------------------- /client/components/menu.jsx: -------------------------------------------------------------------------------- 1 | /* globals console */ 2 | /* eslint-disable no-console */ 3 | 4 | import React from "react"; 5 | import ExecutionEnvironment from "exenv"; 6 | import fetch from "isomorphic-fetch"; 7 | 8 | export default class Component extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | menu: {} 13 | }; 14 | } 15 | 16 | componentWillMount() { 17 | if (!ExecutionEnvironment.canUseDOM) { 18 | return; 19 | } 20 | 21 | const host = window.location.origin; 22 | 23 | return fetch(`${host}/data/orgs.json`) 24 | .then((res) => { 25 | if (res.status >= 400) { 26 | throw res; 27 | } 28 | return res.json(); 29 | }) 30 | .then((menu) => { 31 | this.setState({menu: menu.allOrgs}); 32 | }).catch((err) => { 33 | console.log(err); 34 | }); 35 | } 36 | 37 | _subModuleLink(link, submodule) { 38 | const hash = submodule.replace(/\s/g, "").toLowerCase(); 39 | return ( 40 |
  • 41 | 42 | {submodule} 43 | 44 |
  • 45 | ); 46 | } 47 | 48 | _renderSubModules(link, submodules) { 49 | return submodules && submodules.length && ( 50 |
      51 | {submodules.map((submodule) => this._subModuleLink(link, submodule))} 52 |
    53 | ); 54 | } 55 | 56 | _renderLinks(org) { 57 | const { menu } = this.state; 58 | const { repos } = menu[org]; 59 | const sortedRepos = Object.keys(repos); 60 | sortedRepos.sort(); 61 | return sortedRepos.map((repoName) => { 62 | const { link, submodules } = repos[repoName]; 63 | 64 | return (
  • 65 | 67 | {repoName} 68 | 69 | {this._renderSubModules(link, submodules)} 70 |
  • ); 71 | }); 72 | } 73 | 74 | render() { 75 | const { menu } = this.state; 76 | 77 | return ( 78 |
    79 | {menu && Object.keys(menu).map((org) => ( 80 | 81 |

    {org}

    82 |
      83 | {this._renderLinks(org)} 84 |
    85 |
    86 | ))} 87 |
    88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /server/poll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hapi plugin to poll all the repos under `ORGS` peoriodically 3 | * and update demo modules if there is a newer version. 4 | */ 5 | 6 | "use strict"; 7 | 8 | const GitHubApi = require("github"); 9 | const Promise = require("bluebird"); 10 | const Config = require("electrode-confippet").config; 11 | const github = new GitHubApi(Config.githubApi); 12 | const githubAuthObject = require("./utils/github-auth-object"); 13 | const fs = require("fs"); 14 | const path = require("path"); 15 | const exec = require('child_process').exec; 16 | 17 | const Poll = {}; 18 | 19 | let command = ""; 20 | 21 | function getRepos(org, page, repos) { 22 | return new Promise((resolve) => { 23 | github.repos.getForOrg({ 24 | org, 25 | page, 26 | per_page: 100 27 | }, (err, res) => { 28 | res.forEach((repo) => { 29 | const fullName = repo.full_name.split("/"); 30 | repos.push({ 31 | org: fullName[0], 32 | repoName: fullName[1] 33 | }); 34 | }); 35 | 36 | if (res.length < 100) { 37 | return resolve(repos); 38 | } else { 39 | return resolve(getRepos(org, page + 1, repos)); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | function addCronJob(cmd, index) { 46 | // 15 minutes between each job 47 | const minute = index % 4 * 15; 48 | const hour = Math.floor(index / 4) % 24; 49 | command += `${minute} ${hour} * * * ${cmd}\n`; 50 | } 51 | 52 | Poll.register = (server, options, next) => { 53 | if (options.enable === false) { 54 | return next(); 55 | } 56 | 57 | try { 58 | github.authenticate(githubAuthObject); 59 | } catch (e) { 60 | console.log("A valid GitHub access token is needed."); 61 | } 62 | 63 | next(); 64 | 65 | const ORGS = Config.ORGS; 66 | const repos = []; 67 | 68 | const promises = []; 69 | ORGS.forEach((org) => { 70 | promises.push(getRepos(org, 1, repos)); 71 | }); 72 | 73 | return Promise.all(promises) 74 | .then(() => { 75 | repos.forEach((repo, index) => { 76 | const { org, repoName } = repo; 77 | addCronJob(`curl -X POST http://localhost:3000/api/update/${org}/${repoName} > /dev/null`, index); 78 | }); 79 | }) 80 | .then(() => { 81 | const filePath = path.join(__dirname, "../myjob"); 82 | fs.writeFile(filePath, command, "utf8", (err) => { 83 | if (err) { 84 | console.log(err); 85 | return; 86 | } 87 | 88 | exec(`crontab ${filePath}`, (error) => { 89 | if (error) { 90 | console.error(`exec error: ${error}`); 91 | return; 92 | } 93 | 94 | console.log("Cron job set."); 95 | }); 96 | }); 97 | }); 98 | }; 99 | 100 | Poll.register.attributes = { 101 | name: "poll", 102 | version: "1.0.0" 103 | }; 104 | 105 | module.exports = Poll; 106 | -------------------------------------------------------------------------------- /server/component-api/check-dependencies.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Fs = require("fs"); 4 | const Promise = require("bluebird"); 5 | const Path = require("path"); 6 | const Config = require("electrode-confippet").config; 7 | const checkVersion = require("../utils/check-version"); 8 | const execFile = require("child_process").execFile; 9 | 10 | const prefixes = Config.MODULE_PREFIXES_INCLUDE; 11 | const pattern = prefixes && prefixes.length && new RegExp(prefixes.join("|")); 12 | 13 | const getDepLatest = (dep, version, isDev) => { 14 | return new Promise((resolve, reject) => { 15 | 16 | execFile("bash", [Path.join(__dirname, "../../scripts/info-module.sh"), dep], (err, stdout, stderr) => { 17 | if (err || stderr) { 18 | console.log("error getting module info", err || stderr); 19 | return resolve({}); 20 | } 21 | 22 | const m = stdout.match(/latest:\s'([\d\.]+)'/); 23 | let wantedVersion = m ? m[1] : version.replace(/[^\.\d]/g, ""); 24 | 25 | if (!(/\./).test(wantedVersion)) { 26 | wantedVersion += ".0.0"; 27 | } 28 | 29 | resolve({ 30 | uri: "#", 31 | displayName: dep, 32 | version: checkVersion(wantedVersion, version), 33 | description: isDev ? "[dev]" : "" 34 | }); 35 | }); 36 | }); 37 | }; 38 | 39 | const processDeps = (deps, isDev) => { 40 | 41 | const promises = []; 42 | 43 | if (deps) { 44 | Object.keys(deps).map((dep) => { 45 | 46 | if (pattern && !(pattern).test(dep)) { 47 | return; 48 | } 49 | 50 | const promise = getDepLatest(dep, deps[dep], isDev); 51 | promises.push(promise); 52 | 53 | }); 54 | } 55 | 56 | return Promise.all(promises); 57 | 58 | }; 59 | 60 | const checkDepVersions = (deps, isDev) => { 61 | return processDeps(deps, isDev) 62 | .then((depArr) => { 63 | return depArr; 64 | }); 65 | }; 66 | 67 | const writeDeps = (moduleName, moduleDeps) => { 68 | 69 | if (!moduleDeps.length) { 70 | return; 71 | } 72 | 73 | return new Promise((resolve) => { 74 | Fs.readFile(Path.join(__dirname, `../../data/${moduleName}.json`), (err, repoFile) => { 75 | let data = {}; 76 | try { 77 | data = JSON.parse(repoFile); 78 | } catch (e) {} 79 | 80 | data.deps = moduleDeps; 81 | 82 | const writePath = Path.join(__dirname, `../../data/${moduleName}.json`); 83 | Fs.writeFile(writePath, JSON.stringify(data), (err) => { 84 | if (err) { 85 | console.error("Error writing file with dependencies", err); 86 | } 87 | resolve({}); 88 | }); 89 | }); 90 | }); 91 | }; 92 | 93 | module.exports = (moduleName, deps, devDeps) => { 94 | 95 | return Promise.all([ 96 | checkDepVersions(deps), 97 | checkDepVersions(devDeps, true) 98 | ]) 99 | .then((depArrays) => { 100 | return writeDeps(moduleName, Array.concat(depArrays[0], depArrays[1])) 101 | }) 102 | .catch((err) => { 103 | console.log("error getting module dependencies", err); 104 | }); 105 | 106 | }; 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Created by https://www.gitignore.io/api/gitbook,node,webstorm,bower,osx 4 | 5 | ### GitBook ### 6 | # Node rules: 7 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 8 | .grunt 9 | 10 | ## Dependency directory 11 | ## Commenting this out is preferred by some people, see 12 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 13 | node_modules 14 | 15 | # Book build output 16 | _book 17 | 18 | # eBook build output 19 | *.epub 20 | *.mobi 21 | *.pdf 22 | 23 | 24 | ### Node ### 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | 35 | # Directory for instrumented libs generated by jscoverage/JSCover 36 | lib-cov 37 | 38 | # Coverage directory used by tools like istanbul 39 | coverage 40 | 41 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Compiled binary addons (http://nodejs.org/api/addons.html) 48 | build/Release 49 | 50 | # Dependency directory 51 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 52 | node_modules 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | 61 | ### WebStorm ### 62 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 63 | 64 | *.iml 65 | 66 | ## Directory-based project format: 67 | .idea/ 68 | # if you remove the above rule, at least ignore the following: 69 | 70 | # User-specific stuff: 71 | # .idea/workspace.xml 72 | # .idea/tasks.xml 73 | # .idea/dictionaries 74 | # .idea/shelf 75 | 76 | # Sensitive or high-churn files: 77 | # .idea/dataSources.ids 78 | # .idea/dataSources.xml 79 | # .idea/sqlDataSources.xml 80 | # .idea/dynamic.xml 81 | # .idea/uiDesigner.xml 82 | 83 | # Gradle: 84 | # .idea/gradle.xml 85 | # .idea/libraries 86 | 87 | # Mongo Explorer plugin: 88 | # .idea/mongoSettings.xml 89 | 90 | ## File-based project format: 91 | *.ipr 92 | *.iws 93 | 94 | ## Plugin-specific files: 95 | 96 | # IntelliJ 97 | /out/ 98 | 99 | # mpeltonen/sbt-idea plugin 100 | .idea_modules/ 101 | 102 | # JIRA plugin 103 | atlassian-ide-plugin.xml 104 | 105 | # Crashlytics plugin (for Android Studio and IntelliJ) 106 | com_crashlytics_export_strings.xml 107 | crashlytics.properties 108 | crashlytics-build.properties 109 | fabric.properties 110 | 111 | 112 | ### Bower ### 113 | bower_components 114 | .bower-cache 115 | .bower-registry 116 | .bower-tmp 117 | 118 | 119 | ### OSX ### 120 | .DS_Store 121 | .AppleDouble 122 | .LSOverride 123 | 124 | # Icon must end with two \r 125 | Icon 126 | 127 | 128 | # Thumbnails 129 | ._* 130 | 131 | # Files that might appear in the root of a volume 132 | .DocumentRevisions-V100 133 | .fseventsd 134 | .Spotlight-V100 135 | .TemporaryItems 136 | .Trashes 137 | .VolumeIcon.icns 138 | 139 | # Directories potentially created on remote AFP share 140 | .AppleDB 141 | .AppleDesktop 142 | Network Trash Folder 143 | Temporary Items 144 | .apdisk 145 | 146 | # others 147 | .hg 148 | .project 149 | .tmp 150 | 151 | # Build 152 | dist 153 | Procfile 154 | npm-shrinkwrap.json 155 | .isomorphic-loader-config.json 156 | 157 | -------------------------------------------------------------------------------- /server/component-api/update-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Promise = require("bluebird"); 4 | const fs = require("fs"); 5 | const readFile = Promise.promisify(fs.readFile); 6 | const writeFile = Promise.promisify(fs.writeFile); 7 | const Path = require("path"); 8 | const semver = require("semver"); 9 | 10 | const Config = require("electrode-confippet").config; 11 | const ghToken = process.env[Config.GHACCESS_TOKEN_NAME]; 12 | 13 | const ensureDirectoryExists = require("../utils/ensure-directory-exists"); 14 | const fetchRepo = require("./fetch-repo"); 15 | const fetchModuleDemo = require("./fetch-module-demo"); 16 | 17 | const updateFiles = (repoFilePath, org, repoName, data) => { 18 | return writeFile(repoFilePath, data) 19 | .then(() => { 20 | 21 | // update the map of known orgs 22 | const orgMap = Path.join(__dirname, `../../data/orgs.json`); 23 | 24 | return readFile(orgMap) 25 | .then((catalog) => { 26 | try { 27 | catalog = JSON.parse(catalog); 28 | 29 | if (!catalog.allOrgs) { 30 | catalog.allOrgs = {}; 31 | } 32 | 33 | if (!catalog.allOrgs[org]) { 34 | catalog.allOrgs[org] = { 35 | repos: {} 36 | }; 37 | } 38 | 39 | const current = catalog.allOrgs[org]; 40 | 41 | current.repos[repoName] = { 42 | link: `${org}/${repoName}` 43 | }; 44 | 45 | fs.writeFileSync(orgMap, JSON.stringify(catalog)); 46 | 47 | } catch (err) { 48 | console.error("Problem checking org map", err); 49 | } 50 | }); 51 | }) 52 | .catch((err) => { 53 | console.log("repo file save error", err); 54 | throw err; 55 | }); 56 | }; 57 | 58 | const UpdateHandler = function (request, reply) { 59 | 60 | if (!ghToken) { 61 | // No token? No automatic updates. 62 | return reply("Automatic updating requires a Github access token. No token found."); 63 | } 64 | 65 | const { org, repoName } = request.params; 66 | 67 | const { ref, ref_type } = request.payload || {}; 68 | 69 | const updateNow = request.query.updateNow; 70 | const waitingTime = updateNow ? 0 : Config.NPM_WAITING_TIME; 71 | 72 | const orgDataPath = Path.join(__dirname, `../../data/${org}`); 73 | 74 | ensureDirectoryExists(orgDataPath); 75 | 76 | return fetchRepo(org, repoName).then((result) => { 77 | const repoFilePath = `${orgDataPath}/${repoName}.json`; 78 | 79 | // Preserve saved deps if already saved, prepare empty deps array if not 80 | let deps; 81 | let currentVersion; 82 | 83 | return readFile(repoFilePath, "utf8") 84 | .then((data) => { 85 | data = JSON.parse(data); 86 | deps = data.deps || []; 87 | currentVersion = data.meta && data.meta.version; 88 | const latestVersion = result.meta.version; 89 | 90 | if (!updateNow && currentVersion && !semver.lt(currentVersion, latestVersion)) { 91 | return reply(`${org}:${repoName} is at its latest version.`); 92 | } 93 | 94 | result.deps = deps; 95 | 96 | let version; 97 | if (ref_type === "tag") { 98 | version = semver.clean(ref); 99 | } else { 100 | version = result.pkg.version; 101 | } 102 | version = version.substring(0, version.indexOf(".")); 103 | 104 | const keywords = result.pkg.keywords; 105 | setTimeout(() => { 106 | console.log(`fetching module ${result.meta.name}`); 107 | fetchModuleDemo(result.meta, version, request.server, keywords); 108 | }, waitingTime); 109 | 110 | delete result.pkg; 111 | 112 | return updateFiles(repoFilePath, org, repoName, JSON.stringify(result)) 113 | .then(() => reply(`${org}:${repoName} was saved.`)); 114 | }); 115 | }).catch((e) => { 116 | console.log("e", e); 117 | return reply(`Error encountered: ${e.message}`); 118 | }); 119 | 120 | }; 121 | 122 | module.exports = UpdateHandler; 123 | -------------------------------------------------------------------------------- /server/component-api/fetch-usage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const GitHubApi = require("github"); 4 | const Promise = require("bluebird"); 5 | const Config = require("electrode-confippet").config; 6 | const github = new GitHubApi(Config.githubApi); 7 | const REPOS_USAGE_INCLUDE = Config.REPOS_USAGE_INCLUDE; 8 | const REPOS_USAGE_EXCLUDE = Config.REPOS_USAGE_EXCLUDE; 9 | const contentToString = require("../utils/content-to-string"); 10 | const checkVersion = require("../utils/check-version"); 11 | const githubAuthObject = require("../utils/github-auth-object"); 12 | const uniqBy = require("lodash/uniqBy"); 13 | 14 | const shouldIncludeRepo = (repo) => { 15 | let shouldExclude = false; 16 | REPOS_USAGE_EXCLUDE.some((r) => { 17 | if (repo.indexOf(r) !== -1) { 18 | shouldExclude = true; 19 | return true; 20 | } 21 | }); 22 | return !shouldExclude && REPOS_USAGE_INCLUDE.some((org) => repo.indexOf(org) === 0); 23 | }; 24 | 25 | const getOrgRepo = (uri) => { 26 | if (!uri || typeof uri !== "string") { 27 | throw new Error("uri problem for ", JSON.stringify(uri)); 28 | } 29 | 30 | const parts = uri.replace("://","").split("/"); 31 | return { 32 | org: parts[1], 33 | repo: parts[2] 34 | }; 35 | }; 36 | 37 | const getVersion = (moduleSearchedFor, moduleVersion, githubUri) => { 38 | github.authenticate(githubAuthObject); 39 | 40 | const orgRepo = getOrgRepo(githubUri); 41 | 42 | return new Promise((resolve, reject) => { 43 | if (typeof githubUri !== "string") { 44 | return reject("Not a string"); 45 | } 46 | 47 | github.repos.getContent({ 48 | user: orgRepo.org, 49 | repo: orgRepo.repo, 50 | path: "package.json" 51 | }, (err, res) => { 52 | if (err) { 53 | return reject(err); 54 | } 55 | 56 | const pkg = JSON.parse(contentToString(res.content)); 57 | 58 | const version = pkg.dependencies && pkg.dependencies[moduleSearchedFor] || 59 | pkg.devDependencies && pkg.devDependencies[moduleSearchedFor] || 60 | "unknown"; 61 | 62 | return resolve({ 63 | uri: githubUri, 64 | displayName: `${orgRepo.org}/${orgRepo.repo}`, 65 | version: checkVersion(moduleVersion, version), 66 | description: pkg.description 67 | }); 68 | 69 | }); 70 | }); 71 | }; 72 | 73 | const fetchUsage = (meta) => { 74 | github.authenticate(githubAuthObject); 75 | 76 | const moduleParts = meta.name.split("/"); 77 | const moduleName = moduleParts[1] || meta.name; 78 | 79 | const opts = { 80 | q: `${moduleName}:+in:file+language:json+filename:package.json` 81 | }; 82 | 83 | return new Promise((resolve, reject) => { 84 | github.search.code(opts, (err, res) => { 85 | if (err) { 86 | console.log("error fetchUsage", err); 87 | return reject(err); 88 | } 89 | 90 | const promises = []; 91 | const usage = []; 92 | res.items.forEach((item) => { 93 | if (meta.github.indexOf(item.repository.html_url) === -1 && 94 | shouldIncludeRepo(item.repository.full_name)) { 95 | const promise = getVersion(meta.name, meta.version, item.repository.html_url) 96 | .then((detail) => { 97 | usage.push(detail); 98 | }) 99 | .catch((err) => { 100 | if (err.code === 404) { 101 | console.log("Missing package.json?: " + meta.github); 102 | } else { 103 | console.log(err); 104 | } 105 | }); 106 | promises.push(promise); 107 | } 108 | }); 109 | 110 | return Promise.all(promises) 111 | .then(() => { 112 | usage.sort(function compare(a, b) { 113 | if (a.displayName < b.displayName) { 114 | return -1; 115 | } 116 | if (a.displayName > b.displayName) { 117 | return 1; 118 | } 119 | return 0; 120 | }); 121 | return resolve(uniqBy(usage, "uri")); 122 | }); 123 | }); 124 | }); 125 | }; 126 | 127 | module.exports = fetchUsage; 128 | -------------------------------------------------------------------------------- /client/components/revealer/collapsable.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import tweenState from "react-tween-state"; 3 | import classNames from "classnames"; 4 | 5 | export default React.createClass({ 6 | displayName: "Collapsable", 7 | 8 | mixins: [tweenState.Mixin], 9 | 10 | propTypes: { 11 | /** 12 | CSS class name to apply to the component container 13 | */ 14 | containerClassName: React.PropTypes.string, 15 | /** 16 | CSS class name to apply to the children container 17 | */ 18 | className: React.PropTypes.string, 19 | /** 20 | Children to render in the container 21 | */ 22 | children: React.PropTypes.node, 23 | /** 24 | True if the collapsable area is open 25 | */ 26 | isOpen: React.PropTypes.bool, 27 | /** 28 | The duration of the collasping transition (in milliseconds) 29 | */ 30 | transitionDuration: React.PropTypes.number, 31 | /** 32 | The easing function for the transition 33 | */ 34 | transitionTimingFunction: React.PropTypes.string, 35 | /** 36 | Event callback for when the transition is complete 37 | */ 38 | transitionComplete: React.PropTypes.func, 39 | /** 40 | True if the layout is vertical 41 | */ 42 | isVertical: React.PropTypes.bool, 43 | /** 44 | The collapsed height, in pixels 45 | */ 46 | baseHeight: React.PropTypes.number, 47 | /** 48 | The collapsed width, in pixels 49 | */ 50 | baseWidth: React.PropTypes.number, 51 | /** 52 | What CSS overflow style to apply when collapsed 53 | */ 54 | overflow: React.PropTypes.string, 55 | hidden: React.PropTypes.bool 56 | }, 57 | 58 | getDefaultProps() { 59 | return { 60 | className: "zeus-collapsable clearfix", 61 | isOpen: true, 62 | isVertical: true, 63 | transitionDuration: 400, 64 | transitionComplete: () => {}, 65 | baseHeight: 0, 66 | baseWidth: 0, 67 | overflow: "hidden" 68 | }; 69 | }, 70 | 71 | getInitialState() { 72 | return { 73 | maxHeight: this.props.isVertical ? this.props.baseHeight : "none", 74 | maxWidth: this.props.isVertical ? "none" : this.props.baseWidth, 75 | transitioning: false 76 | }; 77 | }, 78 | 79 | _transitionComplete() { 80 | this.setState({transitioning: false}); 81 | if (this.props.transitionComplete) { 82 | this.props.transitionComplete(); 83 | } 84 | }, 85 | 86 | componentWillReceiveProps(nextProps) { 87 | const base = nextProps.isVertical ? 88 | nextProps.baseHeight : nextProps.baseWidth; 89 | const max = nextProps.isVertical ? 90 | this.refs.interior.offsetHeight : this.refs.interior.offsetWidth; 91 | 92 | const tweenProp = this.props.isVertical ? "maxHeight" : "maxWidth"; 93 | const tweenEasing = this.props.transitionTimingFunction || tweenState.easingTypes.easeInQuad; 94 | 95 | this.setState({transitioning: true}); 96 | this.tweenState(tweenProp, { 97 | easing: tweenEasing, 98 | duration: this.props.transitionDuration, 99 | endValue: nextProps.isOpen ? max : base, 100 | onEnd: this._transitionComplete 101 | }); 102 | }, 103 | 104 | _isInitialRender() { 105 | return !this.refs.interior; 106 | }, 107 | 108 | _getContentWrapperStyles() { 109 | const isVertical = this.props.isVertical; 110 | const isOpen = this.props.isOpen; 111 | let maxWidth = null; 112 | let maxHeight = null; 113 | 114 | if (this._isInitialRender() || this.state.transitioning === false) { 115 | // if it's the initial render, we can't rely on interior or tweening values 116 | // to show or hide without any tweening 117 | maxWidth = !isVertical && !isOpen ? this.props.baseWidth : "none"; 118 | maxHeight = isVertical && !isOpen ? this.props.baseHeight : "none"; 119 | } else { 120 | maxWidth = !isVertical ? this.getTweeningValue("maxWidth") || this.props.baseWidth : "none"; 121 | maxHeight = isVertical ? this.getTweeningValue("maxHeight") || this.props.baseHeight : "none"; 122 | } 123 | 124 | return { 125 | position: "relative", 126 | boxSizing: "border-box", 127 | maxWidth, 128 | maxHeight, 129 | overflow: (!this.state.transitioning && isOpen) ? "visible" : this.props.overflow 130 | }; 131 | }, 132 | 133 | render() { 134 | return ( 135 |
    139 |
    {this.props.children}
    142 |
    143 | ); 144 | } 145 | }); 146 | -------------------------------------------------------------------------------- /client/components/revealer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import classNames from "classnames"; 4 | import Collapsable from "./collapsable"; 5 | 6 | export default class Revealer extends React.Component { 7 | constructor(props) { 8 | super(); 9 | this.state = { 10 | isOpen: props.defaultOpen, 11 | visibleToggle: (props.defaultOpen && props.disableClose) ? false : true, 12 | baseHeight: props.baseHeight, 13 | contentSet: false 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | this.normalizeHeight(); 19 | } 20 | 21 | _afterAnimation(): void { 22 | if (this.state.isOpen && this.props.disableClose) { 23 | this.setState({ 24 | visibleToggle: false 25 | }); 26 | } 27 | } 28 | 29 | componentDidUpdate() { 30 | this.normalizeHeight(); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | // If the content is changing, reset baseHeight and visibleToggle 35 | if (nextProps.children !== this.props.children) { 36 | this.setState({ 37 | baseHeight: nextProps.baseHeight, 38 | contentSet: false, 39 | visibleToggle: (nextProps.defaultOpen && nextProps.disableClose) ? false : true 40 | }); 41 | } 42 | } 43 | 44 | normalizeHeight() { 45 | // Check that our content fills the baseHeight, if not, remove toggle and 46 | // slim down 47 | const contentElement = ReactDOM.findDOMNode(this.refs.content); 48 | const contentWidth = contentElement.offsetWidth; 49 | const contentHeight = contentElement.offsetHeight; 50 | 51 | this.checkVisibilityAndResize(contentHeight, contentWidth); 52 | } 53 | 54 | checkVisibilityAndResize(contentHeight, contentWidth) { 55 | // Exit early if the element"s has no height or width, indicating that it"s 56 | // not visible. 57 | if (contentWidth === 0 && contentHeight === 0) { 58 | return; 59 | } 60 | 61 | if (contentHeight < this.state.baseHeight) { 62 | this.setState({ 63 | baseHeight: contentHeight, 64 | visibleToggle: false, 65 | contentSet: true 66 | }); 67 | } 68 | } 69 | 70 | toggleOpen() { 71 | this.setState({ 72 | isOpen: !this.state.isOpen 73 | }); 74 | } 75 | 76 | render() { 77 | const buttonText = this.state.isOpen ? this.props.buttonOpenText : this.props.buttonClosedText; 78 | const baseHeight = this.state.baseHeight; 79 | 80 | return ( 81 |
    82 | this._afterAnimation()} 87 | > 88 |
    89 | {this.props.children} 90 |
    91 |
    92 | {this.state.visibleToggle && 93 |
    100 | 103 |
    104 | } 105 |
    106 | ); 107 | } 108 | } 109 | 110 | Revealer.displayName = "Revealer"; 111 | 112 | Revealer.propTypes = { 113 | /** 114 | The base height of the container 115 | */ 116 | baseHeight: React.PropTypes.number, 117 | /** 118 | True if we should display a border above the button 119 | */ 120 | border: React.PropTypes.bool, 121 | /** 122 | Text to be displayed within the button when closed 123 | */ 124 | buttonClosedText: React.PropTypes.string, 125 | /** 126 | Text to be displayed within the button when open 127 | */ 128 | buttonOpenText: React.PropTypes.string, 129 | /** 130 | Children node to be placed in collapsable container 131 | */ 132 | children: React.PropTypes.node, 133 | /** 134 | True if the revealer should start open 135 | */ 136 | defaultOpen: React.PropTypes.bool, 137 | /** 138 | True the revealer should not be closeable 139 | */ 140 | disableClose: React.PropTypes.bool, 141 | /** 142 | True if we should display button as a fake link 143 | */ 144 | fakeLink: React.PropTypes.bool, 145 | /** 146 | True if we should display the inverse button 147 | */ 148 | inverse: React.PropTypes.bool 149 | }; 150 | 151 | Revealer.contextTypes = { 152 | analytics: React.PropTypes.object 153 | }; 154 | 155 | Revealer.defaultProps = { 156 | baseHeight: 100, 157 | border: true, 158 | buttonClosedText: "Show more", 159 | buttonOpenText: "Show less", 160 | defaultOpen: false, 161 | disableClose: false, 162 | fakeLink: true, 163 | inverse: false 164 | }; 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Electrode Explorer 2 | ================ 3 | 4 | > An electrode application that allows you to view all the components in your organization, play with the components, read the component docs, view the different versions of the component, and more - All in your browser. 5 | [View The Live Demo](https://electrode-explorer.herokuapp.com/) 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | * node: ">=4.2.0" 12 | * npm: ">=3.0.0" 13 | * Github access token 14 | 15 | ## Overview 16 | 17 | This is a central place where you can view 18 | 19 | * demos of all the components under the organizations you specified 20 | * documentations of your components 21 | * dependencies your components have (dependencies) 22 | * other components/applications that depend on your components (usages) 23 | 24 | There are 2 ways the components can update dynamically: 25 | 26 | 1. Add github hooks to send POST requests to `/api/update/{org}/{repoName}` when a new tag is created 27 | 2. Enable `./server/poll` plugin to set up cron job that sends the POST requests every day 28 | 29 | After the server receives the POST request, it will fetch the `package.json` file under `yourGithubUrl/org/repoName`, 30 | update [data/orgs.json] and `data/{org}/{repoName}.json` files. If there is a newer version, it will try to download the 31 | new component through npm ([scripts/install-module.sh]) after a waiting period, babel transpile, and webpack the demo module ([scripts/post-install-module.sh]). 32 | 33 | To make the server update immediately or force an update, add a url parameter to the POST request, `/api/update/{org}/{repoName}?updateNow=1`. 34 | 35 | This post processing script works well with all electrode components (meaning components using our [archetype]). If you have non-electrode components, you can modify your [scripts/post-install-module.sh]. See our [wiki] page for more details. 36 | 37 | ## Config 38 | 39 | ```js 40 | // config/default.json 41 | { 42 | "plugins": { 43 | "./server/poll": { 44 | "options": { 45 | "enable": true 46 | } 47 | } 48 | }, 49 | 50 | "githubApi": { 51 | "version": "3.0.0", 52 | "pathPrefix": "/api/v3", 53 | "protocol": "https", 54 | "host": "github.com" 55 | }, 56 | 57 | "ORGS": [ 58 | // org/user names under which components will be included in the explorer 59 | // for example, put ["xxx", "yyy"] to include every repo under github.com/xxx and github.com/yyy 60 | ], 61 | 62 | "REPOS_USAGE_INCLUDE": [ 63 | // consumers need to contain one of these substrings to be included in usages 64 | // for example, put ["react"] so consumers named /*react*/ will be included in usages 65 | ], 66 | 67 | "REPOS_USAGE_EXCLUDE": [ 68 | // consumers containing any of these substrings won't be included in usages 69 | // for example, put ["training"] so consumers named /*training*/ will be excluded in usages 70 | ], 71 | 72 | "MODULE_PREFIXES_INCLUDE": [ 73 | // only module names beginning with one of these strings will be included in dependencies 74 | // for example, put ["react"] so only modules with name starting with "react" will be included in dependencies 75 | ], 76 | 77 | "NPM_WAITING_TIME": 300000, // wait for 5 minutes before `npm install` 78 | 79 | "GHACCESS_TOKEN_NAME": "GHACCESS_TOKEN" // github token variable name, your token would be accessible via `process.env["GHACCESS_TOKEN"]` 80 | } 81 | ``` 82 | 83 | ## Start server 84 | 85 | First install dependencies 86 | ```sh 87 | npm i 88 | ``` 89 | 90 | Export github access token or set it as an environment variable 91 | ```sh 92 | export GHACCESS_TOKEN=YOUR_GITHUB_TOKEN 93 | ``` 94 | 95 | For development mode 96 | ```sh 97 | gulp dev 98 | ``` 99 | or 100 | ```sh 101 | GHACCESS_TOKEN=YOUR_GITHUB_TOKEN gulp dev 102 | ``` 103 | 104 | For production mode 105 | ```sh 106 | gulp build 107 | ``` 108 | and 109 | ```sh 110 | NODE_ENV=production node . 111 | ``` 112 | or 113 | ```sh 114 | GHACCESS_TOKEN=YOUR_GITHUB_TOKEN NODE_ENV=production node . 115 | ``` 116 | 117 | ## Deploy 118 | 119 | Since this is an electrode app, it can be deployed the same way as any other electrode app. Just remember to set your github token as an environment variable. 120 | 121 | ## Learn more 122 | 123 | Wish to learn more? Check our [wiki] page! 124 | 125 | Built with :heart: by [Team Electrode](https://github.com/orgs/electrode-io/people) @WalmartLabs. 126 | 127 | [archetype]: https://github.com/electrode-io/electrode-archetype-react-component 128 | [data/orgs.json]: https://github.com/electrode-io/electrode-explorer/blob/master/data/orgs.json 129 | [scripts/install-module.sh]: https://github.com/electrode-io/electrode-explorer/blob/master/scripts/install-module.sh 130 | [scripts/post-install-module.sh]: https://github.com/electrode-io/electrode-explorer/blob/master/scripts/post-install-module.sh 131 | [wiki]: https://github.com/electrode-io/electrode-explorer/wiki 132 | 133 | -------------------------------------------------------------------------------- /server/webapp/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const Promise = require("bluebird"); 5 | const fs = require("fs"); 6 | const Path = require("path"); 7 | const assert = require("assert"); 8 | 9 | const HTTP_ERROR_500 = 500; 10 | const HTTP_REDIRECT = 302; 11 | 12 | /** 13 | * Load stats.json which is created during build. 14 | * The file contains bundle files which are to be loaded on the client side. 15 | * 16 | * @param {string} statsFilePath - path of stats.json 17 | * @returns {Promise.} an object containing an array of file names 18 | */ 19 | function loadAssetsFromStats(statsFilePath) { 20 | return Promise.resolve(Path.resolve(statsFilePath)) 21 | .then(require) 22 | .then((stats) => { 23 | const assets = {}; 24 | _.each(stats.assetsByChunkName.main, (v) => { 25 | if (v.endsWith(".js")) { 26 | assets.js = v; 27 | } else if (v.endsWith(".css")) { 28 | assets.css = v; 29 | } 30 | }); 31 | return assets; 32 | }) 33 | .catch(() => ({})); 34 | } 35 | 36 | function makeRouteHandler(options, userContent) { 37 | const CONTENT_MARKER = "{{SSR_CONTENT}}"; 38 | const BUNDLE_MARKER = "{{WEBAPP_BUNDLES}}"; 39 | const TITLE_MARKER = "{{PAGE_TITLE}}"; 40 | const PREFETCH_MARKER = "{{PREFETCH_BUNDLES}}"; 41 | const WEBPACK_DEV = options.webpackDev; 42 | const RENDER_JS = options.renderJS; 43 | const RENDER_SS = options.serverSideRendering; 44 | const html = fs.readFileSync(Path.join(__dirname, "index.html")).toString(); 45 | const assets = options.__internals.assets; 46 | const devJSBundle = options.__internals.devJSBundle; 47 | const devCSSBundle = options.__internals.devCSSBundle; 48 | 49 | /* Create a route handler */ 50 | return (request, reply) => { 51 | const mode = request.query.__mode || ""; 52 | const renderJs = RENDER_JS && mode !== "nojs"; 53 | const renderSs = RENDER_SS && mode !== "noss"; 54 | 55 | const bundleCss = () => { 56 | return WEBPACK_DEV ? devCSSBundle : assets.css && `/js/${assets.css}` || ""; 57 | }; 58 | 59 | const bundleJs = () => { 60 | if (!renderJs) { 61 | return ""; 62 | } 63 | return WEBPACK_DEV ? devJSBundle : assets.js && `/js/${assets.js}` || ""; 64 | }; 65 | 66 | const callUserContent = (content) => { 67 | const x = content(request); 68 | return !x.catch ? x : x.catch((err) => { 69 | return Promise.reject({ 70 | status: err.status || HTTP_ERROR_500, 71 | html: err.message || err.toString() 72 | }); 73 | }); 74 | }; 75 | 76 | const makeBundles = () => { 77 | const css = bundleCss(); 78 | const cssLink = css ? `` : ""; 79 | const js = bundleJs(); 80 | const jsLink = js ? `` : ""; 81 | return `${cssLink}${jsLink}`; 82 | }; 83 | 84 | const addPrefetch = (prefetch) => { 85 | return prefetch ? `` : ""; 86 | }; 87 | 88 | const renderPage = (content) => { 89 | return html.replace(/{{[A-Z_]*}}/g, (m) => { 90 | switch (m) { 91 | case CONTENT_MARKER: 92 | return content.html || ""; 93 | case TITLE_MARKER: 94 | return options.pageTitle; 95 | case BUNDLE_MARKER: 96 | return makeBundles(); 97 | case PREFETCH_MARKER: 98 | return addPrefetch(content.prefetch); 99 | default: 100 | return `Unknown marker ${m}`; 101 | } 102 | }); 103 | }; 104 | 105 | const renderSSRContent = (content) => { 106 | const p = _.isFunction(content) ? 107 | callUserContent(content) : 108 | Promise.resolve(_.isObject(content) ? content : {html: content}); 109 | return p.then((c) => renderPage(c)); 110 | }; 111 | 112 | const handleStatus = (data) => { 113 | const status = data.status; 114 | if (status === HTTP_REDIRECT) { 115 | reply.redirect(data.path); 116 | } else { 117 | reply({message: "error"}).code(status); 118 | } 119 | }; 120 | 121 | const doRender = () => { 122 | return renderSs ? renderSSRContent(userContent) : renderPage(""); 123 | }; 124 | 125 | Promise.try(doRender) 126 | .then((data) => { 127 | return data.status ? handleStatus(data) : reply(data); 128 | }) 129 | .catch((err) => { 130 | reply(err.html).code(err.status || HTTP_ERROR_500); 131 | }); 132 | }; 133 | } 134 | 135 | const registerRoutes = (server, options, next) => { 136 | 137 | const pluginOptionsDefaults = { 138 | pageTitle: "Untitled Electrode Web Application", 139 | webpackDev: process.env.WEBPACK_DEV === "true", 140 | renderJS: true, 141 | serverSideRendering: true, 142 | devServer: { 143 | host: "127.0.0.1", 144 | port: "2992" 145 | }, 146 | paths: {}, 147 | stats: "dist/server/stats.json" 148 | }; 149 | 150 | const resolveContent = (content) => { 151 | if (!_.isString(content) && !_.isFunction(content) && content.module) { 152 | const module = content.module.startsWith(".") ? Path.join(process.cwd(), content.module) : content.module; // eslint-disable-line 153 | return require(module); // eslint-disable-line 154 | } 155 | 156 | return content; 157 | }; 158 | 159 | const pluginOptions = _.defaultsDeep({}, options, pluginOptionsDefaults); 160 | 161 | return Promise.try(() => loadAssetsFromStats(pluginOptions.stats)) 162 | .then((assets) => { 163 | const devServer = pluginOptions.devServer; 164 | pluginOptions.__internals = { 165 | assets, 166 | devJSBundle: `http://${devServer.host}:${devServer.port}/js/bundle.dev.js`, 167 | devCSSBundle: `http://${devServer.host}:${devServer.port}/js/style.css` 168 | }; 169 | 170 | _.each(options.paths, (v, path) => { 171 | assert(v.content, `You must define content for the webapp plugin path ${path}`); 172 | server.route({ 173 | method: "GET", 174 | path, 175 | config: v.config || {}, 176 | handler: makeRouteHandler(pluginOptions, resolveContent(v.content)) 177 | }); 178 | }); 179 | next(); 180 | }) 181 | .catch(next); 182 | }; 183 | 184 | registerRoutes.attributes = { 185 | pkg: { 186 | name: "webapp", 187 | version: "1.0.0" 188 | } 189 | }; 190 | 191 | module.exports = registerRoutes; 192 | -------------------------------------------------------------------------------- /server/images/electrode.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.13, written by Peter Selinger 2001-2015 9 | 10 | 12 | 26 | 48 | 51 | 55 | 58 | 69 | 77 | 86 | 96 | 104 | 114 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /client/components/component.jsx: -------------------------------------------------------------------------------- 1 | /* globals document _COMPONENTS setTimeout setInterval clearInterval */ 2 | 3 | import React from "react"; 4 | import Revealer from "./revealer"; 5 | import { canUseDOM } from "exenv"; 6 | import marked from "marked"; 7 | import fetch from "isomorphic-fetch"; 8 | 9 | export default class Component extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | meta: {}, 14 | usage: [], 15 | deps: [], 16 | demo: null, 17 | doc: null, 18 | currentVersion: null, 19 | latestVersion: null, 20 | error: null 21 | }; 22 | } 23 | 24 | componentWillMount() { 25 | if (!canUseDOM) { 26 | return; 27 | } 28 | 29 | const { org, repo, version } = this.props.params; 30 | 31 | const currentVersion = parseInt(version); 32 | if (!isNaN(currentVersion)) { 33 | this.setState({ currentVersion }); 34 | } 35 | 36 | Promise.all([ 37 | this._getComponentInfo(org, repo), 38 | this._getDoc(org, repo) 39 | ]); 40 | } 41 | 42 | _getComponentInfo(org, repo) { 43 | const host = window.location.origin; 44 | const url = `${host}/data/${org}/${repo}.json`; 45 | 46 | const compare = (a, b) => { 47 | if (a.displayName < b.displayName) { 48 | return -1; 49 | } 50 | if (a.displayName > b.displayName) { 51 | return 1; 52 | } 53 | return 0; 54 | }; 55 | 56 | return fetch(url) 57 | .then((res) => { 58 | if (res.status >= 400) { 59 | throw res; 60 | } 61 | return res.json(); 62 | }) 63 | .then((res) => { 64 | const meta = res.meta || {}; 65 | const usage = res.usage.sort(compare); 66 | 67 | const deps = res.deps || []; 68 | deps.sort(compare); 69 | 70 | const latestVersion = parseInt(meta.version.substring(0, meta.version.indexOf("."))); 71 | const currentVersion = this.state.currentVersion || latestVersion; 72 | 73 | this.setState({ meta, usage, deps, latestVersion, currentVersion }); 74 | 75 | this._getDemo(meta); 76 | }) 77 | .catch((err) => { 78 | console.log(err); 79 | }); 80 | } 81 | 82 | _getDemo(meta) { 83 | const host = window.location.origin; 84 | const script = document.createElement("script"); 85 | const { currentVersion } = this.state; 86 | script.src = `${host}/data/demo-modules/${meta.name}/v${currentVersion}/bundle.min.js`; 87 | script.async = true; 88 | 89 | const placeholder = document.getElementById("placeholder"); 90 | placeholder.appendChild(script); 91 | 92 | const x = setInterval(() => { 93 | if (typeof _COMPONENTS !== "undefined" && _COMPONENTS[meta.name]) { 94 | this.setState({ demo: _COMPONENTS[meta.name] }); 95 | clearInterval(x); 96 | } 97 | }, 500); 98 | 99 | setTimeout(() => { 100 | if (typeof _COMPONENTS === "undefined") { 101 | clearInterval(x); 102 | this.setState({ error: true }); 103 | } 104 | }, 10000); 105 | } 106 | 107 | _getDoc(org, repo) { 108 | const host = window.location.origin; 109 | const url = `${host}/api/doc/${org}/${repo}`; 110 | 111 | return fetch(url) 112 | .then((res) => { 113 | if (res.status >= 400) { 114 | throw res; 115 | } 116 | return res.json(); 117 | }) 118 | .then((res) => { 119 | this.setState({ doc: marked(res.doc) }); 120 | }) 121 | .catch(() => { 122 | this.setState({ doc: marked("# Error fetching doc") }); 123 | }); 124 | } 125 | 126 | _renderUsage(usage, deps) { 127 | return ( 128 |
    129 |

    Component Usage

    130 | { usage.length > 0 && 139 |
    140 | This component is used in {usage.length} modules / apps. 141 | { this._renderModuleData(usage) } 142 |
    143 |
    } 144 |

    Module Dependencies

    145 | { deps.length > 0 && 154 |
    155 | This component has {deps.length} Electrode dependencies. 156 | { this._renderModuleData(deps) } 157 |
    158 |
    } 159 |
    160 | ); 161 | } 162 | 163 | _renderTitle(meta) { 164 | return ( 165 |

    166 | { meta.title } 167 | 168 | { meta.version && 169 | 170 | {` v${meta.version}`} 171 | } 172 | 173 | { this._renderVersion() } 174 | 175 | { meta.description && 176 | 177 | {meta.description} 178 | } 179 | 180 | 181 | { meta.github && 182 | } 185 | { meta.name && 186 |
    187 | npm i --save {meta.name} 188 |
    } 189 |
    190 | 191 |

    192 | ); 193 | } 194 | 195 | _onVersionChange(e) { 196 | const { org, repo } = this.props.params; 197 | const curr = this.state.currentVersion; 198 | const next = +e.target.value; 199 | 200 | if (!isNaN(next) && curr !== next) { 201 | window.location.pathname = `/${org}/${repo}/${next}`; 202 | } 203 | } 204 | 205 | _renderVersionOptions() { 206 | const { latestVersion, currentVersion } = this.state; 207 | 208 | const chooser = [ 209 | 212 | ]; 213 | 214 | if (latestVersion === 0) { 215 | chooser.push( 216 | 219 | ); 220 | } else { 221 | for (let i = 1; i <= latestVersion; i += 1) { 222 | if (i !== currentVersion) { 223 | chooser.push( 224 | 227 | ); 228 | } 229 | } 230 | } 231 | 232 | return chooser; 233 | } 234 | 235 | _renderVersion() { 236 | const { latestVersion } = this.state; 237 | 238 | return latestVersion ? ( 239 | 240 | Switch version: 241 | 244 | 245 | ) : null; 246 | } 247 | 248 | _renderDoc() { 249 | const { doc } = this.state; 250 | 251 | return ( 252 |
    253 | 262 |
    263 | 264 |
    265 | ); 266 | } 267 | 268 | _renderModuleData(data) { 269 | return ( 270 | 271 | 272 | {data.map((detail) => ( 273 | 274 | 279 | 284 | 287 | 288 | ))} 289 | 290 |
    275 | 276 | {detail.displayName} 277 | 278 | 280 | 281 | {detail.version && detail.version.str} 282 | 283 | 285 | {detail.description} 286 |
    291 | ); 292 | } 293 | 294 | _renderDemo() { 295 | const { demo, error } = this.state; 296 | 297 | if (!demo && !error) { 298 | return (
    Loading, please wait.
    ); 299 | } 300 | 301 | if (!demo && error) { 302 | return (This component does not have demo or demo does not work properly.); 303 | } 304 | 305 | return React.createElement(demo); 306 | } 307 | 308 | render() { 309 | const { meta, usage, deps } = this.state; 310 | 311 | if (!meta.title) { 312 | meta.title = this.props.params.repo || "[ Missing Title ]"; 313 | } 314 | 315 | return ( 316 |
    317 | { this._renderTitle(meta) } 318 | { this._renderDoc() } 319 |
    320 |
    321 | { this._renderDemo() } 322 |
    323 | { this._renderUsage(usage, deps) } 324 |
    325 | ); 326 | } 327 | } 328 | 329 | Component.propTypes = { 330 | params: React.PropTypes.shape({ 331 | org: React.PropTypes.string, 332 | repo: React.PropTypes.string, 333 | version: React.PropTypes.string 334 | }) 335 | }; 336 | --------------------------------------------------------------------------------