feat(my-joyent): fetch packages and implement filters

This commit is contained in:
Sara Vieira 2017-09-14 12:26:57 +01:00 committed by Sérgio Ramos
parent 3b427871cf
commit 884db125e0
46 changed files with 27671 additions and 1194 deletions

View File

@ -7,7 +7,8 @@
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"lint": "eslint . --fix", "lint": "eslint . --fix",
"lint-ci": "eslint . --format junit --output-file $CIRCLE_TEST_REPORTS/lint/cloudapi-gql.xml", "lint-ci":
"eslint . --format junit --output-file $CIRCLE_TEST_REPORTS/lint/cloudapi-gql.xml",
"test": "echo 0", "test": "echo 0",
"test-ci": "echo 0", "test-ci": "echo 0",
"start": "node src/index.js", "start": "node src/index.js",
@ -15,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"bunyan": "^1.8.12", "bunyan": "^1.8.12",
"cors": "^2.8.4",
"dotenv": "^4.0.0", "dotenv": "^4.0.0",
"express": "^4.15.4", "express": "^4.15.4",
"express-graphql": "^0.6.11", "express-graphql": "^0.6.11",

View File

@ -1,7 +1,11 @@
const express = require('express'); const express = require('express');
const cors = require('cors');
const app = express(); const app = express();
app.use(cors());
app.options('*', cors());
app.use('/graphql', require('./endpoint')); app.use('/graphql', require('./endpoint'));
const server = app.listen(4000, err => { const server = app.listen(4000, err => {

View File

View File

@ -6,7 +6,7 @@
"main": "build/", "main": "build/",
"scripts": { "scripts": {
"dev": "dev":
"REACT_APP_GQL_PORT=3000 PORT=3069 REACT_APP_GQL_PROTOCOL=http react-scripts start", "REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http react-scripts start",
"start": "PORT=3069 react-scripts start", "start": "PORT=3069 react-scripts start",
"build": "NODE_ENV=production react-scripts build", "build": "NODE_ENV=production react-scripts build",
"lint:css": "stylelint './src/**/*.js'", "lint:css": "stylelint './src/**/*.js'",
@ -18,7 +18,8 @@
"lint-ci": "redrun -p lint-ci:*", "lint-ci": "redrun -p lint-ci:*",
"test": "NODE_ENV=test ./test/run --env=jsdom", "test": "NODE_ENV=test ./test/run --env=jsdom",
"test-ci": "test-ci":
"echo 0 `# NODE_ENV=test JEST_JUNIT_OUTPUT=$CIRCLE_TEST_REPORTS/test/cp-frontend.xml ./test/run --env=jsdom --coverage --coverageDirectory=$CIRCLE_ARTIFACTS/cp-frontend --testResultsProcessor=$(node -e \"console.log(require.resolve('jest-junit'))\")`" "echo 0 `# NODE_ENV=test JEST_JUNIT_OUTPUT=$CIRCLE_TEST_REPORTS/test/cp-frontend.xml ./test/run --env=jsdom --coverage --coverageDirectory=$CIRCLE_ARTIFACTS/cp-frontend --testResultsProcessor=$(node -e \"console.log(require.resolve('jest-junit'))\")`",
"prepublish": "node scripts/postinstall"
}, },
"dependencies": { "dependencies": {
"apollo": "^0.2.2", "apollo": "^0.2.2",

View File

@ -0,0 +1,21 @@
/**
* @jest-environment jsdom
*/
import React from 'react';
import renderer from 'react-test-renderer';
import 'jest-styled-components';
import { Router, FiltersMock } from '@mocks/';
import Filters from '../filters';
it('renders <Filters /> without throwing', () => {
const tree = renderer
.create(
<Router>
<Filters filters={FiltersMock} />
</Router>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -0,0 +1,72 @@
import React from 'react';
import { Col } from 'react-styled-flexboxgrid';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { Slider, FormLabel } from 'joyent-ui-toolkit';
const FilterWrapper = styled.section`
display: flex;
width: 100%;
> div {
flex-grow: 1;
&:not(:last-child) {
margin-right: ${remcalc(36)};
}
}
`;
const Filters = ({
filters: { cpu, cost, ram, disk },
ramSliderChange,
cpuSliderChange,
diskSliderChange,
costSliderChange
}) => {
return (
<Col xs={12}>
<FormLabel>Choose a package</FormLabel>
<FilterWrapper>
<Slider
minValue={ram.min}
maxValue={ram.max}
step={0.256}
value={ram}
onChangeComplete={value => ramSliderChange(value)}
>
GB RAM
</Slider>
<Slider
minValue={cpu.min}
maxValue={cpu.max}
step={0.25}
value={cpu}
onChangeComplete={value => cpuSliderChange(value)}
>
vCPUs
</Slider>
<Slider
minValue={disk.min}
maxValue={disk.max}
step={0.01}
value={disk}
onChangeComplete={value => diskSliderChange(value)}
>
TB Disk
</Slider>
<Slider
minValue={cost.min}
maxValue={cost.max}
step={0.02}
value={cost}
onChangeComplete={value => costSliderChange(value)}
>
$/hr
</Slider>
</FilterWrapper>
</Col>
);
};
export default Filters;

View File

@ -0,0 +1 @@
export { default as Filters } from './filters';

View File

@ -3,19 +3,19 @@
*/ */
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; import ShallowRenderer from 'react-test-renderer/shallow';
import 'jest-styled-components'; import 'jest-styled-components';
import { Router } from '@mocks/'; import { Router, FiltersMock } from '@mocks/';
import Home from '../home'; import Home from '../home';
it('renders <Home /> without throwing', () => { it('renders <Home /> without throwing', () => {
const tree = renderer const renderer = new ShallowRenderer();
.create( renderer.render(
<Router> <Router>
<Home /> <Home filters={FiltersMock} />
</Router> </Router>
) );
.toJSON(); const tree = renderer.getRenderOutput();
expect(tree).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}); });

View File

@ -1,6 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Row } from 'react-styled-flexboxgrid'; import { Row } from 'react-styled-flexboxgrid';
import { SectionNav } from '@components/navigation'; import { SectionNav } from '@components/navigation';
import { Filters } from '@components/filters';
import PackagesHOC from '@containers/packages';
import { Message, Breadcrumb, BreadcrumbItem, Anchor } from 'joyent-ui-toolkit'; import { Message, Breadcrumb, BreadcrumbItem, Anchor } from 'joyent-ui-toolkit';
class Home extends Component { class Home extends Component {
@ -12,6 +14,7 @@ class Home extends Component {
}; };
this.closeMessage = this.closeMessage.bind(this); this.closeMessage = this.closeMessage.bind(this);
this.changeValue = this.changeValue.bind(this);
} }
closeMessage() { closeMessage() {
@ -20,8 +23,18 @@ class Home extends Component {
}); });
} }
changeValue(key, value) {
const filters = this.props.filters;
this.props.onFilterChange({
...filters,
[key]: value
});
}
render() { render() {
const _msg = this.state.showMessage ? ( const { showMessage } = this.state;
const { filters } = this.props;
const _msg = showMessage ? (
<Message <Message
title="Choosing deployement data center" title="Choosing deployement data center"
onCloseClick={this.closeMessage} onCloseClick={this.closeMessage}
@ -33,6 +46,7 @@ class Home extends Component {
</p> </p>
</Message> </Message>
) : null; ) : null;
return ( return (
<main> <main>
<SectionNav /> <SectionNav />
@ -41,6 +55,18 @@ class Home extends Component {
<BreadcrumbItem>Create Instance</BreadcrumbItem> <BreadcrumbItem>Create Instance</BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
<Row>{_msg}</Row> <Row>{_msg}</Row>
<Row>
<Filters
filters={filters}
ramSliderChange={value => this.changeValue('ram', value)}
cpuSliderChange={value => this.changeValue('cpu', value)}
diskSliderChange={value => this.changeValue('disk', value)}
costSliderChange={value => this.changeValue('cost', value)}
/>
</Row>
<Row>
<PackagesHOC />
</Row>
</main> </main>
); );
} }

View File

@ -0,0 +1,21 @@
/**
* @jest-environment jsdom
*/
import React from 'react';
import renderer from 'react-test-renderer';
import 'jest-styled-components';
import { Router, PackagesMock, FiltersMock } from '@mocks/';
import { Packages } from '../';
it('renders <Packages /> without throwing', () => {
const tree = renderer
.create(
<Router>
<Packages packages={PackagesMock} filters={FiltersMock} />
</Router>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -0,0 +1 @@
export { default as Packages } from './list';

View File

@ -0,0 +1,80 @@
import React, { Component } from 'react';
import remcalc from 'remcalc';
import styled from 'styled-components';
import {
Card,
CardSubTitle,
CardTitle,
CardView,
CardFooter,
CardMeta
} from 'joyent-ui-toolkit';
const ListStyled = styled.ul`
display: flex;
width: 100%;
list-style: none;
padding: 0;
flex-wrap: wrap;
justify-content: space-between;
margin-top: ${remcalc(36)};
`;
class Packages extends Component {
constructor(props) {
super(props);
const { filters: { ram, cpu, cost, disk }, packages } = props;
this.state = {
ram,
cpu,
cost,
disk,
packages
};
}
componentWillReceiveProps(nextProps) {
const { filters: { ram, cpu, cost, disk }, packages } = nextProps;
this.setState({
ram,
cpu,
cost,
disk,
packages: packages
.filter(pack => pack.memory >= ram.min && pack.memory <= ram.max)
.filter(
pack => pack.disk / 1000 >= disk.min && pack.disk / 1000 <= disk.max
)
.filter(pack => pack.vcpus >= cpu.min && pack.vcpus <= cpu.max)
.filter(pack => pack.price >= cost.min && pack.price <= cost.max)
});
}
_packages() {
return (
<ListStyled>
{this.state.packages.map(pack => (
<li>
<Card transparent>
<CardView>
<CardMeta>
<CardTitle>${pack.price} per hour</CardTitle>
<CardSubTitle>{pack.memory} GB RAM</CardSubTitle>
<CardSubTitle>{pack.vcpus} vCPUs</CardSubTitle>
<CardSubTitle>{pack.disk / 100} TB disk</CardSubTitle>
<CardSubTitle>SSD</CardSubTitle>
<CardFooter>{pack.group}</CardFooter>
</CardMeta>
</CardView>
</Card>
</li>
))}
</ListStyled>
);
}
render() {
return this._packages();
}
}
export default Packages;

View File

@ -7,13 +7,15 @@ import renderer from 'react-test-renderer';
import 'jest-styled-components'; import 'jest-styled-components';
import HomeHOC from '../'; import HomeHOC from '../';
import { Router } from '@mocks/'; import { Router, Store } from '@mocks/';
it('renders <HomeHOC /> without throwing', () => { it('renders <HomeHOC /> without throwing', () => {
const tree = renderer const tree = renderer
.create( .create(
<Router> <Router>
<Store>
<HomeHOC /> <HomeHOC />
</Store>
</Router> </Router>
) )
.toJSON(); .toJSON();

View File

@ -1,11 +1,27 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import changeFilters from '../../state/actions';
import { LayoutContainer } from '@components/layout'; import { LayoutContainer } from '@components/layout';
import { Home } from '@components/home'; import { Home } from '@components/home';
const HomeHOC = () => ( const HomeHOC = ({ filters, onFilterChange }) => (
<LayoutContainer> <LayoutContainer>
<Home /> <Home filters={filters} onFilterChange={onFilterChange} />
</LayoutContainer> </LayoutContainer>
); );
export default HomeHOC; const mapStateToProps = state => {
return {
filters: state.filters
};
};
const mapDispatchToProps = dispatch => {
return {
onFilterChange: filters => {
dispatch(changeFilters(filters));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(HomeHOC);

View File

@ -0,0 +1,23 @@
/**
* @jest-environment jsdom
*/
import React from 'react';
import renderer from 'react-test-renderer';
import 'jest-styled-components';
import PackagesHOC from '../';
import { Router, Store } from '@mocks/';
it('renders <PackagesHOC /> without throwing', () => {
const tree = renderer
.create(
<Router>
<Store>
<PackagesHOC />
</Store>
</Router>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import { connect } from 'react-redux';
// import { graphql } from 'react-apollo';
import { Packages } from '@components/packages';
// import packagesQuery from '@graphql/packages.gql';
import data from '../../data/packages';
const PackagesHOC = ({ packages, filters }) => (
<Packages packages={data} filters={filters} />
);
// const PackagesHOCWithData = graphql(packagesQuery, {
// props: ({ data: { packages = [], loading = true } }) => ({
// packages,
// loading
// })
// })(PackagesHOC);
const mapStateToProps = state => {
return {
filters: state.filters
};
};
export default connect(mapStateToProps)(PackagesHOC);

View File

@ -0,0 +1,146 @@
[
{
"name": "High CPU 0.25",
"vcpus": 0.25,
"memory": 0.256,
"disk": 10,
"group": "Compute Optimized",
"price": "0.016"
},
{
"name": "High CPU 0.75",
"vcpus": 0.5,
"memory": 0.768,
"disk": 25,
"group": "Compute Optimized",
"price": "0.033"
},
{
"name": "High CPU 1.75",
"vcpus": 1,
"memory": 1.8,
"disk": 50,
"group": "Compute Optimized",
"price": "0.066"
},
{
"name": "High CPU 3.75",
"vcpus": 2,
"memory": 3.8,
"disk": 100,
"group": "Compute Optimized",
"price": "0.131"
},
{
"name": "High CPU 7.75",
"vcpus": 4,
"memory": 7.8,
"disk": 200,
"group": "Compute Optimized",
"price": "0.263"
},
{
"name": "High CPU 15.75",
"vcpus": 8,
"memory": 15.8,
"disk": 400,
"group": "Compute Optimized",
"price": "0.525"
},
{
"name": "General 3.75",
"vcpus": 1,
"memory": 3.8,
"disk": 50,
"group": "General Purpose",
"price": "0.084"
},
{
"name": "General 7.75",
"vcpus": 2,
"memory": 7.8,
"disk": 100,
"group": "General Purpose",
"price": "0.166"
},
{
"name": "General 15.75",
"vcpus": 4,
"memory": 15.8,
"disk": 200,
"group": "General Purpose",
"price": "0.333"
},
{
"name": "General 31.75",
"vcpus": 8,
"memory": 31.8,
"disk": 400,
"group": "General Purpose",
"price": "0.665"
},
{
"name": "High RAM 15.75",
"vcpus": 2,
"memory": 15.8,
"disk": 100,
"group": "Memory Optimized",
"price": "0.259"
},
{
"name": "High RAM 31.75",
"vcpus": 4,
"memory": 31.8,
"disk": 200,
"group": "Memory Optimized",
"price": "0.52"
},
{
"name": "High RAM 63.75",
"vcpus": 8,
"memory": 63.8,
"disk": 400,
"group": "Memory Optimized",
"price": "1.039"
},
{
"name": "Fast Disk 31.75",
"vcpus": 4,
"memory": 31.8,
"disk": 800,
"group": "Storage Optimized",
"price": "1.066"
},
{
"name": "Fast Disk 63.75",
"vcpus": 8,
"memory": 63.8,
"disk": 1600,
"group": "Storage Optimized",
"price": "2.31"
},
{
"name": "Big Disk 15.75",
"vcpus": 2,
"memory": 15.8,
"disk": 1200,
"group": "Storage Optimized",
"price": "0.413"
},
{
"name": "Big Disk 31.75",
"vcpus": 4,
"memory": 31.8,
"disk": 2400,
"group": "Storage Optimized",
"price": "0.825"
},
{
"name": "Big Disk 63.75",
"vcpus": 8,
"memory": 63.8,
"disk": 4900,
"group": "Storage Optimized",
"price": "1.75"
}
]

View File

@ -0,0 +1,5 @@
query Portal {
datacenters {
name
}
}

View File

@ -0,0 +1,10 @@
query Portal {
packages {
name
vcpus
memory
disk
version
group
}
}

View File

@ -0,0 +1,8 @@
const changeFilters = filters => {
return {
type: 'CHANGE_FILTERS',
filters
};
};
export default changeFilters;

View File

@ -0,0 +1,13 @@
const filterReducer = (state = [], action) => {
switch (action.type) {
case 'CHANGE_FILTERS':
return {
...state,
...action.filters
};
default:
return state;
}
};
export default filterReducer;

View File

@ -1,3 +1,10 @@
const state = {}; const state = {
filters: {
cpu: { min: 0.25, max: 8 },
cost: { min: 0.016, max: 2.131 },
ram: { min: 0.256, max: 63.8 },
disk: { min: 0.01, max: 4.9 }
}
};
export default state; export default state;

View File

@ -1,6 +1,7 @@
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { reducer as formReducer } from 'redux-form'; import { reducer as formReducer } from 'redux-form';
import { ApolloClient, createNetworkInterface } from 'react-apollo'; import { ApolloClient, createNetworkInterface } from 'react-apollo';
import filterReducer from './filterReducer';
import state from './state'; import state from './state';
const GLOBAL = const GLOBAL =
@ -37,14 +38,15 @@ export const client = new ApolloClient({
return `${o.__typename}:${id}`; return `${o.__typename}:${id}`;
}, },
networkInterface: createNetworkInterface({ networkInterface: createNetworkInterface({
uri: `${GQL_PROTOCOL}://${GQL_HOSTNAME}:${GQL_PORT}/api/graphql` uri: `${GQL_PROTOCOL}://${GQL_HOSTNAME}:${GQL_PORT}/graphql`
}) })
}); });
export const store = createStore( export const store = createStore(
combineReducers({ combineReducers({
apollo: client.reducer(), apollo: client.reducer(),
form: formReducer form: formReducer,
filters: filterReducer
}), }),
state, // Initial state state, // Initial state
compose( compose(

View File

@ -0,0 +1,18 @@
export default {
cpu: {
min: 0.25,
max: 3.25
},
cost: {
min: 0.016,
max: 0.525
},
ram: {
min: 0.256,
max: 50.688
},
disk: {
min: 0.01,
max: 107.26
}
};

View File

@ -1,3 +1,5 @@
export { default as Router } from './router'; export { default as Router } from './router';
export { default as Store } from './store'; export { default as Store } from './store';
export { default as PackagesMock } from './packages';
export { default as FiltersMock } from './filters';
export { default as Theme } from './theme'; export { default as Theme } from './theme';

View File

@ -0,0 +1,50 @@
export default [
{
name: 'High CPU 0.25',
vcpus: 0.25,
memory: 0.256,
disk: 10,
group: 'Compute Optimized',
price: '0.016'
},
{
name: 'High CPU 0.75',
vcpus: 0.5,
memory: 0.768,
disk: 25,
group: 'Compute Optimized',
price: '0.033'
},
{
name: 'High CPU 1.75',
vcpus: 1,
memory: 1.8,
disk: 50,
group: 'Compute Optimized',
price: '0.066'
},
{
name: 'High CPU 3.75',
vcpus: 2,
memory: 3.8,
disk: 100,
group: 'Compute Optimized',
price: '0.131'
},
{
name: 'High CPU 7.75',
vcpus: 4,
memory: 7.8,
disk: 200,
group: 'Compute Optimized',
price: '0.263'
},
{
name: 'High CPU 15.75',
vcpus: 8,
memory: 15.8,
disk: 400,
group: 'Compute Optimized',
price: '0.525'
}
];

View File

@ -1,6 +1,7 @@
{ {
"extends": "joyent-portal", "extends": "joyent-portal",
"rules": { "rules": {
"new-cap": 0 "new-cap": 0,
"jsx-a11y/href-no-hash": 0
} }
} }

View File

View File

@ -10,16 +10,22 @@
"lint:css": "echo 0", "lint:css": "echo 0",
"lint-ci:css": "echo 0", "lint-ci:css": "echo 0",
"lint:js": "eslint . --fix", "lint:js": "eslint . --fix",
"lint-ci:js": "eslint . --format junit --output-file $CIRCLE_TEST_REPORTS/lint/ui-toolkit.xml", "lint-ci:js":
"eslint . --format junit --output-file $CIRCLE_TEST_REPORTS/lint/ui-toolkit.xml",
"lint": "redrun -s lint:*", "lint": "redrun -s lint:*",
"lint-ci": "redrun -s lint-ci:*", "lint-ci": "redrun -s lint-ci:*",
"test": "echo 0", "test": "echo 0",
"test-ci": "echo 0", "test-ci": "echo 0",
"copy-fonts": "rm -rf dist; mkdir -p dist/es/typography; mkdir -p dist/umd/typography; cp -r src/typography/libre-franklin dist/es/typography; cp -r src/typography/libre-franklin dist/umd/typography", "copy-fonts":
"compile-watch:es": "NODE_ENV=development babel src --out-dir dist/es --source-maps inline --watch", "rm -rf dist; mkdir -p dist/es/typography; mkdir -p dist/umd/typography; cp -r src/typography/libre-franklin dist/es/typography; cp -r src/typography/libre-franklin dist/umd/typography",
"compile:es": "NODE_ENV=development babel src --out-dir dist/es --source-maps inline", "compile-watch:es":
"compile:umd": "cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline", "NODE_ENV=development babel src --out-dir dist/es --source-maps inline --watch",
"compile-watch:umd": "cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline --watch", "compile:es":
"NODE_ENV=development babel src --out-dir dist/es --source-maps inline",
"compile:umd":
"cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline",
"compile-watch:umd":
"cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline --watch",
"compile": "redrun -p compile:*", "compile": "redrun -p compile:*",
"watch": "redrun copy-fonts && redrun -p compile-watch:*", "watch": "redrun copy-fonts && redrun -p compile-watch:*",
"styleguide:build": "cross-env NODE_ENV=production styleguidist build", "styleguide:build": "cross-env NODE_ENV=production styleguidist build",
@ -47,6 +53,7 @@
"polished": "^1.6.1", "polished": "^1.6.1",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react-broadcast": "^0.1.2", "react-broadcast": "^0.1.2",
"react-input-range": "^1.2.1",
"react-styled-flexboxgrid": "^2.0.3", "react-styled-flexboxgrid": "^2.0.3",
"redrun": "^5.9.16", "redrun": "^5.9.16",
"reduce-css-calc": "^2.0.5", "reduce-css-calc": "^2.0.5",
@ -81,7 +88,8 @@
"stylelint": "^8.0.0", "stylelint": "^8.0.0",
"stylelint-config-primer": "^2.0.1", "stylelint-config-primer": "^2.0.1",
"stylelint-config-standard": "^17.0.0", "stylelint-config-standard": "^17.0.0",
"stylelint-processor-styled-components": "styled-components/stylelint-processor-styled-components#2a33b5f", "stylelint-processor-styled-components":
"styled-components/stylelint-processor-styled-components#2a33b5f",
"svgo": "^0.7.2", "svgo": "^0.7.2",
"tinycolor2": "^1.4.1", "tinycolor2": "^1.4.1",
"title-case": "^2.1.1", "title-case": "^2.1.1",

View File

@ -35,6 +35,16 @@ const StyledCard = Row.extend`
box-shadow: none; box-shadow: none;
`}; `};
${is('transparent')`
border-radius: ${remcalc(4)}
background:
border: 1px solid ${props => props.theme.grey};
background: ${props => props.theme.background};
box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.05);
min-height: ${remcalc(185)};
min-width: 200px;
`};
${is('stacked')` ${is('stacked')`
${paperEffect} ${paperEffect}
`}; `};
@ -85,7 +95,8 @@ Card.propTypes = {
collapsed: PropTypes.bool, collapsed: PropTypes.bool,
headed: PropTypes.bool, headed: PropTypes.bool,
flat: PropTypes.bool, flat: PropTypes.bool,
stacked: PropTypes.bool stacked: PropTypes.bool,
transparent: PropTypes.bool
}; };
export default Baseline(Card); export default Baseline(Card);

View File

@ -0,0 +1,44 @@
import { Subscriber } from 'react-broadcast';
import styled from 'styled-components';
import Baseline from '../baseline';
import typography from '../typography';
import remcalc from 'remcalc';
import PropTypes from 'prop-types';
import Title from './title';
import React from 'react';
const StyledTitle = Title.extend`
${typography.fontFamily};
${typography.normal};
flex-grow: 1;
flex-basis: ${remcalc(90)};
`;
const Span = styled.span`
display: inline-block;
flex-direction: column;
${typography.fontFamily};
${typography.normal};
font-size: ${remcalc(13)};
font-weight: 500;
text-transform: uppercase;
color: rgba(73, 73, 73, 0.5);
`;
const Footer = ({ children }) => {
const render = () => (
<StyledTitle name="card-footer">
<Span>{children}</Span>
</StyledTitle>
);
return <Subscriber channel="card">{render}</Subscriber>;
};
Footer.propTypes = {
children: PropTypes.node
};
export default Baseline(Footer);

View File

@ -9,3 +9,4 @@ export { default as CardSubTitle } from './subtitle.js';
export { default as CardTitle } from './title.js'; export { default as CardTitle } from './title.js';
export { default as CardView } from './view.js'; export { default as CardView } from './view.js';
export { default as CardInfo } from './info.js'; export { default as CardInfo } from './info.js';
export { default as CardFooter } from './footer.js';

View File

@ -52,6 +52,36 @@ const {
</Card> </Card>
``` ```
#### `transparent`
```
const {
CardDescription,
CardHeader,
CardMeta,
CardOptions,
CardOutlet,
CardSubTitle,
CardTitle,
CardView,
CardGroupView,
CardFooter
} = require('./');
<Card transparent>
<CardView>
<CardMeta>
<CardTitle>$0.016 per hour</CardTitle>
<CardSubTitle>0.256 GB RAM</CardSubTitle>
<CardSubTitle>0.25 vCPUs</CardSubTitle>
<CardSubTitle>0.01 TB disk</CardSubTitle>
<CardSubTitle>SSD</CardSubTitle>
<CardFooter>Compute Optimise</CardFooter>
</CardMeta>
</CardView>
</Card>
```
#### `headed` #### `headed`
``` ```

View File

@ -27,6 +27,7 @@ export {
export { Dropdown } from './dropdown'; export { Dropdown } from './dropdown';
export { default as StatusLoader } from './status-loader'; export { default as StatusLoader } from './status-loader';
export { default as Message } from './message'; export { default as Message } from './message';
export { default as Slider } from './slider';
export { export {
default as Progressbar, default as Progressbar,
@ -69,7 +70,8 @@ export {
CardSubTitle, CardSubTitle,
CardTitle, CardTitle,
CardView, CardView,
CardInfo CardInfo,
CardFooter
} from './card'; } from './card';
export { export {

View File

@ -72,7 +72,7 @@ const Message = ({ title, message, onCloseClick, children, ...type }) => {
Message.propTypes = { Message.propTypes = {
title: PropTypes.string, title: PropTypes.string,
message: PropTypes.string.isRequired, message: PropTypes.string,
onCloseClick: PropTypes.func, onCloseClick: PropTypes.func,
error: PropTypes.boolean, error: PropTypes.boolean,
warning: PropTypes.boolean, warning: PropTypes.boolean,

View File

@ -0,0 +1,25 @@
### Double Range Slider
```
<Slider
minValue={0.25}
maxValue={8}
step={0.25}
value={{ min: 0.25, max: 8 }}
onChangeComplete={value => console.log(value)}
onChange={value => console.log(value)}
>vCPUs</Slider>
```
### Normal Slider
```
<Slider
minValue={10}
maxValue={100}
step={5}
value={0}
onChangeComplete={value => console.log(value)}
onChange={value => console.log(value)}
>Price</Slider>
```

View File

@ -0,0 +1,147 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import InputRange from 'react-input-range';
import remcalc from 'remcalc';
import theme from '../theme';
import FormLabel from '../form/label';
import {
sliderStyles,
disabledStyles,
trackStyles,
activeStyles,
rangeStyles
} from './inputStyles';
const SliderStyled = styled.div`
.input-range {
${rangeStyles};
}
.slider {
${sliderStyles};
}
.disabled {
${disabledStyles};
}
.min,
.max {
display: none;
}
.value {
top: ${remcalc(8)};
position: absolute;
.label-container {
font-weight: 600;
font-size: ${remcalc(10)};
color: ${theme.secondary};
left: -50%;
position: relative;
}
}
.track {
${trackStyles};
}
.active-track {
${activeStyles};
}
`;
const Label = styled(FormLabel)`
margin-bottom: ${remcalc(10)};
margin-top: ${remcalc(12)};
`;
class Slider extends Component {
constructor(props) {
super(props);
this.state = {
minValue: this.props.minValue,
maxValue: this.props.maxValue,
value: this.props.value
};
this.changeValue = this.changeValue.bind(this);
}
changeValue(value) {
this.setState({ value }, () => this.props.onChange(value));
}
render() {
const { minValue, maxValue, value } = this.state;
const { children, ...rest } = this.props;
return (
<SliderStyled>
<Label>{children}</Label>
<InputRange
{...rest}
minValue={minValue}
maxValue={maxValue}
value={value}
onChange={value => this.changeValue(value)}
/>
</SliderStyled>
);
}
}
Slider.propTypes = {
minValue: PropTypes.number,
maxValue: PropTypes.number,
step: PropTypes.number,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.shape()]),
onChangeComplete: PropTypes.func,
onChange: PropTypes.func,
formatLabel: PropTypes.func,
ariaLabelledby: PropTypes.string,
ariaControls: PropTypes.string,
classNames: PropTypes.shape({
activeTrack: PropTypes.string,
disabledInputRange: PropTypes.string,
inputRange: PropTypes.string,
labelContainer: PropTypes.string,
maxLabel: PropTypes.string,
minLabel: PropTypes.string,
slider: PropTypes.string,
sliderContainer: PropTypes.string,
track: PropTypes.string,
valueLabel: PropTypes.string
}),
disabled: PropTypes.bool,
draggableTrack: PropTypes.bool,
onChangeStart: PropTypes.func,
children: PropTypes.node
};
Slider.defaultProps = {
onChangeComplete: () => {},
onChange: () => {},
formatLabel: value =>
(value.toString().split('.')[1] || []).length > 3
? Math.round(value).toFixed(3)
: value,
onChangeStart: () => {},
step: 1,
classNames: {
activeTrack: 'active-track',
disabledInputRange: 'disabled-range',
inputRange: 'input-range',
labelContainer: 'label-container',
maxLabel: 'max',
minLabel: 'min',
sliderContainer: 'slider-container',
track: 'track',
valueLabel: 'value',
slider: 'slider'
}
};
export default Slider;

View File

@ -0,0 +1,61 @@
import { css } from 'styled-components';
import remcalc from 'remcalc';
import theme from '../theme';
export const sliderStyles = css`
appearance: none;
background: ${theme.white};
border: 2px solid ${theme.greyLight};
border-radius: 50%;
cursor: pointer;
display: block;
height: ${remcalc(14)};
width: ${remcalc(14)};
transform: translateY(-50%) translateX(-50%);
outline: none;
position: absolute;
top: 50%;
margin-top: ${remcalc(2)};
&::active {
transform: scale(1.3);
}
&::focus {
box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2);
}
`;
export const disabledStyles = css`
.track {
background: ${theme.grey};
}
.slider {
background: ${theme.greyDark};
border: 1px solid ${theme.greyDark};
box-shadow: none;
transform: none;
}
`;
export const trackStyles = css`
background: ${theme.grey};
cursor: pointer;
display: block;
height: ${remcalc(4)};
position: relative;
`;
export const activeStyles = css`
background: ${theme.blue};
height: 100%;
position: absolute;
`;
export const rangeStyles = css`
position: relative;
width: calc(100% - 18px);
margin: auto;
min-height: ${remcalc(10)};
`;

View File

@ -50,6 +50,8 @@ export const base = {
...tertiary, ...tertiary,
text: '#494949', // used text: '#494949', // used
grey: '#D8D8D8', // used grey: '#D8D8D8', // used
greyDark: '#CCC',
greyLight: '#bdbdbd', // used
disabled: '#FAFAFA', // used disabled: '#FAFAFA', // used
background: '#FAFAFA', // used background: '#FAFAFA', // used
green: '#00AF66', // used green: '#00AF66', // used
@ -57,7 +59,8 @@ export const base = {
orange: '#E38200', // used orange: '#E38200', // used
orangeDark: '#CB7400', // not used - BORDER orangeDark: '#CB7400', // not used - BORDER
red: '#DA4B42', // used red: '#DA4B42', // used
redDark: '#CD251B' // not used - BORDER redDark: '#CD251B', // not used - BORDER
blue: '#364ACD'
}; };
/** ********************************** HEADER ********************************** */ /** ********************************** HEADER ********************************** */

View File

@ -70,7 +70,8 @@ module.exports = snapguidist({
'src/tooltip/tooltip.js', 'src/tooltip/tooltip.js',
'src/close-button/index.js', 'src/close-button/index.js',
'src/icon-button/index.js', 'src/icon-button/index.js',
'src/message/index.js' 'src/message/index.js',
'src/slider/index.js'
] ]
}, },
{ {

View File

@ -684,6 +684,10 @@ auto-bind@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-1.1.0.tgz#93b864dc7ee01a326281775d5c75ca0a751e5961" resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-1.1.0.tgz#93b864dc7ee01a326281775d5c75ca0a751e5961"
autobind-decorator@^1.3.4:
version "1.4.3"
resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-1.4.3.tgz#4c96ffa77b10622ede24f110f5dbbf56691417d1"
automated-readability@^1.0.0: automated-readability@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/automated-readability/-/automated-readability-1.0.1.tgz#5cf9111efcead6fe13b8c954d425b0068d59949d" resolved "https://registry.yarnpkg.com/automated-readability/-/automated-readability-1.0.1.tgz#5cf9111efcead6fe13b8c954d425b0068d59949d"
@ -2960,6 +2964,13 @@ cors@2.8.1:
dependencies: dependencies:
vary "^1" vary "^1"
cors@^2.8.4:
version "2.8.4"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.4.tgz#2bd381f2eb201020105cd50ea59da63090694686"
dependencies:
object-assign "^4"
vary "^1"
cosmiconfig@^2.1.0, cosmiconfig@^2.1.1, cosmiconfig@^2.1.3: cosmiconfig@^2.1.0, cosmiconfig@^2.1.1, cosmiconfig@^2.1.3:
version "2.2.2" version "2.2.2"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892"
@ -8565,7 +8576,7 @@ oauth-sign@~0.8.1:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: object-assign@4.1.1, object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -9934,6 +9945,13 @@ react-icons@^2.2.5:
dependencies: dependencies:
react-icon-base "2.0.7" react-icon-base "2.0.7"
react-input-range@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-input-range/-/react-input-range-1.2.1.tgz#10ff5fc1ec6ab9d95e15cddebe6f6879db2c3386"
dependencies:
autobind-decorator "^1.3.4"
prop-types "^15.5.8"
react-redux@^5.0.6: react-redux@^5.0.6:
version "5.0.6" version "5.0.6"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946"