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;
