Repository: aurora
Updated Branches:
  refs/heads/master 3ec0430b8 -> 8d5f6a227


HomePage implemented in Preact

Reviewed at https://reviews.apache.org/r/62135/


Project: http://git-wip-us.apache.org/repos/asf/aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/8d5f6a22
Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/8d5f6a22
Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/8d5f6a22

Branch: refs/heads/master
Commit: 8d5f6a227c5c31a03b0acb57188f1240d1582666
Parents: 3ec0430
Author: David McLaughlin <[email protected]>
Authored: Wed Sep 13 11:31:30 2017 -0700
Committer: David McLaughlin <[email protected]>
Committed: Wed Sep 13 11:31:30 2017 -0700

----------------------------------------------------------------------
 .../assets/images/aurora_logo_white.png         | Bin 0 -> 17572 bytes
 ui/.eslintrc                                    |   9 ++
 ui/package.json                                 |   7 +-
 ui/src/main/js/client/scheduler-client.js       |   7 +
 ui/src/main/js/components/Breadcrumb.js         |  39 +++++
 ui/src/main/js/components/Home.js               |   5 +-
 ui/src/main/js/components/Icon.js               |   5 +
 ui/src/main/js/components/Loading.js            |   5 +
 ui/src/main/js/components/Navigation.js         |  19 +++
 ui/src/main/js/components/RoleList.js           |  48 ++++++
 .../js/components/__tests__/Breadcrumb-test.js  |  35 ++++
 .../main/js/components/__tests__/Home-test.js   |   2 +-
 ui/src/main/js/index.js                         |  11 +-
 ui/src/main/js/pages/Home.js                    |  38 +++++
 ui/src/main/js/pages/__tests__/Home-test.js     |  42 +++++
 ui/src/main/js/utils/ShallowRender.js           | 160 +++++++++++++++++++
 .../js/utils/__tests__/ShallowRender-test.js    |  86 ++++++++++
 ui/src/main/sass/app.scss                       |  13 ++
 ui/src/main/sass/components/_base.scss          |  11 ++
 ui/src/main/sass/components/_breadcrumb.scss    |  19 +++
 ui/src/main/sass/components/_home-page.scss     |   0
 ui/src/main/sass/components/_layout.scss        |   5 +
 ui/src/main/sass/components/_navigation.scss    |  23 +++
 ui/src/main/sass/components/_tables.scss        |  93 +++++++++++
 ui/src/main/sass/modules/_all.scss              |   2 +
 ui/src/main/sass/modules/_colors.scss           |  15 ++
 ui/src/main/sass/modules/_typography.scss       |   7 +
 27 files changed, 699 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/src/main/resources/scheduler/assets/images/aurora_logo_white.png
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/images/aurora_logo_white.png 
b/src/main/resources/scheduler/assets/images/aurora_logo_white.png
new file mode 100644
index 0000000..d3dd0e1
Binary files /dev/null and 
b/src/main/resources/scheduler/assets/images/aurora_logo_white.png differ

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/.eslintrc
----------------------------------------------------------------------
diff --git a/ui/.eslintrc b/ui/.eslintrc
index 355b6a8..8d37c60 100644
--- a/ui/.eslintrc
+++ b/ui/.eslintrc
@@ -5,10 +5,16 @@
     "standard",
     "standard-react"
   ],
+  "env": {
+    "jasmine": true
+  },
   "globals": {
     "Thrift": true,
     "ReadOnlySchedulerClient"
   },
