Repository: aurora Updated Branches: refs/heads/master 6fd6d5028 -> 4e7cdc422
Implement Role and Environment pages in Preact. Reviewed at https://reviews.apache.org/r/62451/ Project: http://git-wip-us.apache.org/repos/asf/aurora/repo Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/4e7cdc42 Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/4e7cdc42 Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/4e7cdc42 Branch: refs/heads/master Commit: 4e7cdc422b033a89337d59541e1979283f363016 Parents: 6fd6d50 Author: David McLaughlin <[email protected]> Authored: Tue Oct 3 12:49:38 2017 -0700 Committer: David McLaughlin <[email protected]> Committed: Tue Oct 3 12:49:38 2017 -0700 ---------------------------------------------------------------------- ui/src/main/js/components/JobList.js | 67 ++++++++++++++++ ui/src/main/js/components/JobListItem.js | 45 +++++++++++ ui/src/main/js/components/Layout.js | 16 ++++ ui/src/main/js/components/Pagination.js | 9 ++- ui/src/main/js/components/RoleQuota.js | 78 +++++++++++++++++++ .../js/components/__tests__/JobList-test.js | 25 ++++++ .../js/components/__tests__/RoleQuota-test.js | 35 +++++++++ ui/src/main/js/index.js | 9 ++- ui/src/main/js/pages/Jobs.js | 60 ++++++++++++++ ui/src/main/js/pages/__tests__/Jobs-test.js | 65 ++++++++++++++++ ui/src/main/js/utils/Common.js | 3 + ui/src/main/js/utils/Job.js | 3 + ui/src/main/sass/app.scss | 3 +- ui/src/main/sass/components/_job-list-page.scss | 82 ++++++++++++++++++++ ui/src/main/sass/components/_layout.scss | 27 +++++++ ui/src/main/sass/components/_tables.scss | 22 ++++++ ui/src/main/sass/modules/_colors.scss | 20 +++++ 17 files changed, 562 insertions(+), 7 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/components/JobList.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/JobList.js b/ui/src/main/js/components/JobList.js new file mode 100644 index 0000000..ff5377d --- /dev/null +++ b/ui/src/main/js/components/JobList.js @@ -0,0 +1,67 @@ +import React from 'react'; + +import Icon from 'components/Icon'; +import JobListItem from 'components/JobListItem'; +import Pagination from 'components/Pagination'; + +import { isNully } from 'utils/Common'; +import { TASK_COUNTS } from 'utils/Job'; + +export function JobListSortControl({ onClick }) { + return (<ul className='job-task-stats job-list-sort-control'> + <li>sort by:</li> + {TASK_COUNTS.map((key) => { + const label = key.replace('TaskCount', ''); + return (<li key={key} onClick={(e) => onClick(key)}> + <span className={`img-circle ${label}-task`} /> {label} + </li>); + })} + </ul>); +} + +export default class JobList extends React.Component { + constructor(props) { + super(props); + this.state = {filter: props.filter, sortBy: props.sortBy}; + } + + setFilter(e) { + this.setState({filter: e.target.value}); + } + + setSort(sortBy) { + this.setState({sortBy}); + } + + _renderRow(job) { + return <JobListItem job={job} key={`${job.job.key.environment}/${job.job.key.name}`} />; + } + + render() { + const that = this; + const sortFn = this.state.sortBy ? (j) => j.stats[that.state.sortBy] : (j) => j.job.key.name; + const filterFn = (j) => that.state.filter ? j.job.key.name.startsWith(that.state.filter) : true; + return (<div className='job-list'> + <div className='table-input-wrapper'> + <Icon name='search' /> + <input + autoFocus + onChange={(e) => this.setFilter(e)} + placeholder='Search jobs...' + type='text' /> + </div> + <JobListSortControl onClick={(key) => this.setSort(key)} /> + <table className='psuedo-table'> + <Pagination + data={this.props.jobs} + filter={filterFn} + isTable + numberPerPage={25} + renderer={this._renderRow} + // Always sort task count sorts in descending fashion (for UX reasons) + reverseSort={!isNully(this.state.sortBy)} + sortBy={sortFn} /> + </table> + </div>); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/components/JobListItem.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/JobListItem.js b/ui/src/main/js/components/JobListItem.js new file mode 100644 index 0000000..5a461dd --- /dev/null +++ b/ui/src/main/js/components/JobListItem.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import Icon from 'components/Icon'; + +import { TASK_COUNTS } from 'utils/Job'; + +export function JobTaskStats({ stats }) { + const taskStats = []; + TASK_COUNTS.forEach((k) => { + if (stats[k] > 0) { + const label = k.replace('TaskCount', ''); + taskStats.push(<li key={k}><span className={`img-circle ${label}-task`} /> {stats[k]} </li>); + } + }); + return <ul className='job-task-stats'>{taskStats}</ul>; +} + +export default function JobListItem(props) { + const {job: {job: { cronSchedule, key: {role, name, environment}, taskConfig }, stats}} = props; + + const envLink = (props.env) ? '' : (<span className='job-env'> + <Link to={`/beta/scheduler/${role}/${environment}`}>{environment}</Link> + </span>); + + return (<tr key={`${environment}/${name}`}> + <td className='job-list-type' column='type'> + <span className='job-tier'> + {taskConfig.isService ? 'service' : (cronSchedule) ? 'cron' : 'adhoc'} + </span> + </td> + <td className='job-list-name' column='name' value={name}> + <h4> + {envLink} + <Link to={`/beta/scheduler/${role}/${environment}/${name}`}> + {name} + {taskConfig.production ? <Icon name='star' /> : ''} + </Link> + </h4> + </td> + <td className='job-list-stats' column='stats'> + <JobTaskStats stats={stats} /> + </td> + </tr>); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/components/Layout.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/Layout.js b/ui/src/main/js/components/Layout.js new file mode 100644 index 0000000..4ca54e3 --- /dev/null +++ b/ui/src/main/js/components/Layout.js @@ -0,0 +1,16 @@ +import React from 'react'; + +function ContentPanel({ children }) { + return <div className='content-panel'>{children}</div>; +} + +export function StandardPanelTitle({ title }) { + return <div className='content-panel-title'>{title}</div>; +} + +export default function PanelGroup({ children, title }) { + return (<div className='content-panel-group'> + {title} + {React.Children.map(children, (p) => <ContentPanel>{p}</ContentPanel>)} + </div>); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/components/Pagination.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/Pagination.js b/ui/src/main/js/components/Pagination.js index dec89be..7bf2c04 100644 --- a/ui/src/main/js/components/Pagination.js +++ b/ui/src/main/js/components/Pagination.js @@ -56,6 +56,11 @@ export default class Pagination extends React.Component { const { reverseSort, sortBy } = this.props; const gte = reverseSort ? -1 : 1; const lte = reverseSort ? 1 : -1; + if (typeof sortBy === 'function') { + return data.sort((a, b) => { + return (sortBy(a) > sortBy(b)) ? gte : lte; + }); + } return data.sort((a, b) => { return (a[sortBy] > b[sortBy]) ? gte : lte; }); @@ -88,8 +93,8 @@ export default class Pagination extends React.Component { numPages={Math.ceil(filtered.length / numberPerPage)} onClick={(page) => that.changePage(page)} />; - // React/JSX statements must resolve to a single node, so we need to wrap the page in a parent. - // We need the caller to be able to signify they are paging through a table element. + // We need the caller to be able to signify they are paging through a table element so + // we know to wrap the pagination links in a tr. if (isTable) { return (<tbody> {elements} http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/components/RoleQuota.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/RoleQuota.js b/ui/src/main/js/components/RoleQuota.js new file mode 100644 index 0000000..36a69b4 --- /dev/null +++ b/ui/src/main/js/components/RoleQuota.js @@ -0,0 +1,78 @@ +import React from 'react'; + +import { isNully } from 'utils/Common'; + +const QUOTA_TYPE_ORDER = [ + 'quota', + 'prodSharedConsumption', + 'prodDedicatedConsumption', + 'nonProdSharedConsumption', + 'nonProdDedicatedConsumption' +]; + +// @VisibleForTesting +export const QUOTA_TYPE_MAP = { + 'quota': 'Quota', + 'prodSharedConsumption': 'Quota Used', + 'nonProdSharedConsumption': 'Non-Production', + 'prodDedicatedConsumption': 'Production Dedicated', + 'nonProdDedicatedConsumption': 'Non-Production Dedicated' +}; + +const UNITS = ['MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + +function formatMb(sizeInMb) { + const unitIdx = (sizeInMb > 0) ? Math.floor(Math.log(sizeInMb) / Math.log(1024)) : 0; + return (sizeInMb / Math.pow(1024, unitIdx)).toFixed(2) + '' + UNITS[unitIdx]; +} + +const CONVERSIONS = { + diskMb: formatMb, + ramMb: formatMb +}; + +function format(resource) { + const resourceKey = Object.keys(resource).find((key) => !isNully(resource[key])); + return (CONVERSIONS[resourceKey]) + ? CONVERSIONS[resourceKey](resource[resourceKey]) + : resource[resourceKey]; +} + +function getResource(resources, key) { + return format(resources.find((r) => !isNully(r[key]))); +} + +function findResource(resource) { + const resourceKey = Object.keys(resource).find((key) => !isNully(resource[key])); + return resource[resourceKey]; +} + +const totalResources = (resources) => resources.map(findResource).reduce((acc, val) => acc + val); + +export default function RoleQuota({ quota }) { + // Only show quota types with non-zero values. + const quotas = QUOTA_TYPE_ORDER.filter((t) => totalResources(quota[t].resources) > 0); + + return (<div className='role-quota'> + <table className='aurora-table'> + <thead> + <tr> + <th> </th> + <th>cpus</th> + <th>ram</th> + <th>disk</th> + </tr> + </thead> + <tbody> + {quotas.map((t) => ( + <tr key={t}> + <td>{QUOTA_TYPE_MAP[t]}</td> + <td>{getResource(quota[t].resources, 'numCpus')}</td> + <td>{getResource(quota[t].resources, 'ramMb')}</td> + <td>{getResource(quota[t].resources, 'diskMb')}</td> + </tr> + ))} + </tbody> + </table> + </div>); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/components/__tests__/JobList-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/__tests__/JobList-test.js b/ui/src/main/js/components/__tests__/JobList-test.js new file mode 100644 index 0000000..f545c24 --- /dev/null +++ b/ui/src/main/js/components/__tests__/JobList-test.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import JobList from '../JobList'; +import Pagination from '../Pagination'; + +const jobs = []; // only need referential equality for tests + +describe('JobList', () => { + it('Should delegate all the heavy lifting to Pagination', () => { + const el = shallow(<JobList jobs={jobs} />); + expect(el.find(Pagination).length).toBe(1); + expect(el.containsAllMatchingElements([ + <Pagination data={jobs} reverseSort={false} /> + ])).toBe(true); + }); + + it('Should reverse sort whenever a task sort is supplied', () => { + const el = shallow(<JobList jobs={jobs} sortBy='test' />); + expect(el.find(Pagination).length).toBe(1); + expect(el.containsAllMatchingElements([ + <Pagination data={jobs} reverseSort /> + ])).toBe(true); + }); +}); http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/components/__tests__/RoleQuota-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/__tests__/RoleQuota-test.js b/ui/src/main/js/components/__tests__/RoleQuota-test.js new file mode 100644 index 0000000..57f332d --- /dev/null +++ b/ui/src/main/js/components/__tests__/RoleQuota-test.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import RoleQuota, { QUOTA_TYPE_MAP } from '../RoleQuota'; + +function createResources(cpus, mem, disk) { + return [{ + 'numCpus': cpus + }, { + 'ramMb': mem + }, { + 'diskMb': disk + }]; +} + +function initQuota() { + const quota = {}; + Object.keys(QUOTA_TYPE_MAP).forEach((key) => { + quota[key] = {resources: createResources(0, 0, 0)}; + }); + return quota; +} + +describe('RoleQuota', () => { + it('Should show all resources used', () => { + Object.keys(QUOTA_TYPE_MAP).forEach((key) => { + const quota = initQuota(); + quota[key] = {resources: createResources(10, 1024, 1024)}; + const el = shallow(<RoleQuota quota={quota} />); + expect(el.contains(<tr key={key}> + <td>{QUOTA_TYPE_MAP[key]}</td><td>{10}</td><td>{'1.00GiB'}</td><td>{'1.00GiB'}</td> + </tr>)).toBe(true); + }); + }); +}); http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/index.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/index.js b/ui/src/main/js/index.js index 717aaa0..4a879e6 100644 --- a/ui/src/main/js/index.js +++ b/ui/src/main/js/index.js @@ -5,18 +5,19 @@ import { BrowserRouter as Router, Route } from 'react-router-dom'; import SchedulerClient from 'client/scheduler-client'; import Navigation from 'components/Navigation'; import Home from 'pages/Home'; +import Jobs from 'pages/Jobs'; import styles from '../sass/app.scss'; // eslint-disable-line no-unused-vars -const apiEnabledComponent = (Page) => (props) => <Page api={SchedulerClient} {...props} />; +const injectApi = (Page) => (props) => <Page api={SchedulerClient} {...props} />; const SchedulerUI = () => ( <Router> <div> <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={injectApi(Home)} exact path='/beta/scheduler' /> + <Route component={injectApi(Jobs)} exact path='/beta/scheduler/:role' /> + <Route component={injectApi(Jobs)} exact path='/beta/scheduler/:role/:environment' /> <Route component={Home} exact path='/beta/scheduler/:role/:environment/:name' /> <Route component={Home} exact path='/beta/scheduler/:role/:environment/:name/:instance' /> <Route component={Home} exact path='/beta/scheduler/:role/:environment/:name/update/:uid' /> http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/pages/Jobs.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/pages/Jobs.js b/ui/src/main/js/pages/Jobs.js new file mode 100644 index 0000000..d3bef8c --- /dev/null +++ b/ui/src/main/js/pages/Jobs.js @@ -0,0 +1,60 @@ +import React from 'react'; + +import Breadcrumb from 'components/Breadcrumb'; +import JobList from 'components/JobList'; +import PanelGroup, { StandardPanelTitle } from 'components/Layout'; +import Loading from 'components/Loading'; +import RoleQuota from 'components/RoleQuota'; + +import { isNully } from 'utils/Common'; + +export default class Jobs extends React.Component { + constructor(props) { + super(props); + this.state = {cluster: '', jobs: [], loading: isNully(props.jobs)}; + } + + componentWillMount() { + const that = this; + this.props.api.getJobSummary(this.props.match.params.role, (response) => { + const jobs = (that.props.match.params.environment) + ? response.result.jobSummaryResult.summaries.filter( + (j) => j.job.key.environment === that.props.match.params.environment) + : response.result.jobSummaryResult.summaries; + that.setState({ cluster: response.serverInfo.clusterName, loading: false, jobs }); + }); + + this.props.api.getQuota(this.props.match.params.role, (response) => { + that.setState({ + cluster: response.serverInfo.clusterName, + loading: false, + quota: response.result.getQuotaResult + }); + }); + } + + render() { + return this.state.loading ? <Loading /> : (<div> + <Breadcrumb + cluster={this.state.cluster} + env={this.props.match.params.environment} + role={this.props.match.params.role} /> + <div className='container'> + <div className='row'> + <div className='col-md-12'> + <PanelGroup title={<StandardPanelTitle title='Resources' />}> + <RoleQuota quota={this.state.quota} /> + </PanelGroup> + </div> + </div> + <div className='row'> + <div className='col-md-12'> + <PanelGroup title={<StandardPanelTitle title='Jobs' />}> + <JobList env={this.props.match.params.environment} jobs={this.state.jobs} /> + </PanelGroup> + </div> + </div> + </div> + </div>); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/pages/__tests__/Jobs-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/pages/__tests__/Jobs-test.js b/ui/src/main/js/pages/__tests__/Jobs-test.js new file mode 100644 index 0000000..4fc6778 --- /dev/null +++ b/ui/src/main/js/pages/__tests__/Jobs-test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Jobs from '../Jobs'; +import Breadcrumb from 'components/Breadcrumb'; +import JobList from 'components/JobList'; +import Loading from 'components/Loading'; +import RoleQuota from 'components/RoleQuota'; + +const TEST_CLUSTER = 'test-cluster'; +const TEST_ENV = 'test-env'; +const TEST_ROLE = 'test-role'; + +function createMockApi(jobs, quota) { + const api = {}; + api.getJobSummary = (role, handler) => handler({ + result: { + jobSummaryResult: { + summaries: jobs + } + }, + serverInfo: { + clusterName: TEST_CLUSTER + } + }); + + api.getQuota = (role, handler) => handler({ + result: { + getQuotaResult: quota + }, + serverInfo: { + clusterName: TEST_CLUSTER + } + }); + + return api; +} + +const jobs = [{job: {key: {environment: TEST_ENV}}}, {job: {key: {environment: 'wrong-env'}}}]; +const quota = {}; // No keys are accessed, so just do reference equality later. + +describe('Jobs', () => { + it('Should render Loading before data is fetched', () => { + expect(shallow(<Jobs + api={{getJobSummary: () => {}, getQuota: () => {}}} + match={{params: {role: TEST_ROLE}}} />).equals(<Loading />)).toBe(true); + }); + + it('Should render page elements when jobs are fetched', () => { + const el = shallow( + <Jobs api={createMockApi(jobs, quota)} match={{params: {role: TEST_ROLE}}} />); + expect(el.contains( + <Breadcrumb cluster={TEST_CLUSTER} env={undefined} role={TEST_ROLE} />)).toBe(true); + expect(el.find(JobList).length).toBe(1); + expect(el.find(RoleQuota).length).toBe(1); + }); + + it('Should pass through environment path parameter and filter jobs', () => { + const home = shallow(<Jobs + api={createMockApi(jobs)} + match={{params: {environment: TEST_ENV, role: TEST_ROLE}}} />); + expect(home.contains( + <Breadcrumb cluster={TEST_CLUSTER} env={TEST_ENV} role={TEST_ROLE} />)).toBe(true); + }); +}); http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/utils/Common.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/utils/Common.js b/ui/src/main/js/utils/Common.js new file mode 100644 index 0000000..2e12e3c --- /dev/null +++ b/ui/src/main/js/utils/Common.js @@ -0,0 +1,3 @@ +export function isNully(value) { + return typeof value === 'undefined' || value === null; +} http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/js/utils/Job.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/utils/Job.js b/ui/src/main/js/utils/Job.js new file mode 100644 index 0000000..a0bd5c8 --- /dev/null +++ b/ui/src/main/js/utils/Job.js @@ -0,0 +1,3 @@ +export const TASK_COUNTS = [ + 'pendingTaskCount', 'activeTaskCount', 'finishedTaskCount', 'failedTaskCount' +]; http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/sass/app.scss ---------------------------------------------------------------------- diff --git a/ui/src/main/sass/app.scss b/ui/src/main/sass/app.scss index 0b3967d..d9673d0 100644 --- a/ui/src/main/sass/app.scss +++ b/ui/src/main/sass/app.scss @@ -10,4 +10,5 @@ @import 'components/tables'; /* Page Styles */ -@import 'components/home-page'; \ No newline at end of file +@import 'components/home-page'; +@import 'components/job-list-page'; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/ui/src/main/sass/components/_job-list-page.scss ---------------------------------------------------------------------- diff --git a/ui/src/main/sass/components/_job-list-page.scss b/ui/src/main/sass/components/_job-list-page.scss new file mode 100644 index 0000000..d31344d --- /dev/null +++ b/ui/src/main/sass/components/_job-list-page.scss @@ -0,0 +1,82 @@ +.job-task-stats { + display: block; + padding: 0; + margin: 0 10px; + white-space: nowrap; + + li { + display: inline-block; + margin-right: 12px; + font-size: 16px; + } + + .img-circle { + width: 10px; + height: 10px; + background-color: #CCC; + display: inline-block; + border-radius: 50%; + } +} + +.job-list-sort-control { + text-align: right; + margin-bottom: 10px; + li { + font-size: 14px; + } +} + +td.job-list-name { + width: 99%; + + .glyphicon { + font-size: 12px; + margin-left: 3px; + } +} + +td.job-list-stats { + text-align: right; +} + +td.job-list-type { + text-align: center; +} + +.pending-task { + background-color: $colors_warning !important; +} + +.active-task { + background-color: $colors_success !important; +} + +.failed-task { + background-color: $colors_error !important; +} + +.job-env { + display: inline-block; + font-size: 11px; + text-transform: uppercase; + overflow: hidden; + margin-right: 20px; +} + +.job-tier { + display: inline-block; + font-size: 11px; + text-transform: uppercase; + color: #777; + padding: 2px; +} + +.job-type { + font-size: 11px; + text-transform: uppercase; + display: inline-block; + width: 50px; + text-align: center; + color: $secondary_font_color; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/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 index b8a83b5..1d0553b 100644 --- a/ui/src/main/sass/components/_layout.scss +++ b/ui/src/main/sass/components/_layout.scss @@ -2,4 +2,31 @@ background-color: $content_box_color; margin: 10px 0; padding: 20px; +} + +.content-panel-group { + margin: 10px 0; + + .content-panel:last-child { + border-radius: 0px 0px 5px 5px; + } + + .content-panel-title { + padding: 15px 20px; + background-color: $content_box_color; + border-bottom: 1px solid #d7e2ec; + color: #555; + border-radius: 5px 5px 0px 0px; + font-weight: 700; + font-size: 18px; + } + + .content-panel { + padding: 20px; + background-color: $content_box_color; + } + + .content-panel + .content-panel { + margin-top: 1px; + } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/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 index 2ea60bc..3fc4b0a 100644 --- a/ui/src/main/sass/components/_tables.scss +++ b/ui/src/main/sass/components/_tables.scss @@ -57,6 +57,28 @@ } } +.psuedo-table { + width: 100%; + font-size: 16px; + border-top: 1px solid $grid_color; + + td { + padding: 5px; + } + + tr:hover { + background: #edf5fd; + } + + tr { + border-bottom: 1px solid $grid_color; + } + + a { + font-weight: 600; + } +} + .table-input-wrapper { border-radius: 4px; padding: 5px; http://git-wip-us.apache.org/repos/asf/aurora/blob/4e7cdc42/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 index 147cff6..a75bbe9 100644 --- a/ui/src/main/sass/modules/_colors.scss +++ b/ui/src/main/sass/modules/_colors.scss @@ -13,3 +13,23 @@ $success_secondary_color: #afe8b8; $error_color: #d63c39; $error_secondary_color: rgb(230, 101, 98); + +// Use to signify something is in a good state. +$colors_success_light: #afe8b8; +$colors_success: #74C080; +$colors_success_dark: #628a68; + +// Use to attract user's attention to negative behavior. +$colors_error_light: rgb(230, 101, 98); +$colors_error: #d63c39; +$colors_error_dark: #882422; + +// Use to highlight something that *may* need attention. +$colors_warning_light: #f5d96c; +$colors_warning: #f3c200; +$colors_warning_dark: #d4a900; + +// Use to highlight something important, but that is currently operating as expected. +$colors_highlight_light: #8ac5f9; +$colors_highlight: #5FA2DD; +$colors_highlight_dark: #3b71a0; \ No newline at end of file
