feat: tags and metadata new ui and mutations

This commit is contained in:
Sérgio Ramos 2017-11-09 11:27:32 +00:00 committed by Sérgio Ramos
parent 34db52d541
commit c47333ed1b
52 changed files with 6657 additions and 1317 deletions

View File

@ -19,6 +19,9 @@
"test-ci": "lerna run test-ci", "test-ci": "lerna run test-ci",
"test": "lerna run test", "test": "lerna run test",
"clean": "lerna clean --yes", "clean": "lerna clean --yes",
"dev": "redrun -p dev:*",
"dev:ui": "lerna run watch --scope joyent-ui-toolkit --stream",
"dev:mjb": "lerna run dev --scope my-joy-beta --stream",
"commitmsg": "commitlint -e", "commitmsg": "commitlint -e",
"precommit": "cross-env CI=1 redrun -s lint-staged format-staged", "precommit": "cross-env CI=1 redrun -s lint-staged format-staged",
"postinstall": "lerna run prepublish", "postinstall": "lerna run prepublish",
@ -69,12 +72,11 @@
"lodash.keys": "4.2.0", "lodash.keys": "4.2.0",
"lodash.defaults": "4.2.0", "lodash.defaults": "4.2.0",
"lodash.assign": "4.2.0", "lodash.assign": "4.2.0",
"graphql": "0.11.7",
"isarray": "'2.0.2", "isarray": "'2.0.2",
"moment": "2.19.1", "moment": "2.19.1",
"codemirror": "5.30.0", "codemirror": "5.30.0",
"react": "16.0.0", "react": "16.1.1",
"react-dom": "16.0.0", "react-dom": "16.1.1",
"react-modal": "2.4.1", "react-modal": "2.4.1",
"hoist-non-react-statics": "2.3.1" "hoist-non-react-statics": "2.3.1"
} }

View File

@ -6,48 +6,51 @@
"repository": "github:yldio/joyent-portal", "repository": "github:yldio/joyent-portal",
"main": "build/", "main": "build/",
"scripts": { "scripts": {
"dev": "REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start", "dev":
"REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start",
"start": "PORT=3069 joyent-react-scripts start", "start": "PORT=3069 joyent-react-scripts start",
"build": "NODE_ENV=production joyent-react-scripts build", "build": "NODE_ENV=production joyent-react-scripts build",
"lint-ci": "lint-ci":
"eslint . --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`", "eslint . --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
"lint": "lint":
"eslint . --fix --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`", "eslint . --fix --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
"test-ci": "echo 0 `# NODE_ENV=test ./test/run --env=jsdom --coverage`", "test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
"test": "NODE_ENV=test ./test/run --env=jsdom", "test-ci": "redrun test",
"prepublish": "echo 0" "prepublish": "echo 0"
}, },
"dependencies": { "dependencies": {
"@manaflair/redux-batch": "^0.1.0", "@manaflair/redux-batch": "^0.1.0",
"apollo": "^0.2.2", "apollo": "^0.2.2",
"declarative-redux-form": "^1.0.3",
"joyent-ui-toolkit": "^2.0.1", "joyent-ui-toolkit": "^2.0.1",
"lodash.find": "^4.6.0", "lodash.find": "^4.6.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",
"lodash.sortby": "^4.7.0", "lodash.sortby": "^4.7.0",
"lunr": "^2.1.4", "lunr": "^2.1.4",
"moment": "^2.19.1", "moment": "^2.19.2",
"normalized-styled-components": "^1.0.17", "normalized-styled-components": "^1.0.17",
"param-case": "^2.1.1", "param-case": "^2.1.1",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.0.0", "react": "^16.1.1",
"react-apollo": "^1.4.16", "react-apollo": "^1.4.16",
"react-dom": "^16.0.0", "react-dom": "^16.1.1",
"react-json-view": "^1.13.1", "react-json-view": "^1.13.3",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-redux-values": "^1.0.2",
"react-router": "^4.2.0", "react-router": "^4.2.0",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-actions": "^2.2.1", "redux-actions": "^2.2.1",
"redux-form": "^7.1.1", "redux-form": "^7.1.2",
"remcalc": "^1.0.9", "remcalc": "^1.0.9",
"styled-components": "^2.2.2", "styled-components": "^2.2.3",
"title-case": "^2.1.1" "title-case": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-inline-react-svg": "^0.4.0", "babel-plugin-inline-react-svg": "^0.4.0",
"babel-preset-joyent-portal": "^3.3.3", "babel-preset-joyent-portal": "^3.3.3",
"eslint": "^4.9.0", "eslint": "^4.11.0",
"eslint-config-joyent-portal": "^3.2.0", "eslint-config-joyent-portal": "^3.2.0",
"jest": "^21.2.1", "jest": "^21.2.1",
"jest-alias-preprocessor": "^1.1.1", "jest-alias-preprocessor": "^1.1.1",
@ -58,10 +61,10 @@
"jest-snapshot": "^21.2.1", "jest-snapshot": "^21.2.1",
"jest-styled-components": "^4.9.0", "jest-styled-components": "^4.9.0",
"jest-transform-graphql": "^2.1.0", "jest-transform-graphql": "^2.1.0",
"joyent-react-scripts": "^2.6.0", "joyent-react-scripts": "^3.1.0",
"react-test-renderer": "^16.0.0", "react-test-renderer": "^16.1.1",
"redrun": "^5.9.18", "redrun": "^5.10.0",
"serve": "^6.3.1", "serve": "^6.4.1",
"stylelint": "^8.2.0", "stylelint": "^8.2.0",
"stylelint-config-joyent-portal": "^2.0.1" "stylelint-config-joyent-portal": "^2.0.1"
} }

View File