+  "plugins": [
+    "chai-friendly"
+  ],
   "rules": {
     "arrow-parens": [2, "always"],
     // not covered in standard
@@ -25,5 +31,8 @@
     "react/prop-types": 0,
     "react/sort-prop-types": 2,
     "camelcase": [2, {"properties": "never"}],
+    // deals with the chai expression problem
+    "no-unused-expressions": 0,
+    "chai-friendly/no-unused-expressions": 2
   }
 }

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/package.json
----------------------------------------------------------------------
diff --git a/ui/package.json b/ui/package.json
index f712518..d680202 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -6,7 +6,8 @@
   "dependencies": {
     "preact": "^8.2.1",
     "preact-compat": "^3.17.0",
-    "react-router-dom": "^4.1.2"
+    "react-router-dom": "^4.1.2",
+    "reactable": "^0.14.1"
   },
   "devDependencies": {
     "babel-core": "^6.26.0",
@@ -18,9 +19,11 @@
     "babel-preset-react": "^6.24.1",
     "chai": "^4.1.1",
     "css-loader": "^0.28.5",
+    "deep-equal": "^1.0.1",
     "eslint": "^4.4.1",
     "eslint-config-standard": "^10.2.1",
     "eslint-config-standard-react": "^5.0.0",
+    "eslint-plugin-chai-friendly": "^0.4.0",
     "eslint-plugin-import": "^2.7.0",
     "eslint-plugin-node": "^5.1.1",
     "eslint-plugin-promise": "^3.5.0",
@@ -40,7 +43,7 @@
     "webpack": "^2.6.1"
   },
   "scripts": {
-    "lint": "eslint ui/src/main/js --ext .js",
+    "lint": "eslint src/main/js --ext .js",
     "test": "NODE_ENV=test karma start karma.conf.js"
   },
   "repository": {

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/client/scheduler-client.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/client/scheduler-client.js 
b/ui/src/main/js/client/scheduler-client.js
new file mode 100644
index 0000000..1c38108
--- /dev/null
+++ b/ui/src/main/js/client/scheduler-client.js
@@ -0,0 +1,7 @@
+function makeClient() {
+  const transport = new Thrift.Transport('/api');
+  const protocol = new Thrift.Protocol(transport);
+  return new ReadOnlySchedulerClient(protocol);
+}
+
+export default makeClient();

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/Breadcrumb.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/Breadcrumb.js 
b/ui/src/main/js/components/Breadcrumb.js
new file mode 100644
index 0000000..76c6270
--- /dev/null
+++ b/ui/src/main/js/components/Breadcrumb.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+function url(...args) {
+  return args.join('/');
+}
+
+export default function Breadcrumb({ cluster, role, env, name, instance, 
update }) {
+  const crumbs = [<Link key='cluster' to='/scheduler'>{cluster}</Link>];
+  if (role) {
+    crumbs.push(<span key='role-divider'>/</span>);
+    crumbs.push(<Link key='role' to={`/scheduler/${url(role)}`}>{role}</Link>);
+  }
+  if (env) {
+    crumbs.push(<span key='env-divider'>/</span>);
+    crumbs.push(<Link key='env' to={`/scheduler/${url(role, 
env)}`}>{env}</Link>);
+  }
+  if (name) {
+    crumbs.push(<span key='name-divider'>/</span>);
+    crumbs.push(<Link key='name' to={`/scheduler/${url(role, env, 
name)}`}>{name}</Link>);
+  }
+  if (instance) {
+    crumbs.push(<span key='instance-divider'>/</span>);
+    crumbs.push(<Link key='instance' to={`/scheduler/${url(role, env, name, 
instance)}`}>
+      {instance}
+    </Link>);
+  }
+  if (update) {
+    crumbs.push(<span key='update-divider'>/</span>);
+    crumbs.push(<Link key='update' to={`/scheduler/${url(role, env, name, 
'update', update)}`}>
+      {update}
+    </Link>);
+  }
+  return (<div className='aurora-breadcrumb'>
+    <div className='container'>
+      <h2>{crumbs}</h2>
+    </div>
+  </div>);
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/Home.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/Home.js 
b/ui/src/main/js/components/Home.js
index 91d60b3..619440f 100644
--- a/ui/src/main/js/components/Home.js
+++ b/ui/src/main/js/components/Home.js
@@ -1,4 +1,5 @@
 import React from 'react';
-import ReactDOM from 'react-dom';
 
-export default () => <div>Hello, World!</div>;
+export default function Home() {
+  return <div>Hello, World!</div>;
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/Icon.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/Icon.js 
b/ui/src/main/js/components/Icon.js
new file mode 100644
index 0000000..43caf84
--- /dev/null
+++ b/ui/src/main/js/components/Icon.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default function Icon({ name }) {
+  return <span className={`glyphicon glyphicon-${name}`} />;
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/Loading.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/Loading.js 
b/ui/src/main/js/components/Loading.js
new file mode 100644
index 0000000..4d732ab
--- /dev/null
+++ b/ui/src/main/js/components/Loading.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default function Loading() {
+  return <div>Loading...</div>;
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/Navigation.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/Navigation.js 
b/ui/src/main/js/components/Navigation.js
new file mode 100644
index 0000000..e46cafd
--- /dev/null
+++ b/ui/src/main/js/components/Navigation.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+export default function Navigation({ fluid }) {
+  return (
+    <nav className='navbar'>
+      <div className={fluid ? 'container-fluid' : 'container'}>
+        <div className='navbar-header'>
+          <a className='navbar-brand' href='#'>
+            <img alt='Brand' src='/assets/images/aurora_logo_white.png' />
+          </a>
+        </div>
+        <ul className='nav navbar-nav navbar-right'>
+          <li><Link to='/beta/updates'>updates</Link></li>
+        </ul>
+      </div>
+    </nav>
+  );
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/RoleList.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/RoleList.js 
b/ui/src/main/js/components/RoleList.js
new file mode 100644
index 0000000..3259560
--- /dev/null
+++ b/ui/src/main/js/components/RoleList.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import Reactable, { Table, Tr, Thead, Th, Td } from 'reactable';
+
+import Icon from 'components/Icon';
+
+export default class RoleList extends React.Component {
+  setFilter(e) {
+    this.setState({filter: e.target.value});
+  }
+
+  render() {
+    return (<div className='role-list'>
+      <div className='table-input-wrapper'>
+        <Icon name='search' />
+        <input
+          autoFocus
+          onChange={(e) => this.setFilter(e)}
+          placeholder='Search for roles'
+          type='text' />
+      </div>
+      <Table
+        className='aurora-table'
+        defaultSort={{column: 'role'}}
+        filterBy={this.state.filter}
+        filterPlaceholder='Search roles...'
+        filterable={['role']}
+        hideFilterInput
+        itemsPerPage={25}
+        noDataText={'No results found.'}
+        pageButtonLimit={8}
+        sortable={['role',
+          {'column': 'jobs', sortFunction: Reactable.Sort.Numeric},
+          {'column': 'crons', sortFunction: Reactable.Sort.Numeric}]}>
+        <Thead>
+          <Th column='role'>Role</Th>
+          <Th className='number' column='jobs'>Jobs</Th>
+          <Th className='number' column='crons'>Crons</Th>
+        </Thead>
+        {this.props.roles.map((r) => (<Tr key={r.role}>
+          <Td column='role' value={r.role}><Link 
to={`/scheduler/${r.role}`}>{r.role}</Link></Td>
+          <Td className='number' column='jobs'>{r.jobCount}</Td>
+          <Td className='number' column='crons'>{r.cronJobCount}</Td>
+        </Tr>))}
+      </Table>
+    </div>);
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/__tests__/Breadcrumb-test.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/__tests__/Breadcrumb-test.js 
b/ui/src/main/js/components/__tests__/Breadcrumb-test.js
new file mode 100644
index 0000000..18af0fe
--- /dev/null
+++ b/ui/src/main/js/components/__tests__/Breadcrumb-test.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import Breadcrumb from '../Breadcrumb';
+import shallow from 'utils/ShallowRender';
+
+import chai, { expect } from 'chai';
+import assertJsx from 'preact-jsx-chai';
+chai.use(assertJsx);
+
+describe('Breadcrumb', () => {
+  it('Should render cluster crumb', () => {
+    const el = shallow(<Breadcrumb cluster='devcluster' />);
+    expect(el.contains(<Link to='/scheduler'>devcluster</Link>)).to.be.true;
+    expect(el.find(<Link />).length === 1).to.be.true;
+  });
+
+  it('Should render role crumb', () => {
+    const el = shallow(<Breadcrumb cluster='devcluster' role='www-data' />);
+    expect(el.contains(<Link 
to='/scheduler/www-data'>www-data</Link>)).to.be.true;
+    expect(el.find(<Link />).length === 2).to.be.true;
+  });
+
+  it('Should render env crumb', () => {
+    const el = shallow(<Breadcrumb cluster='devcluster' env='prod' 
role='www-data' />);
+    expect(el.contains(<Link 
to='/scheduler/www-data/prod'>prod</Link>)).to.be.true;
+    expect(el.find(<Link />).length === 3).to.be.true;
+  });
+
+  it('Should render name crumb', () => {
+    const el = shallow(<Breadcrumb cluster='devcluster' env='prod' 
name='hello' role='www-data' />);
+    expect(el.contains(<Link 
to='/scheduler/www-data/prod/hello'>hello</Link>)).to.be.true;
+    expect(el.find(<Link />).length === 4).to.be.true;
+  });
+});

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/components/__tests__/Home-test.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/__tests__/Home-test.js 
b/ui/src/main/js/components/__tests__/Home-test.js
index 2a80958..8e6bc09 100644
--- a/ui/src/main/js/components/__tests__/Home-test.js
+++ b/ui/src/main/js/components/__tests__/Home-test.js
@@ -7,6 +7,6 @@ chai.use(assertJsx);
 
 describe('Home', () => {
   it('Should render Hello, World!', () => {
-    expect(<Home/>).to.deep.equal(<div>Hello, World!</div>);
+    expect(<Home />).to.deep.equal(<div>Hello, World!</div>);
   });
 });

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/index.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/index.js b/ui/src/main/js/index.js
index 2f7467b..717aaa0 100644
--- a/ui/src/main/js/index.js
+++ b/ui/src/main/js/index.js
@@ -2,12 +2,19 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import { BrowserRouter as Router, Route } from 'react-router-dom';
 
-import Home from 'components/Home';
+import SchedulerClient from 'client/scheduler-client';
+import Navigation from 'components/Navigation';
+import Home from 'pages/Home';
+
+import styles from '../sass/app.scss'; // eslint-disable-line no-unused-vars
+
+const apiEnabledComponent = (Page) => (props) => <Page api={SchedulerClient} 
{...props} />;
 
 const SchedulerUI = () => (
   <Router>
     <div>
-      <Route component={Home} exact path='/beta/scheduler' />
+      <Navigation />
+      <Route component={apiEnabledComponent(Home)} exact 
path='/beta/scheduler' />
       <Route component={Home} exact path='/beta/scheduler/:role' />
       <Route component={Home} exact path='/beta/scheduler/:role/:environment' 
/>
       <Route component={Home} exact 
path='/beta/scheduler/:role/:environment/:name' />

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/pages/Home.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/pages/Home.js b/ui/src/main/js/pages/Home.js
new file mode 100644
index 0000000..c9bcbfd
--- /dev/null
+++ b/ui/src/main/js/pages/Home.js
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import Breadcrumb from 'components/Breadcrumb';
+import Loading from 'components/Loading';
+import RoleList from 'components/RoleList';
+
+export default class HomePage extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {cluster: '', roles: [], loading: true};
+  }
+
+  componentWillMount(props) {
+    const that = this;
+    this.props.api.getRoleSummary((response) => {
+      that.setState({
+        cluster: response.serverInfo.clusterName,
+        loading: false,
+        roles: response.result.roleSummaryResult.summaries
+      });
+    });
+  }
+
+  render() {
+    return this.state.loading ? <Loading /> : (<div>
+      <Breadcrumb cluster={this.state.cluster} />
+      <div className='container'>
+        <div className='row'>
+          <div className='col-md-12'>
+            <div className='panel'>
+              <RoleList roles={this.state.roles} />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>);
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/pages/__tests__/Home-test.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/pages/__tests__/Home-test.js 
b/ui/src/main/js/pages/__tests__/Home-test.js
new file mode 100644
index 0000000..4f13f99
--- /dev/null
+++ b/ui/src/main/js/pages/__tests__/Home-test.js
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import Home from '../Home';
+import Breadcrumb from 'components/Breadcrumb';
+import Loading from 'components/Loading';
+import RoleList from 'components/RoleList';
+import shallow from 'utils/ShallowRender';
+
+import chai, { expect } from 'chai';
+import assertJsx from 'preact-jsx-chai';
+chai.use(assertJsx);
+
+const TEST_CLUSTER = 'test-cluster';
+
+function createMockApi(roles) {
+  const api = {};
+  api.getRoleSummary = (handler) => handler({
+    result: {
+      roleSummaryResult: {
+        summaries: roles
+      }
+    },
+    serverInfo: {
+      clusterName: TEST_CLUSTER
+    }
+  });
+  return api;
+}
+
+const roles = [{role: 'test', jobCount: 0, cronJobCount: 5}];
+
+describe('Home', () => {
+  it('Should render Loading before data is fetched', () => {
+    expect(<Home api={{getRoleSummary: () => {}}} />).to.deep.equal(<Loading 
/>);
+  });
+
+  it('Should render page elements when roles are fetched', () => {
+    const home = shallow(<Home api={createMockApi(roles)} />);
+    expect(home.contains(<Breadcrumb cluster={TEST_CLUSTER} />)).to.be.true;
+    expect(home.contains(<RoleList roles={roles} />)).to.be.true;
+  });
+});

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/utils/ShallowRender.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/utils/ShallowRender.js 
b/ui/src/main/js/utils/ShallowRender.js
new file mode 100644
index 0000000..52e8bb2
--- /dev/null
+++ b/ui/src/main/js/utils/ShallowRender.js
@@ -0,0 +1,160 @@
+import { options, render } from 'preact';
+import deepEqual from 'deep-equal';
+
+function propsForElement(el) {
+  return el.__preactattr_ || {};
+}
+
+function extractName(vnode) {
+  return (typeof vnode.nodeName === 'string')
+    ? vnode.nodeName
+    : (vnode.nodeName.prototype.displayName || vnode.nodeName.name);
+}
+
+function textChildrenMatch(domNode, vnode) {
+  const textChildren = vnode.children.filter((c) => typeof c === 
'string').map((s) => s.trim());
+  if (textChildren.length === 0) {
+    return true;
+  }
+  return textChildren.join(' ') === domNode.innerText.replace(/ +(?= )/g, '');
+}
+
+function findInSiblings(domNode, vnode) {
+  let cursor = domNode.nextElementSibling;
+  while (cursor !== null) {
+    if (matches(cursor, vnode)) {
+      return cursor;
+    }
+    cursor = cursor.nextElementSibling;
+  }
+  return null;
+}
+
+function hasSiblings(domNode, vnodes) {
+  let cursor = domNode;
+  const found = [];
+  vnodes.forEach((node) => {
+    if (cursor !== null) {
+      cursor = findInSiblings(cursor, node);
+      if (cursor) {
+        found.push(cursor);
+      }
+    }
+  });
+  return found.length === vnodes.length;
+}
+
+function vnodeChildrenPresent(domNode, vnode) {
+  const vnodeChildren = vnode.children.filter((c) => typeof c !== 'string');
+  if (vnodeChildren.length === 0) {
+    return true;
+  }
+
+  // for children we want to maintain two key properties when matching:
+  //  * order of nodes must match
+  //  * number of nodes should match
+  // to do this we try and find all matches for vnodeChildren[0] and then
+  // use the sibling API to verify the rest of the children are present at the 
same
+  // level in the DOM tree
+  const [head, ...tail] = vnodeChildren;
+
+  const matches = allMatches(domNode, head);
+
+  for (let i = 0; i < matches.length; i++) {
+    if (hasSiblings(matches[i], tail)) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+function childrenMatch(domNode, vnode) {
+  if (vnode.attributes.children.length === 0) {
+    return true;
+  }
+  return textChildrenMatch(domNode, vnode) && vnodeChildrenPresent(domNode, 
vnode);
+}
+
+function propertiesMatch(domNode, vnode) {
+  const domProperties = propsForElement(domNode);
+  const vnodeProperties = vnode.attributes;
+  const defaultProperties = vnode.nodeName.defaultProps || {};
+
+  return Object.keys(vnodeProperties).reduce((matches, key) => {
+    if (key === 'children') {
+      return matches && childrenMatch(domNode, vnode);
+    }
+    if (defaultProperties.hasOwnProperty(key) && vnodeProperties[key] === 
defaultProperties[key]) {
+      return matches;
+    }
+    return matches && deepEqual(domProperties[key], vnodeProperties[key]);
+  }, true);
+}
+
+function allMatches(dom, vnode) {
+  const candidates = dom.querySelectorAll(extractName(vnode));
+  const matches = [];
+  for (let i = 0; i < candidates.length; i++) {
+    if (propertiesMatch(candidates[i], vnode)) {
+      matches.push(candidates[i]);
+    }
+  }
+  return matches;
+}
+
+function domContains(dom, vnode) {
+  return allMatches(dom, vnode).length > 0;
+}
+
+function matches(dom, vnode) {
+  if (dom.nodeName.toLowerCase() === extractName(vnode).toLowerCase()) {
+    return propertiesMatch(dom, vnode);
+  }
+  return false;
+}
+
+// Renders a shallow representation of the vnode into the DOM.
+function shallowRender(preactEl, domEl) {
+  // Override the `vnode` hook to transform composite components in the render
+  // output into DOM elements.
+  const oldVnodeHook = options.vnode;
+  const vnodeHook = (node) => {
+    if (oldVnodeHook) {
+      oldVnodeHook(node);
+    }
+    if (typeof node.nodeName === 'string') {
+      return;
+    }
+    node.nodeName = node.nodeName.name; // eslint-disable-line 
no-param-reassign
+  };
+
+  try {
+    options.vnode = vnodeHook;
+    const el = render(preactEl, domEl);
+    options.vnode = oldVnodeHook;
+    return el;
+  } catch (err) {
+    options.vnode = oldVnodeHook;
+    throw err;
+  }
+}
+
+// Primary interface for testing. The idea is that the vnode you supply will 
be used for property
+// equality comparisons and non-provided properties are ignored. i.e. it is 
considered a match
+// whenever any element in the DOM has at least the properties of the vnode.
+export default function wrapper(preactEl) {
+  const shallow = shallowRender(preactEl, document.createElement('div'));
+  return {
+    __element: shallow,
+    contains: (vnode, matchExactly = false) => {
+      return domContains(shallow, vnode);
+    },
+    is: (vnode, matchExactly = false) => {
+      return matches(shallow, vnode);
+    },
+    find: (vnode) => {
+      return allMatches(shallow, vnode);
+    }
+  };
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/js/utils/__tests__/ShallowRender-test.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/utils/__tests__/ShallowRender-test.js 
b/ui/src/main/js/utils/__tests__/ShallowRender-test.js
new file mode 100644
index 0000000..d5663a7
--- /dev/null
+++ b/ui/src/main/js/utils/__tests__/ShallowRender-test.js
@@ -0,0 +1,86 @@
+import React from 'react';
+
+import { expect } from 'chai';
+
+import shallow from '../ShallowRender';
+
+class Leaf extends React.Component {
+  render() {
+    return <div>Leaf</div>;
+  }
+}
+
+class Node extends React.Component {
+  render() {
+    return <div><Leaf {...this.props} /> <span /> <div>Something 
Else</div></div>;
+  }
+}
+
+class ThinWrapper extends React.Component {
+  render() {
+    return <Leaf {...this.props} />;
+  }
+}
+
+class List extends React.Component {
+  render() {
+    return <ul><li><Leaf /></li><li><Leaf /></li></ul>;
+  }
+}
+
+class GeneratedList extends React.Component {
+  render() {
+    return (<div><ul>{this.props.names.map((i) => <Leaf name={i} 
/>)}</ul></div>);
+  }
+}
+
+describe('shallow::contains', () => {
+  it('Should respect shallow rendering', () => {
+    expect(shallow(<Node />).contains(<Leaf />)).to.be.true;
+  });
+
+  it('Should handle multiple elements', () => {
+    const el = shallow(<div><Node name='jon' /><Node name='dany' /></div>);
+    expect(el.contains(<Leaf name='jon' />)).to.be.true;
+    expect(el.contains(<Leaf name='dany' />)).to.be.true;
+  });
+
+  it('Should match properties based on target node', () => {
+    const el = shallow(<Node name='jon' surname='snow' />);
+    expect(el.contains(<Leaf name='jon' surname='snow' />)).to.be.true;
+    expect(el.contains(<Leaf name='jon' />)).to.be.true;
+    expect(el.contains(<Leaf surname='snow' />)).to.be.true;
+    expect(el.contains(<Leaf />)).to.be.true;
+    expect(el.contains(<Leaf name='jon' surname='snow'>jon 
snow</Leaf>)).to.be.false;
+  });
+
+  it('Should match children with text', () => {
+    expect(shallow(<Node />).contains(<div>Something Else</div>)).to.be.true;
+    expect(shallow(<Node />).contains(<div>Not Present</div>)).to.be.false;
+  });
+
+  it('Should work with deeply nested tree', () => {
+    expect(shallow(<List />).contains(<li><Leaf /></li>)).to.be.true;
+    expect(shallow(<List />).contains(<Leaf />)).to.be.true;
+  });
+
+  it('Should respect ordering of nested items', () => {
+    const generated = shallow(<GeneratedList names={['jon', 'dany']} />);
+    expect(generated.contains(<ul><Leaf name='jon' /><Leaf name='dany' 
/></ul>)).to.be.true;
+    expect(generated.contains(<ul><Leaf name='dany' /></ul>)).to.be.true;
+    expect(generated.contains(<ul><Leaf name='dany' /><Leaf name='jon' 
/></ul>)).to.be.false;
+  });
+});
+
+describe('shallow::is', () => {
+  it('Should handle standard HTML elements', () => {
+    expect(shallow(<ThinWrapper />).is(<Leaf />)).to.be.true;
+  });
+
+  it('Should handle lists', () => {
+    expect(shallow(<List />).is(<ul />)).to.be.true;
+    expect(shallow(<List />).is(<ul><li><Leaf /></li><li><Leaf 
/></li></ul>)).to.be.true;
+    expect(shallow(<List />)
+      .is(<ul><li><Leaf /></li><li><Leaf /></li><li><Leaf 
/></li></ul>)).to.be.false;
+  });
+});

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/app.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/app.scss b/ui/src/main/sass/app.scss
new file mode 100644
index 0000000..0b3967d
--- /dev/null
+++ b/ui/src/main/sass/app.scss
@@ -0,0 +1,13 @@
+/* Variables, Mix-Ins, etc. */
+@import 'components/base';
+
+/* Main grid elements and aesthetics. */
+@import 'components/layout';
+
+/* Indiviudal Components */
+@import 'components/breadcrumb';
+@import 'components/navigation';
+@import 'components/tables';
+
+/* Page Styles */
+@import 'components/home-page';
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/components/_base.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/components/_base.scss 
b/ui/src/main/sass/components/_base.scss
new file mode 100644
index 0000000..fadd0d2
--- /dev/null
+++ b/ui/src/main/sass/components/_base.scss
@@ -0,0 +1,11 @@
+@import 'modules/all';
+
+html, html a {
+  -webkit-font-smoothing: antialiased !important;
+  text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
+}
+
+html, body {
+  font-family: $font_stack;
+  background-color: $main_bg_color;
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/components/_breadcrumb.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/components/_breadcrumb.scss 
b/ui/src/main/sass/components/_breadcrumb.scss
new file mode 100644
index 0000000..67c0c98
--- /dev/null
+++ b/ui/src/main/sass/components/_breadcrumb.scss
@@ -0,0 +1,19 @@
+.aurora-breadcrumb {
+  padding: 1.5em 0;
+  border-bottom: 1px solid #EEE;
+  background-color: #FFF;
+  margin-bottom: 20px;
+
+  h2 {
+    margin: 0;
+    font-size: 20px;
+  }
+
+  a:last-child {
+    font-weight: 700;
+  }
+
+  span {
+    margin: 0 8px;
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/components/_home-page.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/components/_home-page.scss 
b/ui/src/main/sass/components/_home-page.scss
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/components/_layout.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/components/_layout.scss 
b/ui/src/main/sass/components/_layout.scss
new file mode 100644
index 0000000..b8a83b5
--- /dev/null
+++ b/ui/src/main/sass/components/_layout.scss
@@ -0,0 +1,5 @@
+.panel {
+  background-color: $content_box_color;
+  margin: 10px 0;
+  padding: 20px;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/components/_navigation.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/components/_navigation.scss 
b/ui/src/main/sass/components/_navigation.scss
new file mode 100644
index 0000000..ecf0ca6
--- /dev/null
+++ b/ui/src/main/sass/components/_navigation.scss
@@ -0,0 +1,23 @@
+.nav-divider {
+  height: 3px;
+  margin-top: -20px;
+}
+
+.navbar {
+  text-transform: uppercase;
+  font-size: 11px;
+  min-height: 80px;
+  border-radius: 0px;
+  background-color: $main_contrast_color;
+  margin-bottom: 0px;
+}
+
+.navbar-nav>li>a {
+  line-height: 50px;
+  font-size: 14px;
+  color: $content_box_color;
+}
+
+.navbar img {
+  height: 50px;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/components/_tables.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/components/_tables.scss 
b/ui/src/main/sass/components/_tables.scss
new file mode 100644
index 0000000..58f176c
--- /dev/null
+++ b/ui/src/main/sass/components/_tables.scss
@@ -0,0 +1,93 @@
+.aurora-table {
+  width: 100%;
+  font-size: 16px;
+
+  a {
+   font-weight: 600;
+  }
+
+  .number {
+    text-align: center;
+  }
+
+  th {
+   padding: 5px;
+  }
+}
+
+.reactable-data {
+  border: 1px solid $grid_color;
+
+  tr + tr {
+    border-top: 1px solid $grid_color;
+  }
+
+  td + td {
+    border-left: 1px solid $grid_color;
+  }
+
+  tr:nth-child(even) {
+    background: rgba(0,0,0,0.017);
+  }
+
+  tr:hover {
+    background: #edf5fd;
+  }
+
+  td, th {
+    padding: 5px;
+  }
+}
+
+.reactable-pagination {
+  td {
+    padding: 2em 0 4em 0;
+    text-align: center;
+  }
+
+  a {
+    padding: 6px 12px;
+    border: 1px solid $grid_highlight_color;
+    border-left: 0px;
+  }
+
+  a:first-child {
+    padding: 6px 12px;
+    border-left: 1px solid $grid_highlight_color;
+  }
+
+  a:hover {
+    background-color: steelblue;
+    border: 1px solid #FFF;
+    color: white;
+  }
+}
+
+.reactable-current-page {
+  font-weight: normal;
+  color: #222;
+}
+
+.table-input-wrapper {
+  border-radius: 4px;
+  padding: 5px;
+  background-color: $grid_color;
+  margin-bottom: 10px;
+
+  .glyphicon {
+    font-size: 14px;
+    color: $secondary_font_color;
+    margin: 0px 5px;
+  }
+
+  input {
+    width: 90%;
+    font-size: 14px;
+    border: 0;
+    background-color: transparent;
+  }
+
+  input:focus {
+    outline: none;
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/modules/_all.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/modules/_all.scss 
b/ui/src/main/sass/modules/_all.scss
new file mode 100644
index 0000000..b630cb5
--- /dev/null
+++ b/ui/src/main/sass/modules/_all.scss
@@ -0,0 +1,2 @@
+@import './typography';
+@import './colors';
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/modules/_colors.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/modules/_colors.scss 
b/ui/src/main/sass/modules/_colors.scss
new file mode 100644
index 0000000..147cff6
--- /dev/null
+++ b/ui/src/main/sass/modules/_colors.scss
@@ -0,0 +1,15 @@
+/* Layout Colors */
+$main_bg_color: rgba(0, 0, 0, 0.02);
+$content_box_color: #FFF;
+$main_contrast_color: #222;
+$grid_color: #EEE;
+$grid_highlight_color: #DDD;
+
+$primary_font_color: #222;
+$secondary_font_color: #999;
+
+$success_color: #74C080;
+$success_secondary_color: #afe8b8;
+
+$error_color: #d63c39;
+$error_secondary_color: rgb(230, 101, 98);

http://git-wip-us.apache.org/repos/asf/aurora/blob/8d5f6a22/ui/src/main/sass/modules/_typography.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/modules/_typography.scss 
b/ui/src/main/sass/modules/_typography.scss
new file mode 100644
index 0000000..03117b2
--- /dev/null
+++ b/ui/src/main/sass/modules/_typography.scss
@@ -0,0 +1,7 @@
+$font_stack: 'Source Sans Pro', Helvetica, sans-serif;
+
+$light: 100;
+$normal: 400;
+$bold: 500;
+$heavy: 700;
+$title: 900;

Reply via email to