├── 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 |
22 | {matched.map((result) => {
23 | return (
24 |
31 | );
32 | })}
33 |
}
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 |
101 | {buttonText}
102 |
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 |
210 | v{currentVersion}
211 |
212 | ];
213 |
214 | if (latestVersion === 0) {
215 | chooser.push(
216 |
217 | v0
218 |
219 | );
220 | } else {
221 | for (let i = 1; i <= latestVersion; i += 1) {
222 | if (i !== currentVersion) {
223 | chooser.push(
224 |
225 | v{i}
226 |
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 |
242 | { this._renderVersionOptions() }
243 |
244 |
245 | ) : null;
246 | }
247 |
248 | _renderDoc() {
249 | const { doc } = this.state;
250 |
251 | return (
252 |
265 | );
266 | }
267 |
268 | _renderModuleData(data) {
269 | return (
270 |
271 |
272 | {data.map((detail) => (
273 |
274 |
275 |
276 | {detail.displayName}
277 |
278 |
279 |
280 |
281 | {detail.version && detail.version.str}
282 |
283 |
284 |
285 | {detail.description}
286 |
287 |
288 | ))}
289 |
290 |
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 |
--------------------------------------------------------------------------------