@ -0,0 +1,86 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { reduxForm } from 'redux-form';
import Store from '@mocks/store';
import 'jest-styled-components';
import KeyValue from '../key-value';
const KeyValueForm = reduxForm()(KeyValue);
it('renders <KeyValue /> without throwing', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue textarea /> with textareas', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm textarea />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue expanded /> expanded', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm expanded />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue submitting /> with loader', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm submitting />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue first /> without top margin', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm first />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue last /> with bottom border', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm last />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue label /> with proper label', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm label="Label" />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -1,106 +1,192 @@
import React from 'react'; import React, { PureComponent } from 'react';
import { Row, Col } from 'react-styled-flexboxgrid'; import { Row, Col } from 'react-styled-flexboxgrid';
import Value from 'react-redux-values';
import { Field } from 'redux-form'; import { Field } from 'redux-form';
import styled from 'styled-components';
import remcalc from 'remcalc';
import titleCase from 'title-case';
import { import {
Message,
MessageDescription,
MessageTitle,
Card,
CardHeader,
CardHeaderMeta,
CardHeaderBox,
CardOutlet,
ChevronIcon,
FormGroup, FormGroup,
Label,
Input, Input,
FormMeta,
Button, Button,
BinIcon, Textarea,
QueryBreakpoints, Editor,
Divider, Divider,
Editor P
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
const { SmallOnly } = QueryBreakpoints; const CollapsedKeyValue = styled.span`
word-break: break-all;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
`;
const TextareaKeyValue = ({ class ValueTextareaField extends PureComponent {
name, render() {
formName, const { input, submitting } = this.props;
formValue,
handleSubmit, return input.value === 'user-script' ? (
<Field name="value" component={Editor} />
) : (
<Textarea resize="vertical" disabled={submitting} fluid />
);
}
}
const KeyValue = ({
id,
label = '',
textarea,
create,
last,
first,
expanded,
removing,
pristine,
error,
submitting,
onRemove, onRemove,
textarea onToggleExpanded,
}) => (
<form onSubmit={handleSubmit}>
<Row>
<Col xs={8} sm={10}>
<FormGroup name={formName} reduxForm>
<Input fluid mono marginless />
</FormGroup>
</Col>
<Col xs={2} sm={1}>
<Button
type="button"
onClick={() => onRemove(name)}
secondary
small
icon
fluid
marginless
>
<BinIcon />
</Button>
</Col>
<Col xs={2} sm={1}>
<Button type="submit" secondary small icon fluid marginless>
S
</Button>
</Col>
<Col xs={12} sm={12}>
<FormGroup name={formValue} reduxForm>
<Field name={formValue} component={Editor} mode="sh" />
</FormGroup>
</Col>
<Divider height="4" width="100%" transparent />
</Row>
</form>
);
const InputKeyValue = ({
name,
formName,
formValue,
handleSubmit, handleSubmit,
onRemove, onClear
textarea }) => {
}) => ( const _error = error &&
<form onSubmit={handleSubmit}> !submitting && (
<Row> <Message error>
<Col xs={12} sm={5}> <MessageTitle>Ooops!</MessageTitle>
<FormGroup name={formName} reduxForm> <MessageDescription>{error}</MessageDescription>
<Input fluid mono marginless /> </Message>
</FormGroup> );
</Col>
<Col xs={12} sm={5}>
<FormGroup name={formValue} reduxForm>
<Input fluid mono marginless />
</FormGroup>
</Col>
<Col xs={6} sm={1}>
<Button
type="button"
onClick={() => onRemove(name)}
secondary
small
icon
fluid
marginless
>
<BinIcon />
</Button>
</Col>
<Col xs={6} sm={1}>
<Button type="submit" secondary small icon fluid marginless>
S
</Button>
</Col>
<SmallOnly>
<Divider height="4" width="100%" transparent />
</SmallOnly>
</Row>
</form>
);
export default ({ textarea, ...rest }) => const _meta = expanded ? (
textarea ? <TextareaKeyValue {...rest} /> : <InputKeyValue {...rest} />; <P>{create ? `Create ${label}` : `Edit ${label}`}</P>
) : (
<CollapsedKeyValue>
<Field
name="name"
type="text"
component={({ input }) => <b>{`${input.value}: `}</b>}
/>
<Field name="value" type="text" component={({ input }) => input.value} />
</CollapsedKeyValue>
);
const chevronToggle = create ? null : (
<CardHeaderBox onClick={onToggleExpanded} actionable={expanded}>
<ChevronIcon />
</CardHeaderBox>
);
const _valueField = textarea ? (
<Field name="name" component={ValueTextareaField} props={{ submitting }} />
) : (
<Input disabled={submitting} />
);
const _cancel = (
<Button
type="button"
key="cancel"
onClick={
create
? pristine ? onToggleExpanded : onClear
: pristine ? onRemove : onClear
}
disabled={submitting}
loading={submitting && removing}
secondary
marginless
>
{create ? (pristine ? 'Cancel' : 'Clear') : pristine ? 'Remove' : 'Clear'}
</Button>
);
const _submit = (
<Button
type="submit"
key="submit"
disabled={pristine || submitting}
loading={submitting && !removing}
secondary
marginless
>
{create ? 'Create' : 'Update'}
</Button>
);
return (
<form onSubmit={handleSubmit}>
<Divider
transparent
marginBottom={!first && expanded ? remcalc(13) : 0}
/>
<Card
collapsed={!expanded}
actionable={!expanded}
bottomless={!last && !expanded}
>
<CardHeader
secondary={false}
transparent={false}
onClick={onToggleExpanded}
actionable
>
<CardHeaderMeta>{_meta}</CardHeaderMeta>
{chevronToggle}
</CardHeader>
<CardOutlet>
<Row>
<Col xs={12}>{_error}</Col>
</Row>
<Row>
<Col xs={12}>
<FormGroup name="name" reduxForm>
<Label>{titleCase(label)} key</Label>
<Input type="text" disabled={submitting} />
<FormMeta />
</FormGroup>
</Col>
</Row>
<Row>
<Col xs={12}>
<FormGroup name="value" reduxForm>
<Label>{titleCase(label)} value</Label>
{_valueField}
</FormGroup>
</Col>
</Row>
<Row>
<Col xs={12}>
{_cancel}
{_submit}
</Col>
</Row>
</CardOutlet>
</Card>
<Divider transparent marginBottom={last || expanded ? remcalc(13) : 0} />
</form>
);
};
export default ({ id, ...rest }) => (
<Value name={`${id}-removing`}>
{({ value: removing }) => (
<KeyValue {...rest} removing={removing} id={id} />
)}
</Value>
);

View File

@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { import {
Header, Header,
@ -7,28 +9,40 @@ import {
DataCenterIconLight, DataCenterIconLight,
UserIconLight, UserIconLight,
HeaderNav, HeaderNav,
HeaderNavAnchor, HeaderAnchor,
HeaderItem HeaderItem
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
const Logo = styled(TritonBetaIcon)`
padding-top: ${remcalc(11)};
`;
export default () => ( export default () => (
<Header> <Header>
<HeaderBrand beta> <HeaderBrand beta>
<HeaderNavAnchor to="/"> <HeaderAnchor to="/">
<TritonBetaIcon alt="Triton" /> <Logo alt="Triton" />
</HeaderNavAnchor> </HeaderAnchor>
</HeaderBrand> </HeaderBrand>
<HeaderNav> <HeaderNav>
<li> <li>
<HeaderNavAnchor to="/">Compute</HeaderNavAnchor> <HeaderAnchor to="/">Compute</HeaderAnchor>
</li> </li>
</HeaderNav> </HeaderNav>
<HeaderItem>Return to existing portal</HeaderItem>
<HeaderItem> <HeaderItem>
<DataCenterIconLight />eu-east-1 <HeaderAnchor href="https://my.joyent.com">
Return to existing portal
</HeaderAnchor>
</HeaderItem> </HeaderItem>
<HeaderItem> <HeaderItem>
<UserIconLight />Nicola <HeaderAnchor>
<DataCenterIconLight />eu-east-1
</HeaderAnchor>
</HeaderItem>
<HeaderItem>
<HeaderAnchor>
<UserIconLight />Nicola
</HeaderAnchor>
</HeaderItem> </HeaderItem>
</Header> </Header>
); );

View File

@ -0,0 +1,204 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders <Metadata /> without throwing 1`] = `
.c3 {
-webkit-animation: iCqDak 1.5s ease-out 0s infinite;
animation: iCqDak 1.5s ease-out 0s infinite;
}
.c4 {
-webkit-animation: iCqDak 1.5s ease-out 0s infinite;
animation: iCqDak 1.5s ease-out 0s infinite;
-webkit-animation-delay: 0.5s;
animation-delay: 0.5s;
}
.c5 {
-webkit-animation: iCqDak 1.5s ease-out 0s infinite;
animation: iCqDak 1.5s ease-out 0s infinite;
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-content: center;
-ms-flex-line-pack: center;
align-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
min-height: 1.25rem;
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
}
.c6 {
font-weight: 400;
line-height: 1.5rem;
font-size: 0.9375rem;
margin: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
-webkit-align-self: stretch;
-ms-flex-item-align: stretch;
align-self: stretch;
text-align: center;
margin-bottom: 0;
margin-left: 0.375rem;
}
.c6 + p,
.c6 + small,
.c6 + h1,
.c6 + h2,
.c6 + label,
.c6 + h3,
.c6 + h4,
.c6 + h5,
.c6 + div,
.c6 + span {
padding-bottom: 2.25rem;
}
.c0 {
margin-right: auto;
margin-left: auto;
padding-bottom: 1.125rem;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-content: center;
-ms-flex-line-pack: center;
align-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c1 {
margin: 0;
font-weight: 400;
line-height: 1.875rem;
font-size: 1.5rem;
margin-top: 1.1875rem;
margin-bottom: 1.8125rem;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
-webkit-align-self: stretch;
-ms-flex-item-align: stretch;
align-self: stretch;
}
.c1 + p,
.c1 + small,
.c1 + h1,
.c1 + h2,
.c1 + label,
.c1 + h3,
.c1 + h4,
.c1 + h5,
.c1 + div,
.c1 + span {
margin-top: 1.5rem;
}
@media only screen and (min-width:48em) {
.c0 {
width: 46rem;
}
}
@media only screen and (min-width:64em) {
.c0 {
width: 61rem;
}
}
@media only screen and (min-width:75em) {
.c0 {
width: 76rem;
}
}
@media only screen and (max-width:47.9375rem) {
.c0 {
padding-left: 0.375rem;
padding-right: 0.375rem;
}
}
<div
className="c0"
>
<h2
className="c1"
>
Metadata
</h2>
<div
className="c2"
>
<svg
height="10"
width="28"
>
<rect
className="c3"
height="6"
width="6"
x="2"
y="2"
/>
<rect
className="c4"
height="6"
width="6"
x="11"
y="2"
/>
<rect
className="c5"
height="6"
width="6"
x="20"
y="2"
/>
</svg>
<p
className="c6"
>
Loading...
</p>
</div>
</div>
`;

View File

@ -0,0 +1,18 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { reduxForm } from 'redux-form';
import Store from '@mocks/store';
import 'jest-styled-components';
import Metadata from '../metadata';
it('renders <Metadata /> without throwing', () => {
const tree = renderer
.create(
<Store>
<Metadata />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -23,15 +23,15 @@ const DNS = ({ instance, loading, error }) => {
const _summary = !_loading && instance && <ReactJson src={dns_names} />; const _summary = !_loading && instance && <ReactJson src={dns_names} />;
const _error = error && const _error = error &&
!_loading && !_loading &&
!instance && ( !instance && (
<Message error> <Message error>
<MessageTitle>Ooops!</MessageTitle> <MessageTitle>Ooops!</MessageTitle>
<MessageDescription> <MessageDescription>
An error occurred while loading your instance DNS An error occurred while loading your instance DNS
</MessageDescription> </MessageDescription>
</Message> </Message>
); );
return ( return (
<ViewContainer center={Boolean(_loading)} main> <ViewContainer center={Boolean(_loading)} main>

View File

@ -34,31 +34,29 @@ const Firewall = ({
const _title = <Title>Firewall</Title>; const _title = <Title>Firewall</Title>;
const _loading = !(loading && !values.length) ? null : <StatusLoader />; const _loading = !(loading && !values.length) ? null : <StatusLoader />;
const _firewall = (_loading && !values.length) ? null : ( const _firewall =
<Table> _loading && !values.length ? null : (
<TableThead> <Table>
<TableTr> <TableThead>
<TableTh left bottom> <TableTr>
<P>Rule</P> <TableTh left bottom>
</TableTh> <P>Rule</P>
<TableTh xs="63" center bottom> </TableTh>
<P>Global</P> <TableTh xs="63" center bottom>
</TableTh> <P>Global</P>
<TableTh xs="75" center bottom> </TableTh>
<P>Enabled</P> <TableTh xs="75" center bottom>
</TableTh> <P>Enabled</P>
</TableTr> </TableTh>
</TableThead> </TableTr>
<TableTbody>{ </TableThead>
values.map((network) => ( <TableTbody>
<InstanceFirewallRule {values.map(network => (
key={network.id} <InstanceFirewallRule key={network.id} {...network} />
{...network} ))}
/> </TableTbody>
))} </Table>
</TableTbody> );
</Table>
);
const _error = const _error =
error && !values.length && !_loading ? ( error && !values.length && !_loading ? (

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import paramCase from 'param-case'; import paramCase from 'param-case';
import forceArray from 'force-array'; import Value, { set } from 'react-redux-values';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { reduxForm } from 'redux-form'; import { connect } from 'react-redux';
import { SubmissionError, reset, startSubmit, stopSubmit } from 'redux-form';
import ReduxForm from 'declarative-redux-form';
import find from 'lodash.find'; import find from 'lodash.find';
import sortBy from 'lodash.sortby';
import get from 'lodash.get'; import get from 'lodash.get';
import { import {
@ -13,42 +15,93 @@ import {
StatusLoader, StatusLoader,
Message, Message,
MessageDescription, MessageDescription,
MessageTitle MessageTitle,
Button
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
import GetMetadata from '@graphql/list-metadata.gql'; import GetMetadata from '@graphql/list-metadata.gql';
import UpdateMetadata from '@graphql/update-metadata.gql';
import DeleteMetadata from '@graphql/delete-metadata.gql';
import { KeyValue } from '@components/instances'; import { KeyValue } from '@components/instances';
const MetadataForms = (metadata = []) => const METADATA_FORM_KEY = (name, field) => `instance-metadata-${name}-${field}`;
metadata.map(({ key, formName, formValue, value, name }) => { const CREATE_METADATA_FORM_KEY = name => `instance-create-metadata-${name}`;
const MetadataForm = reduxForm({
form: `instance-metadata-${key}`,
initialValues: {
[formName]: name,
[formValue]: value
}
})(KeyValue);
return ( const Metadata = ({
<MetadataForm instance,
key={key} values = [],
formName={formName} loading,
formValue={formValue} error,
name={key} handleRemove,
onSubmit={val => console.log(key, val)} handleClear,
onRemove={key => console.log('remove', key)} handleUpdate,
textarea handleCreate
/> }) => {
);
});
const Metadata = ({ metadata = [], loading, error }) => {
const values = forceArray(metadata);
const _title = <Title>Metadata</Title>; const _title = <Title>Metadata</Title>;
const _loading = !(loading && !values.length) ? null : <StatusLoader />; const _loading = !(loading && !values.length) ? null : <StatusLoader />;
const _metadata = !_loading && MetadataForms(values); // metadata items forms
const _metadata =
!_loading &&
values.map(({ form, initialValues }, i) => (
<Value name={`${form}-expanded`} key={form}>
{({ value: expanded, onValueChange }) => (
<ReduxForm
form={form}
initialValues={initialValues}
onSubmit={newValues => handleUpdate(newValues, form)}
destroyOnUnmount
id={form}
onClear={() => handleClear(form)}
onToggleExpanded={() => onValueChange(!expanded)}
onRemove={() => handleRemove(form)}
label="metadata"
last={values.length - 1 === i}
first={i === 0}
expanded={expanded}
textarea
>
{KeyValue}
</ReduxForm>
)}
</Value>
));
// create metadata form
const _addKey = instance && CREATE_METADATA_FORM_KEY(instance.name);
const _add = _metadata &&
_addKey && (
<Value name={`${_addKey}-expanded`}>
{({ value: expanded, onValueChange }) =>
!expanded ? (
<Button
type="button"
onClick={() => onValueChange(!expanded)}
secondary
>
Add metadata
</Button>
) : (
<ReduxForm
form={_addKey}
onSubmit={handleCreate}
id={_addKey}
onClear={() => handleClear(_addKey)}
onToggleExpanded={() => onValueChange(!expanded)}
onRemove={() => handleRemove(_addKey)}
expanded={expanded}
label="metadata"
create
textarea
>
{KeyValue}
</ReduxForm>
)
}
</Value>
);
// fetching error
const _error = const _error =
error && !values.length && !_loading ? ( error && !values.length && !_loading ? (
<Message error> <Message error>
@ -65,15 +118,14 @@ const Metadata = ({ metadata = [], loading, error }) => {
{_loading} {_loading}
{_error} {_error}
{_metadata} {_metadata}
{_add}
</ViewContainer> </ViewContainer>
); );
}; };
Metadata.propTypes = {
loading: PropTypes.bool
};
export default compose( export default compose(
graphql(UpdateMetadata, { name: 'updateMetadata' }),
graphql(DeleteMetadata, { name: 'deleteMetadata' }),
graphql(GetMetadata, { graphql(GetMetadata, {
options: ({ match }) => ({ options: ({ match }) => ({
pollInterval: 1000, pollInterval: 1000,
@ -81,29 +133,125 @@ export default compose(
name: get(match, 'params.instance') name: get(match, 'params.instance')
} }
}), }),
props: ({ data: { loading, error, variables, ...rest } }) => { props: ({ data: { loading, error, variables, refetch, ...rest } }) => {
const values = get( const { name } = variables;
find(get(rest, 'machines', []), ['name', variables.name]),
'metadata',
[]
);
const metadata = values.reduce((all, { name, value }) => { const instance = find(get(rest, 'machines', []), ['name', name]);
const key = paramCase(name); const metadata = get(instance, 'metadata', []);
const values = sortBy(metadata, 'name').map(({ name, value }) => {
const field = paramCase(name);
const form = METADATA_FORM_KEY(name, field);
return { return {
...all, form,
[key]: { initialValues: {
key, name,
formName: `${key}-name`, value
formValue: `${key}-value`,
value,
name
} }
}; };
}, {}); });
return { metadata: Object.values(metadata), loading, error }; return {
values,
instance,
loading,
error,
refetch
};
} }
}),
connect(null, (dispatch, ownProps) => {
const {
instance,
values,
refetch,
updateMetadata,
deleteMetadata
} = ownProps;
return {
// reset sets values to initialValues
handleClear: form => dispatch(reset(form)),
handleRemove: form =>
Promise.resolve(
// set removing=true (so that we can have a specific removing spinner)
// because remove button is not a submit button, we have to manually flip that flag
dispatch([
set({ name: `${form}-removing`, value: true }),
startSubmit(form)
])
)
.then(() =>
// call mutation. get key from values' initialValues
deleteMetadata({
variables: {
id: instance.id,
name: get(find(values, ['form', form]), 'initialValues.name')
}
})
)
// fetch metadata again
.then(() => refetch())
// we only flip removing and submitting when there is an error.
// the reason for that is that metadata is updated asyncronously and
// it takes longer to have an efect than the mutation
.catch(error =>
dispatch([
set({ name: `${form}-removing`, value: false }),
stopSubmit(form, {
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
})
])
),
handleUpdate: ({ name, value }, form) =>
// call mutation. delete existing metadata, add new
Promise.all([
deleteMetadata({
variables: {
id: instance.id,
name: get(find(values, ['form', form]), 'initialValues.name')
}
}),
updateMetadata({
variables: {
id: instance.id,
metadata: [{ name, value }]
}
})
])
// fetch metadata again
.then(() => refetch())
// submit is flipped once the promise is resolved
.catch(error => {
throw new SubmissionError({
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
});
}),
handleCreate: ({ name, value }) =>
// call mutation
updateMetadata({
variables: {
id: instance.id,
metadata: [{ name, value }]
}
})
// fetch metadata again
.then(() => refetch())
// reset create new metadata form
.then(() => dispatch(reset(CREATE_METADATA_FORM_KEY(instance.name))))
// submit is flipped once the promise is resolved
.catch(error => {
throw new SubmissionError({
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
});
})
};
}) })
)(Metadata); )(Metadata);

View File

@ -28,34 +28,32 @@ const Networks = ({ networks = [], loading, error }) => {
const _title = <Title>Networks</Title>; const _title = <Title>Networks</Title>;
const _loading = !(loading && !values.length) ? null : <StatusLoader />; const _loading = !(loading && !values.length) ? null : <StatusLoader />;
const _networks = (_loading && !values.length) ? null : ( const _networks =
<Table> _loading && !values.length ? null : (
<TableThead> <Table>
<TableTr> <TableThead>
<TableTh left bottom> <TableTr>
<P>Name</P> <TableTh left bottom>
</TableTh> <P>Name</P>
<TableTh xs="90" left bottom> </TableTh>
<P>Gateway</P> <TableTh xs="90" left bottom>
</TableTh> <P>Gateway</P>
<TableTh xs="90" left bottom> </TableTh>
<P>Subnet</P> <TableTh xs="90" left bottom>
</TableTh> <P>Subnet</P>
<TableTh xs="90" left bottom> </TableTh>
<P>Resolvers</P> <TableTh xs="90" left bottom>
</TableTh> <P>Resolvers</P>
</TableTr> </TableTh>
</TableThead> </TableTr>
<TableTbody>{ </TableThead>
values.map((network) => ( <TableTbody>
<InstanceNetwork {values.map(network => (
key={network.id} <InstanceNetwork key={network.id} {...network} />
{...network} ))}
/> </TableTbody>
))} </Table>
</TableTbody> );
</Table>
);
const _error = const _error =
error && !values.length && !_loading ? ( error && !values.length && !_loading ? (

View File

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import paramCase from 'param-case'; import paramCase from 'param-case';
import forceArray from 'force-array';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { reduxForm } from 'redux-form'; import Value, { set } from 'react-redux-values';
import { SubmissionError, reset, startSubmit, stopSubmit } from 'redux-form';
import ReduxForm from 'declarative-redux-form';
import { connect } from 'react-redux';
import find from 'lodash.find'; import find from 'lodash.find';
import get from 'lodash.get'; import get from 'lodash.get';
@ -13,42 +14,91 @@ import {
StatusLoader, StatusLoader,
Message, Message,
MessageDescription, MessageDescription,
MessageTitle MessageTitle,
Button
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
import { KeyValue } from '@components/instances';
import GetTags from '@graphql/list-tags.gql'; import GetTags from '@graphql/list-tags.gql';
import PutTags from '@graphql/add-tags.gql'; import UpdateTags from '@graphql/update-tags.gql';
import DeleteTag from '@graphql/delete-tag.gql';
import { KeyValue } from '@components/instances';
const TagForms = (tags = []) => const TAG_FORM_KEY = (name, field) => `instance-tag-${name}-${field}`;
tags.map(({ key, formName, formValue, value, name }) => { const CREATE_TAG_FORM_KEY = name => `instance-create-tag-${name}`;
const TagForm = reduxForm({
form: `instance-tags-${key}`,
initialValues: {
[formName]: name,
[formValue]: value
}
})(KeyValue);
return ( const Tags = ({
<TagForm instance,
key={key} values = [],
formName={formName} loading,
formValue={formValue} error,
name={key} handleRemove,
onSubmit={val => console.log(key, val)} handleClear,
onRemove={key => console.log('remove', key)} handleUpdate,
/> handleCreate
); }) => {
});
const Tags = ({ tags = [], loading, error }) => {
const values = forceArray(tags);
const _title = <Title>Tags</Title>; const _title = <Title>Tags</Title>;
const _loading = loading && !values.length ? <StatusLoader /> : null; const _loading = !(loading && !values.length) ? null : <StatusLoader />;
const _tags = !_loading && TagForms(tags); // tags items forms
const _tags =
!_loading &&
values.map(({ form, initialValues }, i) => (
<Value name={`${form}-expanded`} key={form}>
{({ value: expanded, onValueChange }) => (
<ReduxForm
form={form}
initialValues={initialValues}
onSubmit={newValues => handleUpdate(newValues, form)}
destroyOnUnmount
id={form}
onClear={() => handleClear(form)}
onToggleExpanded={() => onValueChange(!expanded)}
onRemove={() => handleRemove(form)}
label="tag"
last={values.length - 1 === i}
first={i === 0}
expanded={expanded}
>
{KeyValue}
</ReduxForm>
)}
</Value>
));
// create tags form
const _addKey = instance && CREATE_TAG_FORM_KEY(instance.name);
const _add = _tags &&
_addKey && (
<Value name={`${_addKey}-expanded`}>
{({ value: expanded, onValueChange }) =>
!expanded ? (
<Button
type="button"
onClick={() => onValueChange(!expanded)}
secondary
>
Add tag
</Button>
) : (
<ReduxForm
form={_addKey}
onSubmit={handleCreate}
id={_addKey}
onClear={() => handleClear(_addKey)}
onToggleExpanded={() => onValueChange(!expanded)}
onRemove={() => handleRemove(_addKey)}
expanded={expanded}
label="tag"
create
>
{KeyValue}
</ReduxForm>
)
}
</Value>
);
// fetching error
const _error = const _error =
error && !values.length && !_loading ? ( error && !values.length && !_loading ? (
<Message error> <Message error>
@ -65,15 +115,14 @@ const Tags = ({ tags = [], loading, error }) => {
{_loading} {_loading}
{_error} {_error}
{_tags} {_tags}
{_add}
</ViewContainer> </ViewContainer>
); );
}; };
Tags.propTypes = {
loading: PropTypes.bool
};
export default compose( export default compose(
graphql(UpdateTags, { name: 'updateTags' }),
graphql(DeleteTag, { name: 'deleteTag' }),
graphql(GetTags, { graphql(GetTags, {
options: ({ match }) => ({ options: ({ match }) => ({
pollInterval: 1000, pollInterval: 1000,
@ -81,37 +130,119 @@ export default compose(
name: get(match, 'params.instance') name: get(match, 'params.instance')
} }
}), }),
props: ({ data: { loading, error, variables, ...rest } }) => { props: ({ data: { loading, error, variables, refetch, ...rest } }) => {
const values = get( const { name } = variables;
find(get(rest, 'machines', []), ['name', variables.name]),
'tags',
[]
);
const tags = values.reduce((all, { name, value }) => { const instance = find(get(rest, 'machines', []), ['name', name]);
const key = paramCase(name); const tags = get(instance, 'tags', []);
const values = tags.map(({ name, value }) => {
const field = paramCase(name);
const form = TAG_FORM_KEY(name, field);
return { return {
...all, form,
[key]: { initialValues: {
key, name,
formName: `${key}-name`, value
formValue: `${key}-value`,
value,
name
} }
}; };
}, {}); });
return { tags: Object.values(tags), loading, error }; return {
values,
instance,
loading,
error,
refetch
};
} }
}), }),
graphql(PutTags, { connect(null, (dispatch, ownProps) => {
props: ({ mutate, ownProps }) => ({ const { instance, values, refetch, updateTags, deleteTag } = ownProps;
updateTag: (name = '', value = '') =>
mutate({ return {
variables: { name, value } // reset sets values to initialValues
handleClear: form => dispatch(reset(form)),
handleRemove: form =>
Promise.resolve(
// set removing=true (so that we can have a specific removing spinner)
// because remove button is not a submit button, we have to manually flip that flag
dispatch([
set({ name: `${form}-removing`, value: true }),
startSubmit(form)
])
)
.then(() =>
// call mutation
deleteTag({
variables: {
id: instance.id,
name: get(find(values, ['form', form]), 'initialValues.name')
}
})
)
// fetch tags again
.then(() => refetch())
// we only flip removing and submitting when there is an error.
// the reason for that is that tags is updated asyncronously and
// it takes longer to have an efect than the mutation
.catch(error =>
dispatch([
set({ name: `${form}-removing`, value: false }),
stopSubmit(form, {
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
})
])
),
handleUpdate: ({ name, value }, form) =>
// delete old tag and add a new one
Promise.all([
deleteTag({
variables: {
id: instance.id,
name: get(find(values, ['form', form]), 'initialValues.name')
}
}),
updateTags({
variables: {
id: instance.id,
tags: [{ name, value }]
}
})
])
// fetch tags again
.then(() => refetch())
// submit is flipped once the promise is resolved
.catch(error => {
throw new SubmissionError({
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
});
}),
handleCreate: ({ name, value }) =>
// call mutation
updateTags({
variables: {
id: instance.id,
tags: [{ name, value }]
}
}) })
}) // fetch tags again
.then(() => refetch())
// reset create new tags form
.then(() => dispatch(reset(CREATE_TAG_FORM_KEY(instance.name))))
// submit is flipped once the promise is resolved
.catch(error => {
throw new SubmissionError({
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
});
})
};
}) })
)(Tags); )(Tags);

View File

@ -0,0 +1,5 @@
mutation deleteMachineMetadata($id: ID!, $name: String!) {
deleteMachineMetadata(id: $id, name: $name) {
id
}
}

View File

@ -0,0 +1,5 @@
mutation deleteMachineTag($id: ID!, $name: String!) {
deleteMachineTag(id: $id, name: $name) {
id
}
}

View File

@ -0,0 +1,5 @@
mutation updateMachineMetadata($id: ID!, $metadata: [KeyValueInput]!) {
updateMachineMetadata(id: $id, metadata: $metadata) {
id
}
}

View File

@ -0,0 +1,5 @@
mutation addMachineTags($id: ID!, $tags: [KeyValueInput]!) {
addMachineTags(id: $id, tags: $tags) {
id
}
}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { client, store } from '@state/store'; import { client, store } from '@root/state/store';
import { ApolloProvider } from 'react-apollo'; import { ApolloProvider } from 'react-apollo';
export default ({ children }) => ( export default ({ children }) => (

View File

@ -2,6 +2,7 @@ import { reduxBatch } from '@manaflair/redux-batch';
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 { reducer as valuesReducer } from 'react-redux-values';
import { ui } from './reducers'; import { ui } from './reducers';
import state from './state'; import state from './state';
@ -47,6 +48,7 @@ export const client = new ApolloClient({
export const store = createStore( export const store = createStore(
combineReducers({ combineReducers({
values: valuesReducer,
apollo: client.reducer(), apollo: client.reducer(),
form: formReducer, form: formReducer,
ui ui

View File

@ -1 +0,0 @@
module.exports = 'test-file-mock';

View File

@ -1,66 +0,0 @@
#!/usr/bin/env node
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('react-scripts/config/env');
const jest = require('jest');
const argv = process.argv.slice(2);
// This is not necessary after eject because we embed config into package.json.
const createJestConfig = require('react-scripts/scripts/utils/createJestConfig');
const path = require('path');
const paths = require('react-scripts/config/paths');
const config = createJestConfig(
relativePath =>
path.resolve(
__dirname,
'../../../node_modules/react-scripts',
relativePath
),
path.resolve(__dirname, '../../../'),
false
);
// patch
config.testEnvironment = 'node';
config.transform = Object.assign(
{},
{
'\\.(gql|graphql)$': 'jest-transform-graphql'
},
config.transform
);
config.testMatch = [
'<rootDir>/packages/joyent-boilerplate/src/**/**/__tests__/**/*.js',
'<rootDir>/packages/joyent-boilerplate/src/**/**/**/?(*.)(spec|test).js'
];
config.moduleNameMapper = Object.assign({}, config.moduleNameMapper, {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/packages/joyent-boilerplate/test/file-mock.js',
'^@root/(.*)$': '<rootDir>/packages/joyent-boilerplate/src/$1',
'^@mocks/(.*)$': '<rootDir>/packages/joyent-boilerplate/test/mocks$1',
'^@components/(.*)$':
'<rootDir>/packages/joyent-boilerplate/src/components/$1',
'^@containers/(.*)$':
'<rootDir>/packages/joyent-boilerplate/src/containers/$1',
'^@graphql/(.*)$': '<rootDir>/packages/joyent-boilerplate/src/graphql/$1',
'^@assets/(.*)$': '<rootDir>/packages/joyent-boilerplate/src/assets/$1',
'^@state/(.*)$': '<rootDir>/packages/joyent-boilerplate/src/state/$1'
});
argv.push('--config', JSON.stringify(config));
jest.run(argv);

View File

@ -37,9 +37,10 @@
"disable-scroll": "^0.3.0", "disable-scroll": "^0.3.0",
"fontfaceobserver": "^2.0.13", "fontfaceobserver": "^2.0.13",
"joy-react-broadcast": "^0.6.9", "joy-react-broadcast": "^0.6.9",
"joyent-manifest-editor": "^1.4.0", "joyent-manifest-editor": "^3.0.1",
"lodash.isboolean": "^3.0.3",
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",
"moment": "^2.19.1", "moment": "^2.19.2",
"normalized-styled-components": "^1.0.17", "normalized-styled-components": "^1.0.17",
"outy": "^0.1.2", "outy": "^0.1.2",
"pascal-case": "^2.0.1", "pascal-case": "^2.0.1",
@ -48,54 +49,54 @@
"react-input-range": "^1.2.1", "react-input-range": "^1.2.1",
"react-popper": "^0.7.4", "react-popper": "^0.7.4",
"react-responsive": "^3.0.0", "react-responsive": "^3.0.0",
"react-styled-flexboxgrid": "^2.1.0", "react-styled-flexboxgrid": "^2.1.1",
"redrun": "^5.9.18", "redrun": "^5.10.0",
"remcalc": "^1.0.9", "remcalc": "^1.0.9",
"rnd-id": "^1.1.1", "rnd-id": "^2.0.0",
"styled-components": "^2.2.2", "styled-components": "^2.2.3",
"styled-is": "^1.1.0", "styled-is": "^1.1.0",
"unitcalc": "^1.1.1" "unitcalc": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.26.0", "babel-cli": "^6.26.0",
"babel-plugin-inline-react-svg": "^0.4.0", "babel-plugin-inline-react-svg": "^0.4.0",
"babel-plugin-lodash": "^3.2.11", "babel-plugin-lodash": "^3.3.2",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0", "babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0", "babel-plugin-transform-es3-property-literals": "^6.22.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-preset-joyent-portal": "^3.3.3", "babel-preset-joyent-portal": "^3.3.3",
"codemirror": "^5.31.0", "codemirror": "^5.31.0",
"eslint": "^4.9.0", "eslint": "^4.11.0",
"eslint-config-joyent-portal": "^3.2.0", "eslint-config-joyent-portal": "^3.2.0",
"http-server": "^0.10.0", "http-server": "^0.10.0",
"jest": "^21.2.1", "jest": "^21.2.1",
"jest-diff": "^21.2.1", "jest-diff": "^21.2.1",
"jest-image-snapshot": "^1.0.1", "jest-image-snapshot": "^2.2.0",
"jest-matcher-utils": "^21.2.1", "jest-matcher-utils": "^21.2.1",
"jest-snapshot": "^21.2.1", "jest-snapshot": "^21.2.1",
"jest-styled-components": "^4.9.0", "jest-styled-components": "^4.9.0",
"joyent-react-scripts": "^2.6.0", "joyent-react-scripts": "^3.1.0",
"lodash.isboolean": "^3.0.3", "lodash.isboolean": "^3.0.3",
"navalia": "^1.2.0", "navalia": "^1.2.0",
"react": "^16.0.0", "react": "^16.1.1",
"react-docgen": "^3.0.0-beta8", "react-docgen": "^3.0.0-beta8",
"react-docgen-displayname-handler": "^1.0.1", "react-docgen-displayname-handler": "^1.0.1",
"react-dom": "^16.0.0", "react-dom": "^16.1.1",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-styleguidist": "^6.0.31", "react-styleguidist": "^6.0.33",
"react-test-renderer": "^16.0.0", "react-test-renderer": "^16.1.1",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-form": "^7.1.1", "redux-form": "^7.1.2",
"serve-static": "^1.13.1", "serve-static": "^1.13.1",
"stylelint": "^8.2.0", "stylelint": "^8.2.0",
"stylelint-config-joyent-portal": "^2.0.1", "stylelint-config-joyent-portal": "^2.0.1",
"webpack": "^3.8.1" "webpack": "^3.8.1"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.0.0", "react": "^16.1.1",
"react-dom": "^16.0.0", "react-dom": "^16.1.1",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"redux-form": "^7.1.1" "redux-form": "^7.1.2"
} }
} }

View File

@ -1,5 +1,6 @@
import { css } from 'styled-components'; import { css } from 'styled-components';
import { loadedFontFamily } from '../typography'; import { loadedFontFamily } from '../typography';
import remcalc from 'remcalc';
export default ({ theme }) => css` export default ({ theme }) => css`
[hidden] { [hidden] {
@ -38,5 +39,6 @@ export default ({ theme }) => css`
.CodeMirror { .CodeMirror {
border: solid 1px ${theme.grey}; border: solid 1px ${theme.grey};
margin: ${remcalc(8)} 0 ${remcalc(8)} 0;
} }
`; `;

View File

@ -15,8 +15,8 @@ on the props passed to it. E.g.:
Is going to translate into a `<Button />` that has `12px` of margin. Is going to translate into a `<Button />` that has `12px` of margin.
What enables this is the [`Baseline` What enables this is the
composer](https://github.com/yldio/joyent-portal/blob/a5774063ed8caf2569aff2905af2d7dca7a01a52/ui/src/shared/composers/index.js#L51). [`Baseline` composer](https://github.com/yldio/joyent-portal/blob/a5774063ed8caf2569aff2905af2d7dca7a01a52/ui/src/shared/composers/index.js#L51).
The Baseline composer is essentially an The Baseline composer is essentially an
[HOC](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750): [HOC](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750):

View File

@ -27,13 +27,11 @@ export const BaseCard = styled.div`
min-height: ${remcalc(125)}; min-height: ${remcalc(125)};
position: relative; position: relative;
margin-bottom: 0;
transition: all 300ms ease;
border-width: ${remcalc(1)}; border-width: ${remcalc(1)};
border-style: solid; border-style: solid;
transition: all 300ms ease;
/* primary */ /* primary */
color: ${props => props.theme.text}; color: ${props => props.theme.text};
background-color: ${props => props.theme.white}; background-color: ${props => props.theme.white};
@ -95,18 +93,46 @@ export const BaseCard = styled.div`
height: ${remcalc(46)}; height: ${remcalc(46)};
flex: 0 0 ${remcalc(46)}; flex: 0 0 ${remcalc(46)};
`}; `};
${is('bottomless')`
border-bottom-width: 0;
`};
`; `;
/** /**
* @example ./demo.md * @example ./demo.md
*/ */
const Card = ({ children, ...rest }) => ( const Card = ({
<Broadcast channel="card" value={rest}> children,
<BaseCard {...rest} name="card"> secondary,
{children} tertiary,
</BaseCard> collapsed,
</Broadcast> disabled,
); stacked,
active,
shadow,
actionable,
...rest
}) => {
const newValue = {
secondary,
tertiary,
collapsed,
disabled,
stacked,
active,
shadow,
actionable
};
return (
<Broadcast channel="card" value={newValue}>
<BaseCard {...rest} {...newValue} name="card">
{children}
</BaseCard>
</Broadcast>
);
};
Card.propTypes = { Card.propTypes = {
children: PropTypes.node, children: PropTypes.node,

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Broadcast, Subscriber } from 'joy-react-broadcast'; import { Broadcast, Subscriber } from 'joy-react-broadcast';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import is, { isNot, isOr } from 'styled-is'; import is, { isNot, isOr } from 'styled-is';
import isBoolean from 'lodash.isboolean';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import Baseline from '../baseline'; import Baseline from '../baseline';
@ -9,6 +10,7 @@ import Card, { BaseCard } from './card';
const BaseHeader = BaseCard.extend` const BaseHeader = BaseCard.extend`
flex-direction: row; flex-direction: row;
z-index: 1;
margin: ${remcalc(-1)} ${remcalc(-1)} 0 ${remcalc(-1)}; margin: ${remcalc(-1)} ${remcalc(-1)} 0 ${remcalc(-1)};
@ -47,7 +49,7 @@ const BaseBox = BaseCard.extend`
min-width: ${remcalc(49)}; min-width: ${remcalc(49)};
height: ${remcalc(46)}; height: ${remcalc(46)};
display: flex; display: inline-flex;
flex: 0 0 ${remcalc(49)}; flex: 0 0 ${remcalc(49)};
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -82,17 +84,20 @@ const BaseBox = BaseCard.extend`
`; `;
const BaseMeta = BaseCard.extend` const BaseMeta = BaseCard.extend`
box-sizing: border-box;
height: ${remcalc(47)}; height: ${remcalc(47)};
width: auto; width: auto;
min-width: auto; min-width: auto;
padding: 0 ${remcalc(12)}; padding: ${remcalc(12)};
display: flex; display: inline-flex;
flex: 1 0 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: stretch; align-items: stretch;
align-conent: stretch; align-content: stretch;
overflow: hidden;
background-color: transparent; background-color: transparent;
border-width: 0; border-width: 0;
@ -108,8 +113,8 @@ export const Box = ({ children, border, actionable, ...rest }) => {
return ( return (
<BaseBox <BaseBox
{...rest}
{...value} {...value}
{...rest}
name="card-header-box" name="card-header-box"
border={newBorder} border={newBorder}
secondary={secondary} secondary={secondary}
@ -149,12 +154,15 @@ export const Meta = ({ children, ...rest }) => (
); );
const Header = ({ children, transparent, shadow, ...rest }) => { const Header = ({ children, transparent, shadow, ...rest }) => {
const render = ({ secondary, tertiary, collapsed, ...value }) => { const render = ({ secondary, tertiary, collapsed, actionable, ...value }) => {
const parentPrimary = !secondary && !tertiary; const parentPrimary = !secondary && !tertiary;
// if secondary is hardcoded, use that
// if transparent and parent is secondary, keep seconday // if transparent and parent is secondary, keep seconday
// if parent is secondary, keep being secondary or // if parent is secondary, keep being secondary or
// if parent is primary, become secondary // if parent is primary, become secondary
const isSecondary = transparent ? secondary : secondary || parentPrimary; const isSecondary = isBoolean(rest.secondary)
? rest.secondary
: transparent ? secondary : secondary || parentPrimary;
// if parent is primary, don't become transparent // if parent is primary, don't become transparent
const isTransparent = transparent || secondary || tertiary; const isTransparent = transparent || secondary || tertiary;
@ -162,7 +170,8 @@ const Header = ({ children, transparent, shadow, ...rest }) => {
...value, ...value,
parentCollapsed: collapsed, parentCollapsed: collapsed,
secondary: isSecondary, secondary: isSecondary,
tertiary, tertiary: isBoolean(rest.tertiary) ? rest.tertiary : tertiary,
actionable: isBoolean(rest.actionable) ? rest.actionable : actionable,
transparent: isTransparent, transparent: isTransparent,
collapsed: true, collapsed: true,
shadow: Boolean(shadow) shadow: Boolean(shadow)

View File

@ -10,10 +10,12 @@ import Card, { BaseCard } from './card';
const BaseOutlet = BaseCard.extend` const BaseOutlet = BaseCard.extend`
box-sizing: border-box; box-sizing: border-box;
display: inline-flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
border-width: 0; border-width: 0;
padding: ${remcalc(13)}; padding: ${remcalc(13)};
margin-bottom: 0;
background-color: transparent; background-color: transparent;
@ -21,6 +23,10 @@ const BaseOutlet = BaseCard.extend`
display: none; display: none;
`}; `};
${is('pullUp')`
margin-top: ${remcalc(-48)};
`};
& > [name='card']:not(:last-child) { & > [name='card']:not(:last-child) {
margin-bottom: ${remcalc(13)}; margin-bottom: ${remcalc(13)};
} }

View File

@ -725,6 +725,145 @@ const { Row, Col } = require('react-styled-flexboxgrid');
]; ];
``` ```
#### Card > Header > Secondary
```jsx
const React = require('react');
const { default: Card, Header, HeaderBox, HeaderMeta } = require('.');
const { Row, Col } = require('react-styled-flexboxgrid');
[
<Row>
<Col xs={12}>
<Card shadow>
<Header secondary={false}>
<HeaderBox border="right">
<code>HB</code>
</HeaderBox>
<HeaderBox>
<code>HB</code>
</HeaderBox>
<HeaderMeta>
<code>{`<HeaderMeta />`}</code>
</HeaderMeta>
<HeaderBox border="left">
<code>HB</code>
</HeaderBox>
</Header>
<code>{`<Card shadow />`}</code>
</Card>
</Col>
</Row>,
<br />,
<Row>
<Col xs={12}>
<Card secondary={false} shadow>
<Header secondary>
<HeaderBox border="right">
<code>HB</code>
</HeaderBox>
<HeaderBox>
<code>HB</code>
</HeaderBox>
<HeaderMeta>
<code>{`<HeaderMeta />`}</code>
</HeaderMeta>
<HeaderBox border="left">
<code>HB</code>
</HeaderBox>
</Header>
<code>{`<Card secondary shadow />`}</code>
</Card>
</Col>
</Row>,
<br />,
<Row>
<Col xs={12}>
<Card tertiary shadow>
<Header secondary={false}>
<HeaderBox border="right">
<code>HB</code>
</HeaderBox>
<HeaderBox>
<code>HB</code>
</HeaderBox>
<HeaderMeta>
<code>{`<HeaderMeta />`}</code>
</HeaderMeta>
<HeaderBox border="left">
<code>HB</code>
</HeaderBox>
</Header>
<code>{`<Card tertiary shadow />`}</code>
</Card>
</Col>
</Row>,
<br />,
<Row>
<Col xs={12}>
<Card collapsed shadow>
<Header secondary={false}>
<HeaderBox border="right">
<code>HB</code>
</HeaderBox>
<HeaderBox>
<code>HB</code>
</HeaderBox>
<HeaderMeta>
<code>{`<HeaderMeta />`}</code>
</HeaderMeta>
<HeaderBox border="left">
<code>HB</code>
</HeaderBox>
</Header>
</Card>
</Col>
</Row>,
<br />,
<Row>
<Col xs={12}>
<Card secondary collapsed shadow>
<Header secondary={false}>
<HeaderBox border="right">
<code>HB</code>
</HeaderBox>
<HeaderBox>
<code>HB</code>
</HeaderBox>
<HeaderMeta>
<code>{`<HeaderMeta />`}</code>
</HeaderMeta>
<HeaderBox border="left">
<code>HB</code>
</HeaderBox>
</Header>
</Card>
</Col>
</Row>,
<br />,
<Row>
<Col xs={12}>
<Card tertiary collapsed shadow>
<Header secondary={false}>
<HeaderBox border="right">
<code>HB</code>
</HeaderBox>
<HeaderBox>
<code>HB</code>
</HeaderBox>
<HeaderMeta>
<code>{`<HeaderMeta />`}</code>
</HeaderMeta>
<HeaderBox border="left">
<code>HB</code>
</HeaderBox>
</Header>
</Card>
</Col>
</Row>
];
```
#### Card > Outlet #### Card > Outlet
```jsx ```jsx

View File

@ -7,13 +7,11 @@ import Baseline from '../baseline';
const Divider = styled(Row)` const Divider = styled(Row)`
background-color: ${props => props.theme.grey}; background-color: ${props => props.theme.grey};
margin: 0;
${is('transparent')` ${is('transparent')`
background-color: transparent; background-color: transparent;
`}; `};
height: ${remcalc(1)};
margin: 0;
`; `;
export default Baseline(Divider); export default Baseline(Divider);

View File

@ -1,8 +1,8 @@
### How was the Joyent UI Toolkit built? ### How was the Joyent UI Toolkit built?
The toolkit components were built using The toolkit components were built using
[React](https://facebook.github.io/react/) and [Styled [React](https://facebook.github.io/react/) and
Components](http://styled-components.com). [Styled Components](http://styled-components.com).
### What is the toolkit's license? ### What is the toolkit's license?

View File

@ -19,7 +19,8 @@ const colorWithDefaultValue = props =>
const color = props => const color = props =>
props.defaultValue ? colorWithDefaultValue(props) : colorWithDisabled(props); props.defaultValue ? colorWithDefaultValue(props) : colorWithDisabled(props);
const height = props => (props.multiple ? 'auto' : remcalc(48)); const height = props =>
props.multiple ? 'auto' : props.textarea ? remcalc(96) : remcalc(48);
const paddingTop = props => (props.multiple ? remcalc(20) : remcalc(13)); const paddingTop = props => (props.multiple ? remcalc(20) : remcalc(13));
@ -28,6 +29,7 @@ const style = css`
width: 100%; width: 100%;
height: ${height}; height: ${height};
min-height: ${height};
margin-bottom: ${remcalc(8)}; margin-bottom: ${remcalc(8)};
margin-top: ${remcalc(8)}; margin-top: ${remcalc(8)};
@ -36,19 +38,45 @@ const style = css`
border-radius: ${borderRadius}; border-radius: ${borderRadius};
background-color: ${props => props.theme.white}; background-color: ${props => props.theme.white};
border: ${border.unchecked}; border: ${border.unchecked};
color: ${color};
${is('disabled')` ${is('disabled')`
background-color: ${props => props.theme.disabled};
color: ${props => props.theme.textDisabled};
::-webkit-input-placeholder { /* WebKit, Blink, Edge */ ::-webkit-input-placeholder { /* WebKit, Blink, Edge */
color: ${props => props.theme.placeholder}; color: ${props => props.theme.placeholder};
} }
::-moz-placeholder { /* Mozilla Firefox 19+ */ ::-moz-placeholder { /* Mozilla Firefox 19+ */
color: ${props => props.theme.placeholder}; color: ${props => props.theme.placeholder};
} }
:-ms-input-placeholder { /* Internet Explorer 10-11 */ :-ms-input-placeholder { /* Internet Explorer 10-11 */
color: ${props => props.theme.placeholder}; color: ${props => props.theme.placeholder};
} }
`}; `};
&:disabled {
background-color: ${props => props.theme.disabled};
color: ${props => props.theme.textDisabled};
::-webkit-input-placeholder {
/* WebKit, Blink, Edge */
color: ${props => props.theme.placeholder};
}
::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: ${props => props.theme.placeholder};
}
:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: ${props => props.theme.placeholder};
}
}
${is('error')` ${is('error')`
border-color: ${props => props.theme.redDark} border-color: ${props => props.theme.redDark}
`}; `};
@ -77,13 +105,16 @@ const style = css`
text-overflow: ellipsis; text-overflow: ellipsis;
`}; `};
${is('resize')`
resize: ${props => props.resize};
`};
font-size: ${remcalc(15)}; font-size: ${remcalc(15)};
line-height: normal !important; line-height: normal !important;
${typography.normal}; ${typography.normal};
font-style: normal; font-style: normal;
font-stretch: normal; font-stretch: normal;
color: ${color};
appearance: none; appearance: none;
outline: 0; outline: 0;
@ -94,7 +125,7 @@ const style = css`
} }
`; `;
const BaseInput = Component => props => { const BaseInput = Component => ({ resize, type, ...props }) => {
const render = value => { const render = value => {
const _value = value || {}; const _value = value || {};
const { input = {}, meta = {}, id = '' } = _value; const { input = {}, meta = {}, id = '' } = _value;
@ -103,6 +134,7 @@ const BaseInput = Component => props => {
const hasWarning = Boolean(props.warning || _value.warning || meta.warning); const hasWarning = Boolean(props.warning || _value.warning || meta.warning);
const hasSuccess = Boolean(props.success || _value.success || meta.success); const hasSuccess = Boolean(props.success || _value.success || meta.success);
const textarea = type === 'textarea';
const marginless = Boolean(props.marginless); const marginless = Boolean(props.marginless);
const fluid = Boolean(props.fluid); const fluid = Boolean(props.fluid);
const mono = Boolean(props.mono); const mono = Boolean(props.mono);
@ -118,6 +150,8 @@ const BaseInput = Component => props => {
fluid={fluid} fluid={fluid}
marginless={marginless} marginless={marginless}
mono={mono} mono={mono}
resize={textarea ? resize : null}
textarea={textarea}
/> />
); );
}; };

View File

@ -127,7 +127,9 @@ const ToggleBase = ({ container = null, type = 'radio' }) =>
id: rndId() id: rndId()
}; };
const checked = type === 'checkbox' && rest.value === true; const checked =
['checkbox', 'radio'].indexOf(type) >= 0 &&
(rest.value === true || rest.checked === true);
const toggle = ( const toggle = (
<InnerContainer {...types} type={type}> <InnerContainer {...types} type={type}>

View File

@ -7,6 +7,8 @@ import rndId from 'rnd-id';
import Fieldset from './fieldset'; import Fieldset from './fieldset';
import Baseline from '../baseline'; import Baseline from '../baseline';
const Noop = ({ children }) => children;
class FormGroup extends Component { class FormGroup extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -26,7 +28,7 @@ class FormGroup extends Component {
return ( return (
<Fieldset className={className} style={style}> <Fieldset className={className} style={style}>
<Broadcast channel="input-group" value={value}> <Broadcast channel="input-group" value={value}>
<div>{children}</div> <Noop>{children}</Noop>
</Broadcast> </Broadcast>
</Fieldset> </Fieldset>
); );

View File

@ -2,6 +2,7 @@ export { default as Checkbox, CheckboxList } from './checkbox';
export { default as Fieldset } from './fieldset'; export { default as Fieldset } from './fieldset';
export { default as FormGroup } from './group'; export { default as FormGroup } from './group';
export { default as Input } from './input'; export { default as Input } from './input';
export { default as Textarea } from './textarea';
export { default as FormLabel } from './label'; export { default as FormLabel } from './label';
export { default as Legend } from './legend'; export { default as Legend } from './legend';
export { default as FormMeta } from './meta'; export { default as FormMeta } from './meta';

View File

@ -0,0 +1,42 @@
import React from 'react';
import styled from 'styled-components';
import is from 'styled-is';
import remcalc from 'remcalc';
import PropTypes from 'prop-types';
import Baseline from '../baseline';
import BaseInput, { Stylable } from './base/input';
const TextareaInput = Baseline(BaseInput(Stylable('textarea')));
const BaseTextarea = TextareaInput.extend`
position: relative;
display: inline-flex;
${is('fluid')`
flex: 1 1 auto;
width: 100%;
`};
`;
/**
* @example ./usage-textarea.md
*/
const Textarea = ({ children, fluid, ...rest }) => (
<BaseTextarea {...rest} fluid={fluid} type="textarea">
{children}
</BaseTextarea>
);
export default Textarea;
Textarea.propTypes = {
/**
* Is the Textarea disabled ?
*/
disabled: PropTypes.bool
};
Textarea.defaultProps = {
disabled: false
};

View File

@ -10,11 +10,6 @@ const Brand = H2.extend`
color: ${props => props.theme.white}; color: ${props => props.theme.white};
font-size: ${remcalc(29)}; font-size: ${remcalc(29)};
margin: 0; margin: 0;
${is('beta')`
display: inline-block;
margin-top: ${remcalc(6)};
`};
`; `;
const Box = styled.div` const Box = styled.div`

View File

@ -22,7 +22,6 @@ const Header = styled.div`
background-color: ${props => props.theme.brandBackground}; background-color: ${props => props.theme.brandBackground};
max-height: ${remcalc(53)}; max-height: ${remcalc(53)};
min-height: ${remcalc(53)}; min-height: ${remcalc(53)};
padding-left: ${remcalc(18)};
line-height: ${remcalc(25)}; line-height: ${remcalc(25)};
`; `;
@ -35,6 +34,6 @@ export default ({ children, ...rest }) => (
</Header> </Header>
); );
export { default as HeaderItem } from './item'; export { default as HeaderItem, Anchor as HeaderAnchor } from './item';
export { default as HeaderBrand } from './brand'; export { default as HeaderBrand } from './brand';
export { default as HeaderNav, Anchor as HeaderNavAnchor } from './nav'; export { default as HeaderNav } from './nav';

View File

@ -1,18 +1,35 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import { Link as BaseLink } from 'react-router-dom';
import { A } from 'normalized-styled-components';
import P from '../text/p'; import P from '../text/p';
const style = css`
padding: ${remcalc(15)};
line-height: ${remcalc(24)};
font-size: ${remcalc(15)};
color: ${props => props.theme.white};
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
transition: all 200ms ease;
max-height: ${remcalc(53)};
min-height: ${remcalc(53)};
box-sizing: border-box;
&:hover,
&.active {
background: rgba(255, 255, 255, 0.15);
}
`;
const Text = P.extend` const Text = P.extend`
text-align: center; text-align: center;
color: ${props => props.theme.white}; color: ${props => props.theme.white};
margin: 0; margin: 0;
a {
color: ${props => props.theme.white};
text-decoration: none;
}
`; `;
const Box = styled.section` const Box = styled.section`
@ -21,7 +38,6 @@ const Box = styled.section`
order: 0; order: 0;
display: flex; display: flex;
align-items: center; align-items: center;
padding: ${remcalc(15)};
svg { svg {
margin-right: ${remcalc(6)}; margin-right: ${remcalc(6)};
@ -36,6 +52,25 @@ const Box = styled.section`
} }
`; `;
const StyledAnchor = A.extend`
/* trick prettier */
${style};
`;
const StyledLink = styled(BaseLink)`
/* trick prettier */
${style};
`;
export const Anchor = ({ children, ...rest }) => {
const { to = '' } = rest;
const Views = [() => (to ? StyledLink : null), () => StyledAnchor];
const View = Views.reduce((sel, view) => (sel ? sel : view()), null);
return <View {...rest}>{children}</View>;
};
export default ({ children, ...rest }) => ( export default ({ children, ...rest }) => (
<Box {...rest}> <Box {...rest}>
<Text>{children}</Text> <Text>{children}</Text>

View File

@ -1,53 +1,8 @@
import React from 'react'; import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { Link as BaseLink } from 'react-router-dom';
import { A } from 'normalized-styled-components';
import remcalc from 'remcalc';
const Ul = styled.ul` export default styled.ul`
padding: 0; padding: 0;
margin: 0; margin: 0;
display: flex; display: flex;
list-style: none; list-style: none;
`; `;
const style = css`
padding: ${remcalc(15)};
line-height: ${remcalc(24)};
font-size: ${remcalc(15)};
color: ${props => props.theme.white};
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
transition: all 200ms ease;
max-height: ${remcalc(53)};
min-height: ${remcalc(53)};
box-sizing: border-box;
&:hover,
&.active {
background: rgba(255, 255, 255, 0.15);
}
`;
const StyledAnchor = A.extend`
/* trick prettier */
${style};
`;
const StyledLink = styled(BaseLink)`
/* trick prettier */
${style};
`;
export const Anchor = ({ children, ...rest }) => {
const { to = '' } = rest;
const Views = [() => (to ? StyledLink : null), () => StyledAnchor];
const View = Views.reduce((sel, view) => (sel ? sel : view()), null);
return <View {...rest}>{children}</View>;
};
export default ({ children, ...rest }) => <Ul {...rest}>{children}</Ul>;

View File

@ -2,13 +2,13 @@
const React = require('react'); const React = require('react');
const { default: HeaderBrand } = require('./brand.js'); const { default: HeaderBrand } = require('./brand.js');
const { default: HeaderItem } = require('./item.js'); const { default: HeaderItem } = require('./item.js');
const { default: HeaderNav, HeaderNavAnchor } = require('./nav.js'); const { default: HeaderNav, HeaderAnchor } = require('./nav.js');
<Header> <Header>
<HeaderBrand beta><TritonBetaIcon/></HeaderBrand> <HeaderBrand beta><TritonBetaIcon/></HeaderBrand>
<HeaderNav> <HeaderNav>
<li><HeaderNavAnchor href="#">Compute</HeaderNavAnchor></li> <li><HeaderAnchor href="#">Compute</HeaderAnchor></li>
<li><HeaderNavAnchor href="#" class="active">Network</HeaderNavAnchor></li> <li><HeaderAnchor href="#" class="active">Network</HeaderAnchor></li>
</HeaderNav> </HeaderNav>
<HeaderItem>Return to existing portal</HeaderItem> <HeaderItem>Return to existing portal</HeaderItem>
<HeaderItem><DataCenterIconLight/>eu-east-1</HeaderItem> <HeaderItem><DataCenterIconLight/>eu-east-1</HeaderItem>

View File

@ -42,22 +42,11 @@ export {
} from './breakpoints'; } from './breakpoints';
export { export {
CardDescription, default as Card,
CardHeader, Outlet as CardOutlet,
CardGroupView, Header as CardHeader,
Card, HeaderMeta as CardHeaderMeta,
CardMeta, HeaderBox as CardHeaderBox
CardOptions,
CardOutlet,
CardSubTitle,
CardTitle,
CardAction,
CardView,
CardFooter,
CardLabel,
CardInfo,
CardInfoLabel,
CardInfoIconContainer
} from './card'; } from './card';
export { export {
@ -66,6 +55,7 @@ export {
Fieldset, Fieldset,
FormGroup, FormGroup,
Input, Input,
Textarea,
FormLabel, FormLabel,
Legend, Legend,
FormMeta, FormMeta,
@ -83,7 +73,7 @@ export {
HeaderBrand, HeaderBrand,
HeaderItem, HeaderItem,
HeaderNav, HeaderNav,
HeaderNavAnchor HeaderAnchor
} from './header'; } from './header';
export { export {

View File

@ -54,9 +54,9 @@ export const Message = ({ onCloseClick, children, ...type }) => (
</Container> </Container>
); );
export const MessageTitle = ({ children }) => <H4>{children}</H4>; export const Title = ({ children }) => <H4>{children}</H4>;
export const MessageDescription = ({ children }) => <P>{children}</P>; export const Description = ({ children }) => <P>{children}</P>;
Message.propTypes = { Message.propTypes = {
/** /**

View File

@ -3,5 +3,5 @@ The Joyent UI Toolkit allows anyone designing and building new
prototypes that follow a considered and consistent design direction. prototypes that follow a considered and consistent design direction.
As any style guide and design system, this toolkit is a work in progress, and As any style guide and design system, this toolkit is a work in progress, and
everyone is encouraged to [contribute and improve everyone is encouraged to
it](https://github.com/yldio/joyent-portal/tree/master/packages/ui-toolkit). [contribute and improve it](https://github.com/yldio/joyent-portal/tree/master/packages/ui-toolkit).

View File

@ -1,17 +1,17 @@
Joyent's font is Libre Franklin, which is available to use at [Google Joyent's font is Libre Franklin, which is available to use at
Fonts](https://fonts.google.com/specimen/Libre+Franklin). [Google Fonts](https://fonts.google.com/specimen/Libre+Franklin).
The font sizes in the toolkit are based on an [augmented fourth modular The font sizes in the toolkit are based on an
scale](http://www.modularscale.com/?15,24&px&1.414), with base font size of [augmented fourth modular scale](http://www.modularscale.com/?15,24&px&1.414),
**15px**. with base font size of **15px**.
### Headings ### Headings
Headings are available from `h1` through to `h4`. If demand is shown for `h5` Headings are available from `h1` through to `h4`. If demand is shown for `h5`
and `h6`, these will be included in the toolkit. and `h6`, these will be included in the toolkit.
To learn more about the correct usage of HTML headings, visit [MDN web To learn more about the correct usage of HTML headings, visit
docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements). [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements).
#### Heading 1 #### Heading 1
@ -86,8 +86,8 @@ const Small = require('/').Small;
The `<label>` element is used for captions in the user interface and information The `<label>` element is used for captions in the user interface and information
labels (i.e. text that is not continuous body text). labels (i.e. text that is not continuous body text).
Read more about using the `<label>` element on the [MDN web Read more about using the `<label>` element on the
docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label). [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label).
```jsx ```jsx
const React = require('react'); const React = require('react');
@ -102,8 +102,8 @@ const Label = require('/').Label;
### Anchors ### Anchors
Links in the toolkit are named `Anchor`. This is to avoid confusion with `Link`, Links in the toolkit are named `Anchor`. This is to avoid confusion with `Link`,
which is a [React Router routing which is a
link](http://knowbody.github.io/react-router-docs/api/Link.html). [React Router routing link](http://knowbody.github.io/react-router-docs/api/Link.html).
#### Primary #### Primary

View File

@ -34,7 +34,7 @@ const white = {
const grey = { const grey = {
grey: 'rgb(216, 216, 216)', grey: 'rgb(216, 216, 216)',
greyTransparent: 'rgba(73,73,73, 0.8)' greyTransparent: 'rgba(73, 73, 73, 0.8)'
}; };
const green = { const green = {
@ -62,9 +62,9 @@ export const base = {
...orange, ...orange,
...green, ...green,
...grey, ...grey,
text: 'rgba(73,73,73, 1)', text: 'rgba(73, 73, 73, 1)',
textDisabled: 'rgba(73,73,73, 0.5)', textDisabled: 'rgba(73, 73, 73, 0.5)',
placeholder: 'rgb(99,99,99)', placeholder: 'rgb(99, 99, 99)',
disabled: 'rgb(250, 250, 250)', // used disabled: 'rgb(250, 250, 250)', // used
background: 'rgb(250, 250, 250)' // used background: 'rgb(250, 250, 250)' // used
}; };

View File

@ -62,13 +62,15 @@ const generate = name => css`
font-weight: ${fontFaces[name].weight}; font-weight: ${fontFaces[name].weight};
src: url("${fontFaces[name].filenames.eot}"); src: url("${fontFaces[name].filenames.eot}");
src: src:
url("${fontFaces[name].filenames url("${
.eot}?#iefix") format("embedded-opentype"), fontFaces[name].filenames.eot
}?#iefix") format("embedded-opentype"),
url("${fontFaces[name].filenames.woff}") format("woff"), url("${fontFaces[name].filenames.woff}") format("woff"),
url("${fontFaces[name].filenames.woff2}") format("woff2"), url("${fontFaces[name].filenames.woff2}") format("woff2"),
url("${fontFaces[name].filenames.ttf}") format("truetype"), url("${fontFaces[name].filenames.ttf}") format("truetype"),
url("${fontFaces[name].filenames.svg}#${fontFaces[name] url("${fontFaces[name].filenames.svg}#${
.family}") format("svg"); fontFaces[name].family
}") format("svg");
} }
`; `;

View File

@ -19,21 +19,21 @@
"pascal-case": "^2.0.1", "pascal-case": "^2.0.1",
"path-to-regexp": "^2.1.0", "path-to-regexp": "^2.1.0",
"qs": "^6.5.1", "qs": "^6.5.1",
"react": "^16.0.0", "react": "^16.1.1",
"react-dom": "^16.0.0", "react-dom": "^16.1.1",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-scripts": "1.0.14", "react-scripts": "1.0.17",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-form": "^7.1.1", "redux-form": "^7.1.2",
"styled-components": "^2.2.2" "styled-components": "^2.2.3"
}, },
"devDependencies": { "devDependencies": {
"babel-preset-joyent-portal": "^3.3.3", "babel-preset-joyent-portal": "^3.3.3",
"eslint": "^4.9.0", "eslint": "^4.11.0",
"eslint-config-joyent-portal": "^3.2.0", "eslint-config-joyent-portal": "^3.2.0",
"joyent-react-scripts": "^2.6.0", "joyent-react-scripts": "^3.1.0",
"prettier": "^1.7.4", "prettier": "^1.8.2",
"stylelint": "^8.2.0", "stylelint": "^8.2.0",
"stylelint-config-joyent-portal": "^2.0.1" "stylelint-config-joyent-portal": "^2.0.1"
} }

View File

@ -24,29 +24,29 @@
"lodash.isempty": "^4.4.0", "lodash.isempty": "^4.4.0",
"normalized-styled-components": "^1.0.17", "normalized-styled-components": "^1.0.17",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.0.0", "react": "^16.1.1",
"react-apollo": "^1.4.16", "react-apollo": "^1.4.16",
"react-dom": "^16.0.0", "react-dom": "^16.1.1",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-router": "^4.2.0", "react-router": "^4.2.0",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-styled-flexboxgrid": "^2.1.0", "react-styled-flexboxgrid": "^2.1.1",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-form": "^7.1.1", "redux-form": "^7.1.2",
"remcalc": "^1.0.9", "remcalc": "^1.0.9",
"styled-components": "^2.2.2", "styled-components": "^2.2.3",
"styled-is": "^1.1.0", "styled-is": "^1.1.0",
"unitcalc": "^1.1.1" "unitcalc": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"apr-for-each": "^1.0.6", "apr-for-each": "^1.0.6",
"apr-main": "^1.0.7", "apr-main": "^2.0.2",
"babel-minify-webpack-plugin": "^0.2.0", "babel-minify-webpack-plugin": "^0.2.0",
"babel-plugin-inline-react-svg": "^0.4.0", "babel-plugin-inline-react-svg": "^0.4.0",
"babel-preset-joyent-portal": "^3.3.3", "babel-preset-joyent-portal": "^3.3.3",
"commitizen": "^2.9.6", "commitizen": "^2.9.6",
"cross-env": "^5.1.0", "cross-env": "^5.1.1",
"eslint": "^4.9.0", "eslint": "^4.11.0",
"eslint-config-joyent-portal": "^3.2.0", "eslint-config-joyent-portal": "^3.2.0",
"jest": "^21.2.1", "jest": "^21.2.1",
"jest-alias-preprocessor": "^1.1.1", "jest-alias-preprocessor": "^1.1.1",
@ -57,12 +57,12 @@
"jest-snapshot": "^21.2.1", "jest-snapshot": "^21.2.1",
"jest-styled-components": "^4.9.0", "jest-styled-components": "^4.9.0",
"jest-transform-graphql": "^2.1.0", "jest-transform-graphql": "^2.1.0",
"joyent-react-scripts": "^2.6.0", "joyent-react-scripts": "^3.1.0",
"lodash.sortby": "^4.7.0", "lodash.sortby": "^4.7.0",
"mz": "^2.7.0", "mz": "^2.7.0",
"react-scripts": "^1.0.14", "react-scripts": "^1.0.17",
"react-test-renderer": "^16.0.0", "react-test-renderer": "^16.1.1",
"redrun": "^5.9.18", "redrun": "^5.10.0",
"stylelint": "^8.2.0", "stylelint": "^8.2.0",
"stylelint-config-joyent-portal": "^2.0.1" "stylelint-config-joyent-portal": "^2.0.1"
} }

1238
yarn.lock

File diff suppressed because it is too large Load Diff