feat(sg): bootstrap

This commit is contained in:
Sérgio Ramos 2018-05-23 17:29:04 +01:00 committed by Sérgio Ramos
parent fc84358dff
commit 063e40859d
180 changed files with 8479 additions and 3577 deletions

View File

@ -5,5 +5,5 @@ coverage
dist
styleguide
build
lib/app
consoles/*/lib/app
node_modules

View File

@ -18,12 +18,14 @@
"execa": "^0.10.0",
"graphi": "^5.7.0",
"h2o2": "^8.1.2",
"hapi": "^17.4.0",
"hapi": "^17.5.0",
"hapi-triton-auth": "^3.0.0",
"hapi-webconsole-nav": "^2.1.0",
"my-joy-images": "*",
"my-joy-instances": "*",
"my-joy-navigation": "*",
"my-joy-service-groups": "*",
"my-joy-templates": "*",
"tsg-graphql": "^1.0.0"
}
}

View File

@ -7,6 +7,7 @@ const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-service-groups');
const {
PORT = 4004,
@ -67,6 +68,9 @@ Main(async () => {
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);

View File

@ -7,6 +7,7 @@ const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-templates');
const {
PORT = 4005,
@ -67,6 +68,9 @@ Main(async () => {
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);

View File

@ -6,7 +6,7 @@
"repository": "github:yldio/joyent-portal",
"main": "lib/index.js",
"scripts": {
"dev": "NAMESPACE=images NODE_ENV=development REACT_APP_GQL_PORT=4000 PORT=3070 joyent-react-scripts start",
"dev": "REACT_APP_DEV=1 NAMESPACE=images NODE_ENV=development REACT_APP_GQL_PORT=4000 PORT=3070 joyent-react-scripts start",
"build:test": "echo 0",
"build:lib": "echo 0",
"build:bundle": "NAMESPACE=images NODE_ENV=production redrun -p build:frontend build:ssr",
@ -19,23 +19,23 @@
"dependencies": {
"@manaflair/redux-batch": "^0.1.0",
"apollo": "^0.2.2",
"apollo-cache-inmemory": "^1.1.12",
"apollo-client": "^2.2.8",
"apollo-link-http": "^1.5.3",
"apollo-cache-inmemory": "^1.2.2",
"apollo-client": "^2.3.2",
"apollo-link-http": "^1.5.4",
"apr-intercept": "^3.0.3",
"apr-reduce": "^3.0.3",
"boom": "^7.2.0",
"cross-fetch": "^2.1.0",
"cross-fetch": "^2.2.0",
"date-fns": "^1.29.0",
"declarative-redux-form": "^2.0.8",
"exenv": "^1.2.2",
"force-array": "^3.1.0",
"fuse.js": "^3.2.0",
"hapi-render-react": "^2.5.2",
"hapi-render-react-joyent-document": "^7.1.0",
"hapi-render-react-joyent-document": "^7.2.0",
"inert": "^5.1.0",
"joyent-logo-assets": "^1.1.0",
"joyent-react-styled-flexboxgrid": "^2.2.3",
"joyent-react-styled-flexboxgrid": "^3.1.0",
"joyent-ui-resource-widgets": "^1.0.0",
"joyent-ui-toolkit": "^6.0.0",
"lodash.assign": "^4.2.0",
@ -46,37 +46,37 @@
"lodash.keys": "^4.2.0",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"lunr": "^2.1.6",
"lunr": "^2.2.1",
"mz": "^2.7.0",
"param-case": "^2.1.1",
"react": "^16.3.1",
"react-apollo": "^2.1.2",
"react-dom": "^16.3.1",
"react-helmet-async": "0.0.5",
"react": "^16.4.0",
"react-apollo": "^2.1.4",
"react-dom": "^16.4.0",
"react-helmet-async": "0.1.0",
"react-redux": "^5.0.7",
"react-redux-values": "^1.1.2",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux": "^4.0.0",
"redux-form": "^7.3.0",
"remcalc": "^1.0.10",
"styled-components": "^3.2.5",
"styled-components-spacing": "^2.1.3",
"styled-components": "^3.3.0",
"styled-components-spacing": "^3.0.0",
"styled-flex-component": "^2.2.2",
"styled-is": "^1.1.2",
"styled-is": "^1.1.3",
"title-case": "^2.1.1",
"yup": "^0.24.1"
"yup": "^0.25.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-joyent-portal": "^7.0.1",
"eslint": "^4.19.1",
"eslint-config-joyent-portal": "^3.3.1",
"jest-image-snapshot": "^2.4.0",
"jest-image-snapshot": "^2.4.2",
"jest-styled-components": "^5.0.1",
"joyent-react-scripts": "^8.2.0",
"joyent-react-scripts": "^8.2.1",
"react-screenshot-renderer": "^1.1.2",
"react-test-renderer": "^16.3.1",
"redrun": "^6.0.2"
"react-test-renderer": "^16.4.0",
"redrun": "^6.0.4"
}
}

View File

@ -2,7 +2,6 @@ import React from 'react';
import { Field } from 'redux-form';
import { Margin } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component';
import remcalc from 'remcalc';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import {
@ -31,7 +30,6 @@ export default ({ placeholderName, randomizing, onRandomize }) => (
<Margin left="1">
<Button
type="button"
marginTop={remcalc(8)}
onClick={onRandomize}
loading={randomizing}
marginless
@ -54,7 +52,7 @@ export default ({ placeholderName, randomizing, onRandomize }) => (
</FormGroup>
</Margin>
<Row>
<Col xs={12} sm={8}>
<Col xs="12" sm="8">
<Margin top="3">
<FormGroup name="description" fluid field={Field}>
<FormLabel>Description</FormLabel>

View File

@ -5,7 +5,7 @@ import { P } from 'joyent-ui-toolkit';
export default ({ children }) => (
<Row>
<Col xs={12} sm={8}>
<Col xs="12" sm="8">
<Margin bottom="3">
<P>{children}</P>
</Margin>

View File

@ -17,7 +17,7 @@ const FullWidthCard = styled(Card)`
export default ({ children }) => (
<FullWidthCard>
<Padding all={6}>
<Padding all="6">
<Flex alignCenter justifyCenter column>
<Margin bottom="2">
<EmptyState />

View File

@ -88,7 +88,7 @@ export const Image = ({
<CardAnchor to={`/images/${id}`} component={Link}>
<Card radius>
{removing ? (
<Padding all={2}>
<Padding all="2">
<StatusLoader />
</Padding>
) : (

View File

@ -106,13 +106,13 @@ export const Meta = ({ name, version, type, published_at, state, os }) => (
export default ({ theme = {}, onRemove, removing, ...image }) => (
<Row>
<Col xs={12} sm={12} md={9}>
<Col xs="12" sm="12" md="9">
<Card>
<CardOutlet>
<Padding all={5}>
<Padding all="5">
<Meta {...image} />
<Row between="xs">
<Col xs={9}>
<Col xs="9">
<SmallOnly>
<Button type="button" small icon>
<DuplicateIcon light />
@ -133,7 +133,7 @@ export default ({ theme = {}, onRemove, removing, ...image }) => (
</Button>
</Medium>
</Col>
<Col xs={3}>
<Col xs="3">
<SmallOnly>
<Button type="button" small icon error right>
<DeleteIcon fill="red" />
@ -170,7 +170,7 @@ export default ({ theme = {}, onRemove, removing, ...image }) => (
<CopiableField text={image.id} label="UUID" />
</Margin>
<Row>
<Col xs={12} md={7}>
<Col xs="12" md="7">
<Margin bottom="3">
<FormLabel>Operating system</FormLabel>
<Input

View File

@ -14,8 +14,8 @@ export const EditForm = props => (
export default ({ norMargin, name, value, onClick, onRemoveClick, active }) => (
<Margin
right={norMargin ? 0 : 1}
bottom={norMargin ? 0 : 1}
right={norMargin ? '0' : '1'}
bottom={norMargin ? '0' : '1'}
key={`${name}-${value}`}
>
<TagItem onClick={onClick} active={active} onRemoveClick={onRemoveClick}>

View File

@ -80,7 +80,7 @@ const NameContainer = ({
) : null}
{description ? (
<Row>
<Col xs={12} sm={8}>
<Col xs="12" sm="8">
<Margin top="1">
<P>{description}</P>
</Margin>

View File

@ -76,7 +76,7 @@ const Create = ({
{({ handleSubmit, submitting }) =>
!loading && !loadingError ? (
<form onSubmit={handleSubmit}>
<Margin top={step === 'tag' ? 7 : 4}>
<Margin top={step === 'tag' ? '7' : '4'}>
<Button disabled={disabled} loading={submitting}>
Create Image
</Button>

View File

@ -78,7 +78,7 @@ export const List = ({
</Margin>
<Row>
{images.map(image => (
<Col sm={4}>
<Col sm="4">
<Image
{...image}
onCreateInstance={() => handleCreateInstance(image)}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { Fragment } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import get from 'lodash.get';
@ -19,6 +19,8 @@ import Create from '@containers/create';
import Tags from '@containers/tags';
import { Route as ServerError } from '@root/server-error';
const { REACT_APP_DEV = false } = process.env;
export default () => (
<PageContainer>
{/* Breadcrumb */}
@ -70,6 +72,41 @@ export default () => (
<Route path="/" exact component={() => <Redirect to="/images" />} />
{REACT_APP_DEV ? (
<Fragment>
<Route
path="/instances"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3069${
location.pathname
}${location.search}`
)
}
/>
<Route
path="/templates"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3071${
location.pathname
}${location.search}`
)
}
/>
<Route
path="/service-groups"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3072${
location.pathname
}${location.search}`
)
}
/>
</Fragment>
) : null}
<noscript>
<ViewContainer main>
<Message warning>

View File

@ -2,6 +2,7 @@ import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import fetch from 'cross-fetch';
import get from 'lodash.get';
import global from './global';
@ -14,7 +15,9 @@ const {
const PORT = REACT_APP_GQL_PORT ? `:${REACT_APP_GQL_PORT}` : '';
const URI = `${REACT_APP_GQL_PROTOCOL}://${REACT_APP_GQL_HOSTNAME}${PORT}/images/graphql`;
export default (opts = {}) => {
export default (opts = {}, request = {}) => {
const host = get(request, 'raw.req.headers.host', '');
let cache = new InMemoryCache();
if (global.__APOLLO_STATE__) {
@ -24,7 +27,7 @@ export default (opts = {}) => {
return new ApolloClient({
cache,
link: new HttpLink({
uri: URI,
uri: host ? `${REACT_APP_GQL_PROTOCOL}//${host}/images/graphql` : URI,
credentials: 'same-origin',
fetch,
headers: {

View File

@ -1,8 +1,12 @@
import { canUseDOM } from 'exenv';
import queryString from 'query-string';
export default (() => {
const { NODE_ENV = 'development' } = process.env;
export const Global = () => {
if (!canUseDOM) {
return {
protocol: NODE_ENV === 'development' ? 'http:' : 'https:',
cookie: ''
};
}
@ -11,10 +15,15 @@ export default (() => {
port: window.location.port,
protocol: window.location.protocol.replace(/:$/, ''),
hostname: window.location.hostname,
pathname: window.location.pathname,
origin: window.location.origin,
cookie: document.cookie || '',
search: window.location.search,
query: queryString.parse(window.location.search || ''),
__REDUX_DEVTOOLS_EXTENSION__: window.__REDUX_DEVTOOLS_EXTENSION__,
__APOLLO_STATE__: window.__APOLLO_STATE__,
__REDUX_STATE__: window.__REDUX_STATE__
};
})();
};
export default Global();

View File

@ -2,7 +2,7 @@ import intercept from 'apr-intercept';
import keys from 'lodash.keys';
import reduce from 'apr-reduce';
import assign from 'lodash.assign';
import yup from 'yup';
import * as yup from 'yup';
/*****************************************************************************/

View File

@ -6,7 +6,7 @@
"repository": "github:yldio/joyent-portal",
"main": "lib/index.js",
"scripts": {
"dev": "NAMESPACE=instances NODE_ENV=development REACT_APP_GQL_PORT=4000 PORT=3069 joyent-react-scripts start",
"dev": "REACT_APP_DEV=1 NAMESPACE=instances NODE_ENV=development REACT_APP_GQL_PORT=4000 PORT=3069 joyent-react-scripts start",
"build:test": "echo 0",
"build:lib": "echo 0",
"build:bundle": "NAMESPACE=instances NODE_ENV=production redrun -p build:frontend build:ssr",
@ -18,26 +18,26 @@
},
"dependencies": {
"@manaflair/redux-batch": "^0.1.0",
"apollo-cache-inmemory": "^1.1.12",
"apollo-client": "^2.2.8",
"apollo-link-http": "^1.5.3",
"apollo-cache-inmemory": "^1.2.2",
"apollo-client": "^2.3.2",
"apollo-link-http": "^1.5.4",
"apr-intercept": "^3.0.3",
"apr-reduce": "^3.0.3",
"boom": "^7.2.0",
"bytes": "^3.0.0",
"clipboard-copy": "^2.0.0",
"cross-fetch": "^2.1.0",
"cross-fetch": "^2.2.0",
"date-fns": "^1.29.0",
"declarative-redux-form": "^2.0.8",
"exenv": "^1.2.2",
"fuse.js": "^3.2.0",
"hapi-render-react": "^2.5.2",
"hapi-render-react-joyent-document": "^7.1.0",
"hapi-render-react-joyent-document": "^7.2.0",
"inert": "^5.1.0",
"joyent-logo-assets": "^1.1.0",
"joyent-ui-resource-step": "^1.0.0",
"joyent-manifest-editor": "^1.4.0",
"joyent-react-styled-flexboxgrid": "^2.2.3",
"joyent-react-styled-flexboxgrid": "^3.1.0",
"joyent-ui-toolkit": "^6.0.0",
"lodash.find": "^4.6.0",
"lodash.findindex": "^4.6.0",
@ -56,35 +56,36 @@
"lodash.some": "^4.6.0",
"lodash.sortby": "^4.7.0",
"lodash.values": "^4.3.0",
"mz": "^2.7.0",
"param-case": "^2.1.1",
"query-string": "^6.1.0",
"react": "^16.3.1",
"react-apollo": "^2.1.2",
"react-dom": "^16.3.1",
"react-helmet-async": "0.0.5",
"react": "^16.4.0",
"react-apollo": "^2.1.4",
"react-dom": "^16.4.0",
"react-helmet-async": "0.1.0",
"react-redux": "^5.0.7",
"react-redux-values": "^1.1.2",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux": "^4.0.0",
"redux-form": "^7.3.0",
"remcalc": "^1.0.10",
"styled-components": "^3.2.5",
"styled-components-spacing": "^2.1.3",
"styled-components": "^3.3.0",
"styled-components-spacing": "^3.0.0",
"styled-flex-component": "^2.2.2",
"title-case": "^2.1.1",
"yup": "^0.24.1"
"yup": "^0.25.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-joyent-portal": "^7.0.1",
"eslint": "^4.19.1",
"eslint-config-joyent-portal": "^3.3.1",
"jest-image-snapshot": "^2.4.0",
"jest-image-snapshot": "^2.4.2",
"jest-styled-components": "^5.0.1",
"joyent-react-scripts": "^8.2.0",
"joyent-react-scripts": "^8.2.1",
"react-screenshot-renderer": "^1.1.2",
"react-test-renderer": "^16.3.1",
"redrun": "^6.0.2"
"react-test-renderer": "^16.4.0",
"redrun": "^6.0.4"
}
}

View File

@ -203,7 +203,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 2rem;
display: table-cell;
@ -326,7 +326,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 3.75rem;
display: table-cell;
@ -587,9 +587,14 @@ Array [
className="c4"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -924,7 +929,7 @@ exports[`renders <Item /> without throwing 1`] = `
font: inherit;
}
.c9 {
.c8 {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
@ -933,7 +938,7 @@ exports[`renders <Item /> without throwing 1`] = `
margin-left: 0rem;
}
.c12 {
.c11 {
cursor: pointer;
height: 100%;
width: 100%;
@ -1081,7 +1086,7 @@ exports[`renders <Item /> without throwing 1`] = `
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
vertical-align: middle;
text-align: left;
@ -1096,29 +1101,7 @@ exports[`renders <Item /> without throwing 1`] = `
border-right-width: 0;
}
.c8 {
border-width: 0.0625rem;
border-style: solid;
border-color: rgb(216,216,216);
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 1.5rem;
height: 3.75rem;
vertical-align: middle;
text-align: left;
border-bottom-width: 0;
}
.c8:not(:first-child) {
border-left-width: 0;
}
.c8:not(:last-child) {
border-right-width: 0;
}
.c10 {
.c9 {
border-width: 0.0625rem;
border-style: solid;
border-color: rgb(216,216,216);
@ -1134,32 +1117,32 @@ exports[`renders <Item /> without throwing 1`] = `
border-bottom-width: 0;
}
.c10:not(:first-child) {
.c9:not(:first-child) {
border-left-width: 0;
}
.c10:not(:last-child) {
.c9:not(:last-child) {
border-right-width: 0;
}
.c11 {
.c10 {
border-width: 0.0625rem;
border-style: solid;
border-color: rgb(216,216,216);
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
border-bottom-width: 0;
border-left-width: 0.0625rem !important;
}
.c11:not(:first-child) {
.c10:not(:first-child) {
border-left-width: 0;
}
.c11:not(:last-child) {
.c10:not(:last-child) {
border-right-width: 0;
}
@ -1180,7 +1163,7 @@ exports[`renders <Item /> without throwing 1`] = `
}
@media only screen and (min-width:37.4375rem) {
.c10 {
.c9 {
width: 10rem;
display: table-cell;
}
@ -1232,19 +1215,19 @@ exports[`renders <Item /> without throwing 1`] = `
</div>
</td>
<td
className="c8"
className="c1"
disabled={undefined}
name="td"
selected={undefined}
/>
<td
className="c8"
className="c1"
disabled={undefined}
name="td"
selected={undefined}
>
<span
className="c9"
className="c8"
color={undefined}
size="0.75rem"
/>
@ -1252,7 +1235,7 @@ exports[`renders <Item /> without throwing 1`] = `
</td>
<td
className="c10"
className="c9"
disabled={undefined}
name="td"
selected={undefined}
@ -1260,14 +1243,14 @@ exports[`renders <Item /> without throwing 1`] = `
almost NaN years
</td>
<td
className="c11"
className="c10"
disabled={undefined}
name="td"
selected={undefined}
>
<div
box={true}
className="c12"
className="c11"
onClick={[Function]}
onMouseEnter={undefined}
onMouseLeave={undefined}
@ -1340,7 +1323,7 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
font: inherit;
}
.c9 {
.c8 {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
@ -1349,7 +1332,7 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
margin-left: 0rem;
}
.c12 {
.c11 {
cursor: pointer;
height: 100%;
width: 100%;
@ -1497,7 +1480,7 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
vertical-align: middle;
text-align: left;
@ -1512,29 +1495,7 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
border-right-width: 0;
}
.c8 {
border-width: 0.0625rem;
border-style: solid;
border-color: rgb(216,216,216);
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 1.5rem;
height: 3.75rem;
vertical-align: middle;
text-align: left;
border-bottom-width: 0;
}
.c8:not(:first-child) {
border-left-width: 0;
}
.c8:not(:last-child) {
border-right-width: 0;
}
.c10 {
.c9 {
border-width: 0.0625rem;
border-style: solid;
border-color: rgb(216,216,216);
@ -1550,32 +1511,32 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
border-bottom-width: 0;
}
.c10:not(:first-child) {
.c9:not(:first-child) {
border-left-width: 0;
}
.c10:not(:last-child) {
.c9:not(:last-child) {
border-right-width: 0;
}
.c11 {
.c10 {
border-width: 0.0625rem;
border-style: solid;
border-color: rgb(216,216,216);
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
border-bottom-width: 0;
border-left-width: 0.0625rem !important;
}
.c11:not(:first-child) {
.c10:not(:first-child) {
border-left-width: 0;
}
.c11:not(:last-child) {
.c10:not(:last-child) {
border-right-width: 0;
}
@ -1596,7 +1557,7 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
}
@media only screen and (min-width:37.4375rem) {
.c10 {
.c9 {
width: 10rem;
display: table-cell;
}
@ -1648,7 +1609,7 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
</div>
</td>
<td
className="c8"
className="c1"
disabled={undefined}
name="td"
selected={undefined}
@ -1656,13 +1617,13 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
name
</td>
<td
className="c8"
className="c1"
disabled={undefined}
name="td"
selected={undefined}
>
<span
className="c9"
className="c8"
color={undefined}
size="0.75rem"
/>
@ -1670,7 +1631,7 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
Started
</td>
<td
className="c10"
className="c9"
disabled={undefined}
name="td"
selected={undefined}
@ -1678,14 +1639,14 @@ exports[`renders <Item {...item} /> without throwing 1`] = `
6 months
</td>
<td
className="c11"
className="c10"
disabled={undefined}
name="td"
selected={undefined}
>
<div
box={true}
className="c12"
className="c11"
onClick={[Function]}
onMouseEnter={undefined}
onMouseLeave={undefined}
@ -2146,7 +2107,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 2rem;
display: table-cell;
@ -2269,7 +2230,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 3.75rem;
display: table-cell;
@ -2530,9 +2491,14 @@ Array [
className="c4"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -3029,7 +2995,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 2rem;
display: table-cell;
@ -3152,7 +3118,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 3.75rem;
display: table-cell;
@ -3413,9 +3379,14 @@ Array [
className="c4"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -3912,7 +3883,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 2rem;
display: table-cell;
@ -3971,7 +3942,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 3.75rem;
display: table-cell;
@ -4296,9 +4267,14 @@ Array [
className="c4"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -4795,7 +4771,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 2rem;
display: table-cell;
@ -4854,7 +4830,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 3.75rem;
display: table-cell;
@ -5179,9 +5155,14 @@ Array [
className="c4"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -5678,7 +5659,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 2rem;
display: table-cell;
@ -5801,7 +5782,7 @@ Array [
border-spacing: 0;
white-space: nowrap;
box-sizing: border-box;
padding: 0 0rem;
padding: 0 1.5rem;
height: 3.75rem;
width: 3.75rem;
display: table-cell;
@ -6062,9 +6043,14 @@ Array [
className="c4"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>

View File

@ -1294,6 +1294,11 @@ exports[`renders <Summary /> without throwing 1`] = `
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"
@ -2905,6 +2910,11 @@ exports[`renders <Summary instance /> without throwing 1`] = `
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"
@ -4833,6 +4843,11 @@ exports[`renders <Summary instance /> without throwing 2`] = `
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"
@ -7042,6 +7057,11 @@ exports[`renders <Summary starting stopping rebooting removing /> without throwi
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"
@ -8756,6 +8776,11 @@ exports[`renders <Summary state /> without throwing 1`] = `
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"
@ -10446,6 +10471,11 @@ exports[`renders <Summary state /> without throwing 2`] = `
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"
@ -12298,6 +12328,11 @@ exports[`renders <Summary state /> without throwing 3`] = `
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"

View File

@ -9,7 +9,7 @@ const P = styled(BaseP)`
export default ({ href = '', children }) => (
<Row>
<Col xs={12} sm={7}>
<Col xs="12" sm="7">
<P>
{children}{' '}
{href ? (

View File

@ -26,7 +26,7 @@ export default ({
}) => (
<StickyFooter fill="#FAFAFA" fixed bottom>
<Row between="xs" middle="xs">
<Col xs={7}>
<Col xs="7">
<Flex>
{onStart && [
<SmallOnly key="small-only">
@ -125,7 +125,7 @@ export default ({
</Flex>
</Col>
{onRemove && (
<Col xs={5}>
<Col xs="5">
<SmallOnly key="small-only">
<Button
type="button"

View File

@ -98,8 +98,8 @@ export const Item = ({
onClick
}) => (
<TableTr>
<TableTd padding="0" paddingLeft={remcalc(12)} middle left>
<FormGroup name={id} paddingTop={remcalc(4)} field={Field}>
<TableTd middle left>
<FormGroup name={id} field={Field}>
<Checkbox noMargin />
</FormGroup>
</TableTd>
@ -125,12 +125,12 @@ export const Item = ({
<code>{id.substring(0, 7)}</code>
</TableTd>
{mutating ? (
<TableTd padding="0" hasBorder="left" center middle>
<TableTd hasBorder="left" center middle>
<ActionsIcon disabled />
</TableTd>
) : (
<PopoverContainer clickable>
<TableTd padding="0" hasBorder="left">
<TableTd hasBorder="left">
<PopoverTarget box>
<Actions alignCenter justifyCenter>
<ActionsIcon />
@ -149,7 +149,7 @@ export const Item = ({
<PopoverItem disabled={!allowedActions.remove} onClick={onRemove}>
Delete
</PopoverItem>
<Padding bottom={2}>
<Padding bottom="2">
<PopoverDivider />
</Padding>
<PopoverItem disabled={false} onClick={onCreateImage}>
@ -191,8 +191,8 @@ export default ({
<Table>
<TableThead>
<TableTr>
<TableTh xs="32" padding="0" paddingLeft={remcalc(12)} middle left>
<FormGroup paddingTop={remcalc(4)}>
<TableTh xs="32" middle left>
<FormGroup>
<Checkbox
checked={allSelected}
disabled={submitting || noInstances}
@ -246,7 +246,7 @@ export default ({
>
<span>Short ID </span>
</TableTh>
<TableTh xs="60" padding="0" />
<TableTh xs="60" />
</TableTr>
</TableThead>
<TableTbody>{children}</TableTbody>

View File

@ -43,8 +43,8 @@ export const Item = ({ name, state, created, onStart, onRemove, mutating }) => (
</TableTd>
) : (
<Fragment>
<TableTd padding="0" paddingLeft={remcalc(12)} middle left>
<FormGroup paddingTop={remcalc(4)} name={name} field={Field}>
<TableTd middle left>
<FormGroup name={name} field={Field}>
<Checkbox noMargin />
</FormGroup>
</TableTd>
@ -59,7 +59,7 @@ export const Item = ({ name, state, created, onStart, onRemove, mutating }) => (
{distanceInWordsToNow(created)}
</TableTd>
<PopoverContainer clickable>
<TableTd padding="0" hasBorder="left">
<TableTd hasBorder="left">
<PopoverTarget box>
<ActionsIcon />
</PopoverTarget>
@ -111,8 +111,8 @@ export default ({
<Table>
<TableThead>
<TableTr>
<TableTh xs="32" padding="0" paddingLeft={remcalc(12)} middle left>
<FormGroup paddingTop={remcalc(4)}>
<TableTh xs="32" middle left>
<FormGroup>
<Checkbox
checked={allSelected}
disabled={submitting}
@ -154,7 +154,7 @@ export default ({
>
<span>Created </span>
</TableTh>
<TableTh xs="60" padding="0" />
<TableTh xs="60" />
</TableTr>
</TableThead>
<TableTbody>

View File

@ -99,7 +99,7 @@ export const Meta = ({
...instance
}) => [
<Row middle="xs">
<Col xs={12}>
<Col xs="12">
<Margin bottom="1">
<H2>
{editingName ? (
@ -129,7 +129,7 @@ export const Meta = ({
) : (
<Flex>
{instance.name}
<Actionable left={2} onClick={editName}>
<Actionable left="2" onClick={editName}>
<EditIcon />
</Actionable>
</Flex>
@ -187,14 +187,14 @@ export default ({
...props
}) => (
<Row>
<Col xs={12} sm={12} md={9}>
<Col xs="12" sm="12" md="9">
<Card>
<CardOutlet>
<Padding all={5}>
<Padding all="5">
<Meta {...instance} {...props} />
<Margin top="3">
<Row between="xs">
<Col xs={9}>
<Col xs="9">
<Flex>
<FlexItem>
<Margin right="1">
@ -283,7 +283,7 @@ export default ({
</FlexItem>
</Flex>
</Col>
<Col xs={3}>
<Col xs="3">
<SmallOnly>
<Button
type="button"

View File

@ -260,6 +260,18 @@ exports[`renders <Cns /> without throwing 1`] = `
background-color: transparent;
}
.c25 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
@ -371,18 +383,6 @@ exports[`renders <Cns /> without throwing 1`] = `
display: block;
}
.c25 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c29 {
box-sizing: border-box;
width: 18.75rem;
@ -1013,6 +1013,18 @@ exports[`renders <Cns disabled /> without throwing 1`] = `
background-color: transparent;
}
.c12 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c13 {
display: -webkit-box;
display: -webkit-flex;
@ -1037,18 +1049,6 @@ exports[`renders <Cns disabled /> without throwing 1`] = `
align-items: center;
}
.c12 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c14 {
font-weight: 600;
white-space: pre;
@ -1669,6 +1669,18 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
background-color: transparent;
}
.c36 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
@ -1780,18 +1792,6 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
display: block;
}
.c36 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c39 {
box-sizing: border-box;
width: 18.75rem;
@ -3701,6 +3701,18 @@ exports[`renders <Cns loadingError /> without throwing 1`] = `
background-color: transparent;
}
.c30 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c21 {
display: -webkit-box;
display: -webkit-flex;
@ -3788,18 +3800,6 @@ exports[`renders <Cns loadingError /> without throwing 1`] = `
display: block;
}
.c30 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c34 {
box-sizing: border-box;
width: 18.75rem;
@ -4511,6 +4511,18 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
background-color: transparent;
}
.c36 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
@ -4622,18 +4634,6 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
display: block;
}
.c36 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c28 {
box-sizing: border-box;
width: 18.75rem;
@ -6356,6 +6356,18 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
background-color: transparent;
}
.c30 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c21 {
display: -webkit-box;
display: -webkit-flex;
@ -6467,18 +6479,6 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
display: block;
}
.c30 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c34 {
box-sizing: border-box;
width: 18.75rem;
@ -7308,6 +7308,18 @@ exports[`renders <Cns services /> without throwing 1`] = `
background-color: transparent;
}
.c25 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c16 {
display: -webkit-box;
display: -webkit-flex;
@ -7419,18 +7431,6 @@ exports[`renders <Cns services /> without throwing 1`] = `
display: block;
}
.c25 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c29 {
box-sizing: border-box;
width: 18.75rem;
@ -8214,6 +8214,18 @@ exports[`renders <Cns services hostnames /> without throwing 1`] = `
background-color: transparent;
}
.c12 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c13 {
display: -webkit-box;
display: -webkit-flex;
@ -8238,18 +8250,6 @@ exports[`renders <Cns services hostnames /> without throwing 1`] = `
align-items: center;
}
.c12 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c14 {
font-weight: 600;
white-space: pre;

View File

@ -551,9 +551,14 @@ Array [
className="c26"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -1452,9 +1457,14 @@ Array [
className="c26"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -2301,9 +2311,14 @@ Array [
className="c26"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -3202,9 +3217,14 @@ Array [
className="c26"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -4681,9 +4701,14 @@ exports[`renders <Firewall loadingError /> without throwing 1`] = `
className="c32"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -5594,9 +5619,14 @@ exports[`renders <Firewall mutationError /> without throwing 1`] = `
className="c32"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>

View File

@ -194,6 +194,31 @@ exports[`renders <Metadata /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -222,31 +247,6 @@ exports[`renders <Metadata /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -1139,6 +1139,44 @@ exports[`renders <Metadata addOpen /> without throwing 1`] = `
color: inherit;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c35 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -1195,44 +1233,6 @@ exports[`renders <Metadata addOpen /> without throwing 1`] = `
align-items: center;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c35 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -1995,6 +1995,31 @@ exports[`renders <Metadata error /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -2023,31 +2048,6 @@ exports[`renders <Metadata error /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -2544,6 +2544,31 @@ exports[`renders <Metadata loading /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -2572,31 +2597,6 @@ exports[`renders <Metadata loading /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -3710,6 +3710,44 @@ exports[`renders <Metadata metadata /> without throwing 1`] = `
color: inherit;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c35 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -3766,44 +3804,6 @@ exports[`renders <Metadata metadata /> without throwing 1`] = `
align-items: center;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c35 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;

View File

@ -246,9 +246,14 @@ exports[`renders <Networks /> without throwing 1`] = `
className="c12"
>
<svg
height={109}
height="109"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 119.34 109.47"
width={119}
width="119"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
@ -1642,6 +1647,11 @@ exports[`renders <Networks networks /> without throwing 1`] = `
>
<svg
height="16.2"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 16.2 16.2"
width="16.2"
xmlns="http://www.w3.org/2000/svg"
@ -2175,6 +2185,11 @@ exports[`renders <Networks networks /> without throwing 1`] = `
>
<svg
height="13"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 9 13"
width="9"
xmlns="http://www.w3.org/2000/svg"

View File

@ -2006,6 +2006,11 @@ exports[`renders <Summary starting stopping rebooting removing /> without throwi
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"
@ -3989,6 +3994,11 @@ exports[`renders <Summary starting stopping rebooting removing /> without throwi
>
<svg
height="17.07"
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 17.07 17.07"
width="17.07"
xmlns="http://www.w3.org/2000/svg"

View File

@ -194,6 +194,31 @@ exports[`renders <Tags /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -222,31 +247,6 @@ exports[`renders <Tags /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -1143,6 +1143,44 @@ exports[`renders <Tags addOpen /> without throwing 1`] = `
color: inherit;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c31 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -1249,44 +1287,6 @@ exports[`renders <Tags addOpen /> without throwing 1`] = `
flex-basis: auto;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c31 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -1941,6 +1941,31 @@ exports[`renders <Tags editable /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -1969,31 +1994,6 @@ exports[`renders <Tags editable /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -2919,6 +2919,44 @@ exports[`renders <Tags editing /> without throwing 1`] = `
color: inherit;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c33 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -3025,44 +3063,6 @@ exports[`renders <Tags editing /> without throwing 1`] = `
flex-basis: auto;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c33 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -4348,6 +4348,44 @@ exports[`renders <Tags editing.removing /> without throwing 1`] = `
color: inherit;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c33 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -4454,44 +4492,6 @@ exports[`renders <Tags editing.removing /> without throwing 1`] = `
flex-basis: auto;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c33 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
width: 100%;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -5195,6 +5195,31 @@ exports[`renders <Tags error /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -5223,31 +5248,6 @@ exports[`renders <Tags error /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -5684,6 +5684,31 @@ exports[`renders <Tags loading /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -5712,31 +5737,6 @@ exports[`renders <Tags loading /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;
@ -6156,6 +6156,31 @@ exports[`renders <Tags tags /> without throwing 1`] = `
background-color: transparent;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
@ -6184,31 +6209,6 @@ exports[`renders <Tags tags /> without throwing 1`] = `
align-items: flex-end;
}
.c4 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
}
.c9 {
display: inline-block;
padding: 0;
border: none;
overflow: hidden;
height: auto;
-webkit-padding-before: 0;
-webkit-padding-start: 0;
-webkit-padding-end: 0;
-webkit-padding-after: 0;
float: right;
}
.c8 {
box-sizing: border-box;
width: 18.75rem;

View File

@ -61,10 +61,10 @@ const CnsContainer = ({
</Description>
</Margin>
<Row>
<Col xs={12} sm={12} md={9}>
<Col xs="12" sm="12" md="9">
<Card>
<CardOutlet>
<Padding all={5}>
<Padding all="5">
{loading ? <StatusLoader /> : null}
{!loading && loadingError ? (
<Margin bottom="5">

View File

@ -365,13 +365,13 @@ export default compose(
handleAction: async ({ selected, name }) => {
// eslint-disable-next-line no-alert
if (
!(await Confirm(
!await Confirm(
`Do you want to ${name} ${
selected.length === 1
? `"${selected[0].name}"`
: `${selected.length} instances`
}`
))
)
) {
return;
}

View File

@ -316,7 +316,7 @@ export default compose(
'initialValues.name'
);
if (!(await Confirm(`Do you want to remove "${name}"?`))) {
if (!await Confirm(`Do you want to remove "${name}"?`)) {
return;
}

View File

@ -349,13 +349,13 @@ export default compose(
handleAction: async ({ name, selected = [] }) => {
// eslint-disable-next-line no-alert
if (
!(await Confirm(
!await Confirm(
`Do you want to ${name} ${
selected.length === 1
? `"${selected[0].name}"`
: `${selected.length} snapshots`
}`
))
)
) {
return;
}

View File

@ -265,7 +265,7 @@ export default compose(
const { instance } = ownProps;
const { id } = instance;
if (!(await Confirm(`Do you want to ${action} "${instance.name}"?`))) {
if (!await Confirm(`Do you want to ${action} "${instance.name}"?`)) {
return;
}

View File

@ -285,7 +285,7 @@ export default compose(
},
handleRemove: async (form, { name }) => {
// eslint-disable-next-line no-alert
if (!(await Confirm(`Do you want to remove "${name}"?`))) {
if (!await Confirm(`Do you want to remove "${name}"?`)) {
return;
}

View File

@ -8,8 +8,7 @@ import { BrowserRouter } from 'react-router-dom';
import isFunction from 'lodash.isfunction';
import isFinite from 'lodash.isfinite';
import { theme } from 'joyent-ui-toolkit';
import theme from '@state/theme';
import createStore from '@state/redux-store';
import createClient from '@state/apollo-client';
import App from './app';

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { Fragment } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import get from 'lodash.get';
@ -27,6 +27,8 @@ import {
UserScript as InstanceUserScript
} from '@containers/instances';
const { REACT_APP_DEV = false } = process.env;
export default () => (
<PageContainer>
{/* Breadcrumb */}
@ -102,6 +104,41 @@ export default () => (
<Route path="/" exact component={() => <Redirect to="/instances" />} />
{REACT_APP_DEV ? (
<Fragment>
<Route
path="/images"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3070${
location.pathname
}${location.search}`
)
}
/>
<Route
path="/templates"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3071${
location.pathname
}${location.search}`
)
}
/>
<Route
path="/service-groups"
component={({ location }) =>
window.location.replace(
`${window.location.protocol}//${window.location.hostname}:3072${
location.pathname
}${location.search}`
)
}
/>
</Fragment>
) : null}
<noscript>
<ViewContainer main>
<Message warning>

View File

@ -2,6 +2,7 @@ import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import fetch from 'cross-fetch';
import get from 'lodash.get';
import global from './global';
@ -14,7 +15,9 @@ const {
const PORT = REACT_APP_GQL_PORT ? `:${REACT_APP_GQL_PORT}` : '';
const URI = `${REACT_APP_GQL_PROTOCOL}://${REACT_APP_GQL_HOSTNAME}${PORT}/instances/graphql`;
export default (opts = {}) => {
export default (opts = {}, request = {}) => {
const host = get(request, 'raw.req.headers.host', '');
let cache = new InMemoryCache();
if (global.__APOLLO_STATE__) {
@ -24,7 +27,7 @@ export default (opts = {}) => {
return new ApolloClient({
cache,
link: new HttpLink({
uri: URI,
uri: host ? `${REACT_APP_GQL_PROTOCOL}//${host}/instances/graphql` : URI,
credentials: 'same-origin',
fetch,
headers: {

View File

@ -1,9 +1,12 @@
import { canUseDOM } from 'exenv';
import queryString from 'query-string';
const { NODE_ENV = 'development' } = process.env;
export const Global = () => {
if (!canUseDOM) {
return {
protocol: NODE_ENV === 'development' ? 'http:' : 'https:',
cookie: ''
};
}

View File

@ -2,7 +2,7 @@ import intercept from 'apr-intercept';
import keys from 'lodash.keys';
import reduce from 'apr-reduce';
import assign from 'lodash.assign';
import yup from 'yup';
import * as yup from 'yup';
/*****************************************************************************/

View File

@ -15,16 +15,16 @@
"build": "PREACT=1 joyent-react-scripts build"
},
"dependencies": {
"apollo-cache-inmemory": "^1.1.12",
"apollo-client": "^2.2.8",
"apollo-link": "^1.2.1",
"apollo-link-http": "^1.5.3",
"apollo-cache-inmemory": "^1.2.2",
"apollo-client": "^2.3.2",
"apollo-link": "^1.2.2",
"apollo-link-http": "^1.5.4",
"apollo-link-state": "^0.4.1",
"apr-intercept": "^3.0.3",
"boom": "^7.2.0",
"emotion": "^9.1.1",
"emotion-theming": "^9.0.0",
"graphql-tag": "^2.8.0",
"emotion": "^9.1.3",
"emotion-theming": "^9.1.2",
"graphql-tag": "^2.9.2",
"inert": "^5.1.0",
"joyent-icons": "^5.1.0",
"joyent-ui-toolkit": "^6.0.0",
@ -34,20 +34,20 @@
"mz": "^2.7.0",
"outy": "^0.1.2",
"pascal-case": "^2.0.1",
"preact": "^8.2.7",
"preact": "^8.2.9",
"preact-compat": "^3.18.0",
"preact-emotion": "^9.1.1",
"preact-emotion": "^9.1.3",
"preact-emotion-flexboxgrid": "^2.0.1",
"react-apollo": "^2.1.2",
"react-apollo": "^2.1.4",
"remcalc": "^1.0.10",
"stickybits": "^3.2.0"
"stickybits": "^3.3.2"
},
"devDependencies": {
"babel-eslint": "^8.2.2",
"babel-eslint": "^8.2.3",
"babel-preset-joyent-portal": "^7.0.1",
"eslint": "^4.19.1",
"eslint-config-joyent-portal": "^3.3.1",
"joyent-react-scripts": "^8.2.0",
"redrun": "^6.0.2"
"joyent-react-scripts": "^8.2.1",
"redrun": "^6.0.4"
}
}

View File

@ -69,7 +69,7 @@ const Datacenters = ({ expanded, regions = [] }) =>
<RegionContainer>
<Row>
{regions[region].map(({ name, datacenters }) => (
<Col key={name} xs={12} md={6} lg={3}>
<Col key={name} xs="12" md="6" lg="3">
<DatacenterPlace>{name}</DatacenterPlace>
{datacenters.map(({ name, url }) => (
<Datacenter key={name}>

View File

@ -64,7 +64,7 @@ const Services = ({ expanded = false, categories = [], loading }) =>
<Row>
{!loading &&
categories.map(({ name, services }) => (
<CategoryWrapper xs={12} sm={6} md={4}>
<CategoryWrapper xs="12" sm="6" md="4">
<ServiceCategory>{name}</ServiceCategory>
{services.map(({ name, description, url, tags }) => (
<Service>

View File

@ -0,0 +1,12 @@
{
"ignore": ["_document.js", "_aliases.js"],
"presets": [
[
"joyent-portal",
{
"aliases": true,
"autoAliases": true
}
]
]
}

View File

@ -0,0 +1,25 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
## Image Snapshots Diff
**/__diff_output__
lib/app

View File

@ -0,0 +1,8 @@
{
"setup": {
"compile": "npm run build",
"start": "serve -s build --port 3069 --single",
"href": "http://0.0.0.0:3069"
},
"extends": "lighthouse:default"
}

View File

@ -0,0 +1,27 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
## Image Snapshots Diff
**/__diff_output__
!lib/app
!dist
!build

View File

@ -0,0 +1,4 @@
{
"test": ["./src/**/*.js"],
"extends": ["stylelint-config-joyent-portal"]
}

View File

@ -0,0 +1,13 @@
{
"libs": ["ecmascript", "browser"],
"plugins": {
"doc_comment": true,
"local-scope": true,
"jsx": true,
"node": true,
"webpack": {
"configPath":
"../../node_modules/joyent-react-scripts/src/webpack.config.dev.js"
}
}
}

View File

@ -0,0 +1,133 @@
const Boom = require('boom');
const Inert = require('inert');
const Path = require('path');
const RenderReact = require('hapi-render-react');
const Intercept = require('apr-intercept');
const Fs = require('mz/fs');
const { NAMESPACE = 'service-groups', NODE_ENV = 'development' } = process.env;
exports.register = async server => {
let manifest = {};
try {
manifest = require('../build/asset-manifest.json');
} catch (err) {
if (NODE_ENV === 'production') {
throw err;
} else {
// eslint-disable-next-line no-console
console.error(err);
}
}
const relativeTo = Path.join(__dirname, 'app');
const buildRoot = Path.join(__dirname, '../build');
const buildStatic = Path.join(buildRoot, `${NAMESPACE}`);
const publicRoot = Path.join(__dirname, `../public/static/`);
await server.register([
{
plugin: Inert
},
{
plugin: RenderReact
}
]);
server.route([
{
method: 'GET',
path: `/${NAMESPACE}/service-worker.js`,
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/service-worker.js')
}
}
}
},
{
method: 'GET',
path: `/${NAMESPACE}/favicon.ico`,
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/favicon.ico')
}
}
}
},
{
method: 'GET',
path: `/${NAMESPACE}/static/{rest*}`,
config: {
auth: false
},
handler: async (request, h) => {
const { params } = request;
const { rest } = params;
if (!rest) {
return Boom.notFound();
}
const publicPathname = Path.join(publicRoot, rest);
const [err1] = await Intercept(
Fs.access(publicPathname, Fs.constants.R_OK)
);
if (!err1) {
return h.file(publicPathname, {
confine: publicRoot
});
}
const buildPathname = Path.join(buildStatic, 'static', rest);
const [err2] = await Intercept(
Fs.access(buildPathname, Fs.constants.R_OK)
);
if (!err2) {
return h.file(buildPathname, {
confine: buildStatic
});
}
const filename = manifest[rest];
if (!filename) {
return Boom.notFound();
}
const buildMapPathname = Path.join(buildRoot, filename);
return h.file(buildMapPathname, {
confine: buildStatic
});
}
},
{
method: '*',
path: `/${NAMESPACE}/~server-error`,
handler: {
view: {
name: 'server-error',
relativeTo
}
}
},
{
method: '*',
path: `/${NAMESPACE}/{path*}`,
handler: {
view: {
name: 'app',
relativeTo
}
}
}
]);
};
exports.pkg = require('../package.json');

View File

@ -0,0 +1,73 @@
{
"name": "my-joy-service-groups",
"version": "1.0.0",
"private": true,
"license": "MPL-2.0",
"repository": "github:yldio/joyent-portal",
"main": "lib/index.js",
"scripts": {
"dev": "REACT_APP_DEV=1 NAMESPACE=service-groups NODE_ENV=development REACT_APP_GQL_PORT=4000 PORT=3072 joyent-react-scripts start",
"build:test": "echo 0",
"build:lib": "echo 0",
"build:bundle": "NAMESPACE=service-groups NODE_ENV=production redrun -p build:frontend build:ssr",
"prepublish": "NODE_ENV=production redrun build:bundle",
"test": "DEFAULT_TIMEOUT_INTERVAL=100000 NODE_ENV=test joyent-react-scripts test --env=jsdom",
"test:ci": "NODE_ENV=test joyent-react-scripts test --env=jsdom --testPathIgnorePatterns='.ui.js'",
"build:frontend": "joyent-react-scripts build",
"build:ssr": "SSR=1 UMD=1 babel src --out-dir lib/app --copy-files"
},
"dependencies": {
"@manaflair/redux-batch": "^0.1.0",
"apr-intercept": "^3.0.3",
"boom": "^7.2.0",
"cross-fetch": "^2.2.0",
"date-fns": "^1.29.0",
"declarative-redux-form": "^2.0.8",
"exenv": "^1.2.2",
"fuse.js": "^3.2.0",
"hapi-render-react": "^2.5.2",
"hapi-render-react-joyent-document": "^7.2.0",
"inert": "^5.1.0",
"joyent-react-styled-flexboxgrid": "^3.1.0",
"joyent-ui-resource-widgets": "^1.0.0",
"joyent-ui-toolkit": "^6.0.0",
"lodash.assign": "^4.2.0",
"lodash.find": "^4.6.0",
"lodash.get": "^4.4.2",
"lodash.isstring": "^4.0.1",
"lodash.keys": "^4.2.0",
"lodash.reverse": "^4.0.1",
"lodash.sortby": "^4.7.0",
"mz": "^2.7.0",
"param-case": "^2.1.1",
"plur": "^3.0.1",
"query-string": "^6.1.0",
"react": "^16.4.0",
"react-apollo": "^2.1.4",
"react-dom": "^16.4.0",
"react-helmet-async": "0.1.0",
"react-if": "^2.2.2",
"react-redux": "^5.0.7",
"react-redux-values": "^1.1.2",
"react-router-dom": "^4.2.2",
"redux": "^4.0.0",
"redux-form": "^7.3.0",
"remcalc": "^1.0.10",
"styled-components": "^3.3.0",
"styled-components-spacing": "^3.0.0",
"styled-flex-component": "^2.2.2",
"yup": "^0.25.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-joyent-portal": "^7.0.1",
"eslint": "^4.19.1",
"eslint-config-joyent-portal": "^3.3.1",
"jest-image-snapshot": "^2.4.2",
"jest-styled-components": "^5.0.1",
"joyent-react-scripts": "^8.2.1",
"react-screenshot-renderer": "^1.1.2",
"react-test-renderer": "^16.4.0",
"redrun": "^6.0.4"
}
}

View File

@ -0,0 +1,15 @@
{
"short_name": "Joyent",
"name": "My Joyent &beta;",
"icons": [
{
"src": "favicon.ico",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#1E313B",
"background_color": "#FAFAFA"
}

View File

@ -0,0 +1,31 @@
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 400;
src: local('Libre Franklin'), local('LibreFranklin-Regular'),
url(../fonts/libre-franklin/libre-franklin-regular.ttf) format('truetype');
}
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 500;
src: local('Libre Franklin Medium'), local('LibreFranklin-Medium'),
url(../fonts/libre-franklin/libre-franklin-medium.ttf) format('truetype');
}
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 600;
src: local('Libre Franklin SemiBold'), local('LibreFranklin-SemiBold'),
url(../fonts/libre-franklin/libre-franklin-semibold.ttf) format('truetype');
}
@font-face {
font-family: 'Libre Franklin';
font-style: normal;
font-weight: 700;
src: local('Libre Franklin Bold'), local('LibreFranklin-Bold'),
url(../fonts/libre-franklin/libre-franklin-bold.ttf) format('truetype');
}

View File

@ -0,0 +1,15 @@
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: local('Roboto Mono'), local('RobotoMono-Regular'),
url(../fonts/roboto-mono/roboto-mono-regular.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: local('Roboto Mono Bold'), local('RobotoMono-Bold'),
url(../fonts/roboto-mono/roboto-mono-bold.ttf) format('truetype');
}

View File

@ -0,0 +1,93 @@
Copyright (c) 2015, Impallari Type (www.impallari.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,20 @@
# my-joy-service-groups
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg?style=flat-square)](https://opensource.org/licenses/MPL-2.0)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
## Table of Contents
* [Usage](#usage)
* [License](#license)
## Usage
```
npm run start
open http://0.0.0.0:3069
```
## License
MPL-2.0

View File

@ -0,0 +1,9 @@
const { SSR } = process.env;
const aliases = {};
if (SSR) {
aliases['^joyent-ui-toolkit/dist/es/editor$'] = './src/mocks/editor';
}
module.exports = aliases;

View File

@ -0,0 +1,54 @@
const get = require('lodash.get');
const Document = require('hapi-render-react-joyent-document');
const url = require('url');
const { theme } = require('joyent-ui-toolkit');
const { default: createClient } = require('./state/apollo-client');
const { default: createStore } = require('./state/redux-store');
const assets = require('../../build/asset-manifest.json');
const { NODE_ENV = 'development' } = process.env;
const getState = request => {
const { req } = request.raw;
const { headers } = req;
const { host } = headers;
const protocol = NODE_ENV === 'development' ? 'http:' : 'https:';
const _font = get(theme, 'font.href', () => '');
const _mono = get(theme, 'monoFont.href', () => '');
const _addr = url.parse(`${protocol}//${host}`);
const _theme = Object.assign({}, theme, {
font: Object.assign({}, theme.font, {
href: () =>
_font(
Object.assign(_addr, {
namespace: 'service-groups'
})
)
}),
monoFont: Object.assign({}, theme.monoFont, {
href: () =>
_mono(
Object.assign(_addr, {
namespace: 'service-groups'
})
)
})
});
return {
theme: _theme,
createClient,
createStore
};
};
module.exports = Document({
namespace: 'service-groups/',
assets,
Html: require('./html'),
getState
});

View File

@ -0,0 +1,14 @@
import React from 'react';
import Helmet from 'react-helmet-async';
import { RootContainer } from 'joyent-ui-toolkit';
import Routes from '@root/routes';
export default () => (
<RootContainer>
<Helmet>
<title>Service Groups</title>
</Helmet>
<Routes />
</RootContainer>
);

View File

@ -0,0 +1,251 @@
import React from 'react';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import styled from 'styled-components';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import Flex, { FlexItem } from 'styled-flex-component';
import { Margin, Padding } from 'styled-components-spacing';
import { Link } from 'react-router-dom';
import { Field } from 'redux-form';
import {
Card,
CardOutlet,
Anchor,
Button,
H3,
P,
FormGroup,
Checkbox,
Table,
TableThead,
TableTr,
TableTh,
TableTd,
TableTbody,
StickyFooter,
StatusLoader,
DeleteIcon,
EmptyStateIcon
} from 'joyent-ui-toolkit';
const A = styled(Anchor)`
color: ${props => props.theme.text};
text-decoration: none;
font-weight: ${props => props.theme.font.weight.semibold};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: block;
`;
export const BulkFooter = ({ items = [], onRemove }) => {
const disabled = items.some(({ removing }) => removing);
return (
<StickyFooter fill="disabled" fixed bottom>
<Row between="xs" middle="xs">
<Col xs="7">
<Flex>
<Margin right="1">
<Button
type="button"
component={Link}
to={`/service-groups/~edit/${items[0].id}/name`}
disabled={disabled || items.length > 1}
icon
>
<span>Edit Service Group</span>
</Button>
</Margin>
<Button
type="button"
component={Link}
to={`/instances?sg=${items[0].id}`}
disabled={disabled || items.length > 1}
secondary
icon
>
<span>View instances</span>
</Button>
</Flex>
</Col>
<Col xs="5">
<Flex justifyEnd alignCenter>
<FlexItem>
<Button
type="button"
onClick={ev => onRemove(ev, items)}
disabled={disabled}
error
secondary
icon
>
<Margin right="1">
<DeleteIcon fill={disabled ? 'grey' : 'red'} />
</Margin>
<span>Remove</span>
</Button>
</FlexItem>
</Flex>
</Col>
</Row>
</StickyFooter>
);
};
export const LoadingRow = ({ children }) => (
<TableTr colSpan="5">
<TableTd colSpan="5" middle center>
<Margin vertical="5">
<StatusLoader>{children}</StatusLoader>
</Margin>
</TableTd>
</TableTr>
);
export const EmptyCard = () => (
<Card>
<CardOutlet>
<Row center="xs">
<Col xs="12" sm="9" md="8" lg="6">
<Padding all="5">
<Margin bottom="3">
<EmptyStateIcon />
</Margin>
<Margin bottom="2">
<H3 bold>No service groups found</H3>
</Margin>
<P>You can create a new service group with the below button.</P>
<Margin top="3">
<Button
type="button"
component={Link}
to="/service-groups/~create/template"
>
Create service group
</Button>
</Margin>
</Padding>
</Col>
</Row>
</CardOutlet>
</Card>
);
export const EmptyRow = () => (
<TableTr colSpan="5">
<TableTd colSpan="5" middle center>
<Padding vertical="4">
<P>You have no service groups that match your query</P>
</Padding>
</TableTd>
</TableTr>
);
export const Item = ({
id = '',
name,
capacity,
template,
created,
...group
}) => (
<TableTr>
<TableTd middle left>
<FormGroup name={id} field={Field}>
<Checkbox noMargin />
</FormGroup>
</TableTd>
<TableTd middle left>
<A to={`/service-groups/${id}`} component={Link}>
{name}
</A>
</TableTd>
<TableTd xs="0" sm="120" middle left>
{capacity}
</TableTd>
<TableTd xs="0" sm="180" middle left>
{template.name}
</TableTd>
<TableTd xs="0" sm="180" middle left>
{distanceInWordsToNow(created)}
</TableTd>
</TableTr>
);
export default ({
sortBy = 'name',
sortOrder = 'desc',
submitting = false,
checked = false,
onToggleCheckAll = () => null,
onSortBy = () => null,
children
}) => (
<form>
<Table>
<TableThead>
<TableTr>
<TableTh xs="42" middle left>
<FormGroup>
<Checkbox
checked={checked}
disabled={submitting}
onChange={onToggleCheckAll}
noMargin
/>
</FormGroup>
</TableTh>
<TableTh
sortOrder={sortOrder}
showSort={sortBy === 'name'}
onClick={() => onSortBy('name')}
left
middle
actionable
>
<span>Name</span>
</TableTh>
<TableTh
sortOrder={sortOrder}
showSort={sortBy === 'capacity'}
onClick={() => onSortBy('capacity')}
xs="0"
sm="120"
left
middle
actionable
>
<span>Desired #</span>
</TableTh>
<TableTh
sortOrder={sortOrder}
showSort={sortBy === 'template'}
onClick={() => onSortBy('template')}
xs="0"
sm="180"
left
middle
actionable
>
<span>Template</span>
</TableTh>
<TableTh
sortOrder={sortOrder}
showSort={sortBy === 'created'}
onClick={() => onSortBy('created')}
xs="0"
sm="180"
left
middle
actionable
>
<span>Created</span>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>{children}</TableTbody>
</Table>
</form>
);

View File

@ -0,0 +1,367 @@
import React from 'react';
import { Link } from 'react-router-dom';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { Padding, Margin } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component';
import styled from 'styled-components';
import remcalc from 'remcalc';
import {
Card,
CardOutlet,
H2,
H3,
P,
Label,
FormGroup,
FormLabel,
Input,
Checkbox,
Anchor,
Button,
Hr,
Table,
TableThead,
TableTbody,
PaginationTableFoot,
PaginationItem,
TableTr,
TableTh,
TableTd,
DeleteIcon,
ArrowIcon
} from 'joyent-ui-toolkit';
const GreyLabel = styled(Label)`
opacity: 0.5;
padding-right: ${remcalc(3)};
`;
const VerticalDivider = styled.div`
width: ${remcalc(1)};
background: ${props => props.theme.grey};
height: ${remcalc(24)};
display: flex;
align-self: flex-end;
margin: 0 ${remcalc(12)};
`;
export const Meta = ({
id,
name,
capacity,
template,
created,
updated,
status,
onRemove,
removing = false
}) => (
<Card>
<CardOutlet>
<Padding all="5">
<H2>{name}</H2>
<Margin top="2">
<P>{capacity} desired instances</P>
</Margin>
<Margin top="3" bottom="3">
<Hr />
</Margin>
<Margin bottom="3">
<Flex>
<FlexItem>
<Padding right="3">
<GreyLabel inline>Template: </GreyLabel>
<Label inline>
{' '}
<Anchor
to={`/templates/${template.id}`}
component={Link}
tertiary
>
{template.name}
</Anchor>
</Label>
</Padding>
</FlexItem>
<VerticalDivider />
<FlexItem>
<Padding right="3">
<GreyLabel inline>Created: </GreyLabel>
<Label inline> {distanceInWordsToNow(created)} ago</Label>
</Padding>
</FlexItem>
<VerticalDivider />
<FlexItem>
<Padding horizontal="3">
<GreyLabel inline>Updated: </GreyLabel>
<Label inline> {distanceInWordsToNow(updated)} ago</Label>
</Padding>
</FlexItem>
</Flex>
</Margin>
<Row between="xs">
<Col xs="6">
<Margin right="1" inline>
<Button
type="button"
to={`/service-groups/~edit/${id}/name`}
component={Link}
bold
icon
>
<span>Edit Service Group</span>
</Button>
</Margin>
<Button
type="button"
to={`/instances?sg=${id}`}
component={Link}
secondary
bold
icon
>
<span>View instances</span>
</Button>
</Col>
<Col xs="6">
<Button
type="button"
onClick={onRemove}
loading={removing}
secondary
bold
right
icon
error
>
<Margin right="2">
<DeleteIcon fill="red" />
</Margin>
<span>Remove</span>
</Button>
</Col>
</Row>
</Padding>
</CardOutlet>
</Card>
);
export const EventLogContainer = () => (
<Card>
<CardOutlet>
<Padding all="5">
<H3>Event log</H3>
<Margin top="5" bottom="3">
<Flex justifyBetween alignEnd>
<FormGroup name="filter">
<Margin bottom="0.5">
<FormLabel>Filter</FormLabel>
</Margin>
<Flex alignCenter>
<FlexItem>
<Margin right="5">
<Input />
</Margin>
</FlexItem>
<FlexItem>
<FormGroup>
<Margin right="3">
<Checkbox>
<Margin left="2">
<Label>Show census checks</Label>
</Margin>
</Checkbox>
</Margin>
</FormGroup>
</FlexItem>
<FlexItem>
<FormGroup>
<Margin right="3">
<Checkbox>
<Margin left="2">
<Label>Show users activity</Label>
</Margin>
</Checkbox>
</Margin>
</FormGroup>
</FlexItem>
</Flex>
</FormGroup>
</Flex>
</Margin>
<Margin bottom="5">
<Hr />
</Margin>
<Table>
<TableThead>
<TableTr>
<TableTh xs="200" left middle actionable>
<span>Time & date</span>
</TableTh>
<TableTh left middle actionable>
<span>Log description</span>
</TableTh>
<TableTh xs="230" left middle actionable>
<span>Actor ID</span>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd middle left>
<span>09:52 - 28/03/2018</span>
</TableTd>
<TableTd middle left>
<span>5 of 5 instances running</span>
</TableTd>
<TableTd middle left>
<span>Census check</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>09:50 - 28/03/2018</span>
</TableTd>
<TableTd middle left>
<span>Destroying instances</span>
</TableTd>
<TableTd middle left>
<span>tritionServiceGroups</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>09:45 - 28/03/2018</span>
</TableTd>
<TableTd middle left>
<span>10 of 5 instances running</span>
</TableTd>
<TableTd middle left>
<span>Census checks</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:17 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>Desired instances set to 5</span>
</TableTd>
<TableTd middle left>
<span>raoulmillais</span>
</TableTd>
</TableTr>
<TableTr disabled>
<TableTd colSpan="3" middle left shrinken>
<Anchor>
Show hidden actions (63) <ArrowIcon fill="primary" />
</Anchor>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:16 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>Provisioning instances</span>
</TableTd>
<TableTd middle left>
<span>tritionServiceGroups</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:16 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>7 of 10 instances running</span>
</TableTd>
<TableTd middle left>
<span>Census checks</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:11 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>10 of 10 instances running</span>
</TableTd>
<TableTd middle left>
<span>Census checks</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:11 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>Provisioning instances</span>
</TableTd>
<TableTd middle left>
<span>tritionServiceGroups</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:10 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>0 of 10 instance running</span>
</TableTd>
<TableTd middle left>
<span>Census checks</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:09 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>Job working</span>
</TableTd>
<TableTd middle left>
<span>tritionServiceGroups</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>09:51 - 28/03/2018</span>
</TableTd>
<TableTd middle left>
<span>Job submission</span>
</TableTd>
<TableTd middle left>
<span>tritionServiceGroups</span>
</TableTd>
</TableTr>
<TableTr>
<TableTd middle left>
<span>12:08 - 26/03/2018</span>
</TableTd>
<TableTd middle left>
<span>Service group deployed</span>
</TableTd>
<TableTd middle left>
<span>raoulmillais</span>
</TableTd>
</TableTr>
</TableTbody>
<PaginationTableFoot colSpan="3">
<PaginationItem to="" component={Link} disabled prev>
Prev
</PaginationItem>
<PaginationItem to="" component={Link} active>
1
</PaginationItem>
<PaginationItem to="" component={Link} disabled next>
Next
</PaginationItem>
</PaginationTableFoot>
</Table>
</Padding>
</CardOutlet>
</Card>
);

View File

@ -0,0 +1,175 @@
import React from 'react';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import styled from 'styled-components';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { Margin, Padding } from 'styled-components-spacing';
import { Link } from 'react-router-dom';
import { Field } from 'redux-form';
import {
Card,
CardOutlet,
H4,
P,
Button,
FormGroup,
FormLabel,
Radio,
StatusLoader,
Table,
TableThead,
TableTr,
TableTh,
TableTd,
TableTbody,
ExternalIcon
} from 'joyent-ui-toolkit';
const Name = styled.span`
color: ${props => props.theme.text};
text-decoration: none;
font-weight: ${props => props.theme.font.weight.semibold};
`;
export const EmptyCard = () => (
<Card>
<CardOutlet>
<Row center="xs">
<Col xs="12" sm="9" md="8" lg="6">
<Padding all="5">
<H4 bold>No templates found</H4>
<P>
In order to deploy a Service Group, youll need to first create a
template to base your instances off of. Click below to continue
</P>
<Margin top="3">
<Button
type="button"
component={Link}
to="/templates/~create/name"
secondary
icon
>
<Margin right="2">
<ExternalIcon />
</Margin>
<span>Create template</span>
</Button>
</Margin>
</Padding>
</Col>
</Row>
</CardOutlet>
</Card>
);
export const EmptyRow = () => (
<TableTr colSpan="5">
<TableTd colSpan="5" middle center>
<Padding vertical="4">
<P>You have no templates that match your query</P>
</Padding>
</TableTd>
</TableTr>
);
export const LoadingRow = ({ children }) => (
<TableTr colSpan="5">
<TableTd colSpan="5" middle center>
<Margin vertical="5">
<StatusLoader>{children}</StatusLoader>
</Margin>
</TableTd>
</TableTr>
);
export const Item = ({ id = '', name, image, created, ...template }) => (
<TableTr>
<TableTd colSpan="2" middle left>
<FormGroup name="template" value={id} type="radio" field={Field}>
<Radio onBlur={null} noMargin>
<Margin left="5">
<FormLabel noMargin actionable>
<Name>{name}</Name>
</FormLabel>
</Margin>
</Radio>
</FormGroup>
</TableTd>
<TableTd xs="0" sm="160" middle left>
{image.substring(0, 7)}
</TableTd>
<TableTd xs="0" sm="160" middle left>
{template.package.substring(0, 7)}
</TableTd>
<TableTd middle left>
{distanceInWordsToNow(created)}
</TableTd>
</TableTr>
);
export default ({
sortBy = 'name',
sortOrder = 'desc',
submitting = false,
checked = false,
onToggleCheckAll = () => null,
onSortBy = () => null,
children
}) => (
<form>
<Table>
<TableThead>
<TableTr>
<TableTh xs="42" middle left />
<TableTh
sortOrder={sortOrder}
showSort={sortBy === 'name'}
onClick={() => onSortBy('name')}
left
middle
actionable
>
<span>Name</span>
</TableTh>
<TableTh
xs="0"
sm="160"
sortOrder={sortOrder}
showSort={sortBy === 'image'}
onClick={() => onSortBy('image')}
left
middle
actionable
>
<span>Image</span>
</TableTh>
<TableTh
xs="0"
sm="160"
sortOrder={sortOrder}
showSort={sortBy === 'package'}
onClick={() => onSortBy('package')}
left
middle
actionable
>
<span>Package</span>
</TableTh>
<TableTh
xs="180"
sortOrder={sortOrder}
showSort={sortBy === 'created'}
onClick={() => onSortBy('created')}
left
middle
actionable
>
<span>Created</span>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>{children}</TableTbody>
</Table>
</form>
);

View File

@ -0,0 +1,49 @@
import React from 'react';
import Flex from 'styled-flex-component';
import { If, Then } from 'react-if';
import { Margin } from 'styled-components-spacing';
import { Link } from 'react-router-dom';
import { Field } from 'redux-form';
import { FormGroup, Input, FormLabel, Button } from 'joyent-ui-toolkit';
export const Toolbar = ({
searchLabel = 'Filter',
searchPlaceholder = '',
searchable = true,
actionLabel = 'Create',
actionable = true,
onActionClick,
actionTo
}) => (
<Flex justifyBetween alignEnd>
<FormGroup name="filter" field={Field}>
<FormLabel>{searchLabel}</FormLabel>
<Margin top="0.5">
<Input placeholder={searchPlaceholder} disabled={!searchable} />
</Margin>
</FormGroup>
<If condition={actionable}>
<Then>
<FormGroup right>
<Button
type={actionTo || onActionClick ? 'button' : 'submit'}
component={actionTo ? Link : undefined}
to={actionTo}
onClick={onActionClick}
icon
fluid
>
{actionLabel}
</Button>
</FormGroup>
</Then>
</If>
</Flex>
);
export default ({ handleSubmit, ...rest }) => (
<form onSubmit={handleSubmit}>
<Toolbar {...rest} />
</form>
);

View File

@ -0,0 +1,23 @@
export const Forms = {
SGC_F: 'SERVICE_GROUP_CREATE_FORM',
SGC_T_F: 'SERVICE_GROUP_CREATE_TEMPLATE_FORM',
SGC_F_F: 'SERVICE_GROUP_CREATE_FILTER_FORM',
SGC_N_F: 'SERVICE_GROUP_CREATE_NAME_FORM',
SGE_F: 'SERVICE_GROUP_EDIT_FORM',
SGL_F_F: 'SERVICE_GROUP_LIST_FILTER_FORM',
SGL_T_F: 'SERVICE_GROUP_LIST_TABLE_FORM'
};
export const Values = {
SGC_N_V: 'SERVICE_GROUP_CREATE_NAME_VALUE',
SGC_T_V: 'SERVICE_GROUP_CREATE_TEMPLATE_VALUE',
SGE_N_V: 'SERVICE_GROUP_EDIT_NAME_VALUE',
SGC_T_SB_V: 'SERVICE_GROUP_CREATE_TEMPLATE_SORT_BY_VALUE',
SGC_T_SO_V: 'SERVICE_GROUP_CREATE_TEMPLATE_SORT_ORDER_VALUE',
SGL_R_V: id => `SERVICE_GROUP_LIST_REMOVING_VALUE-${id}`,
SGL_E_V: 'SERVICE_GROUP_LIST_ERROR_VALUE',
SGL_SB_V: 'SERVICE_GROUP_LIST_SORT_BY_VALUE',
SGL_SO_V: 'SERVICE_GROUP_LIST_SORT_ORDER_VALUE',
SGS_R_V: id => `SERVICE_GROUP_SUMMARY_REMOVING_VALUE-${id}`,
SGS_E_V: id => `SERVICE_GROUP_SUMMARY_ERROR_VALUE-${id}`
};

View File

@ -0,0 +1,503 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders <Breadcrumb /> without throwing 1`] = `
.c11 {
margin-left: 0.375rem;
margin-right: 0.375rem;
margin-top: 1.125rem;
margin-bottom: 1.125rem;
}
.c3 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.c2 {
margin-right: auto;
margin-left: auto;
padding-right: 1.875rem;
padding-left: 1.875rem;
}
.c5 {
margin-right: auto;
margin-left: auto;
}
.c6 {
box-sizing: border-box;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -0.625rem;
margin-left: -0.625rem;
}
.c7 {
box-sizing: border-box;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.625rem;
padding-left: 0.625rem;
display: block;
}
.c1 {
box-sizing: border-box;
width: 100%;
padding-left: 0;
padding-right: 0;
}
.c4 {
box-sizing: border-box;
width: 100%;
max-width: 78.75rem;
}
.c10 {
margin: 0;
color: rgb(73,73,73);
font-weight: 400;
line-height: 1.5rem;
font-size: 0.9375rem;
}
.c9 {
font-weight: normal;
margin: 0;
}
.c8 {
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c8:first-child a {
color: inherit;
-webkit-text-decoration: none;
text-decoration: none;
}
.c8:last-child svg {
display: none;
}
.c0 {
border-bottom: 0.0625rem solid rgb(216,216,216);
}
@media only screen and (min-width:48em) {
.c5 {
width: 46rem;
}
}
@media only screen and (min-width:64em) {
.c5 {
width: 56rem;
}
}
@media only screen and (min-width:75em) {
.c5 {
width: 59rem;
}
}
@media only screen and (min-width:0em) {
.c7 {
-webkit-flex-basis: 100%;
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
}
@media only screen and (max-width:37.4375rem) {
.c4 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
@media only screen and (min-width:37.5rem) and (max-width:62.4375rem) {
.c4 {
padding-left: 1.875rem;
padding-right: 1.875rem;
}
}
@media only screen and (min-width:62.5rem) and (max-width:87.4375rem) {
.c4 {
padding-left: 4.375rem;
padding-right: 4.375rem;
}
}
<div
className="c0 c1 c2"
>
<div
className="c3"
>
<div
className="c4 c5"
>
<div
className="c6"
name="breadcrum"
>
<div
className="c7"
>
<div
className="c8"
>
<h4
className="c9 c10"
name="breadcrum-item"
>
<div
className="c11"
>
Compute
</div>
</h4>
<svg
height="6"
style={
Object {
"transform": "rotate(-90deg)",
}
}
viewBox="0 0 9.6 6"
width="9.6"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.6,1.12,8.24,0,4.8,3.5,1.36,0,0,1.12,4.8,6Z"
fill="rgb(151, 151, 151)"
/>
</svg>
</div>
<div
className="c8"
>
<h4
className="c9 c10"
name="breadcrum-item"
>
<div
className="c11"
>
Service Groups
</div>
</h4>
<svg
height="6"
style={
Object {
"transform": "rotate(-90deg)",
}
}
viewBox="0 0 9.6 6"
width="9.6"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.6,1.12,8.24,0,4.8,3.5,1.36,0,0,1.12,4.8,6Z"
fill="rgb(151, 151, 151)"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders <Breadcrumb match /> without throwing 1`] = `
.c11 {
margin-left: 0.375rem;
margin-right: 0.375rem;
margin-top: 1.125rem;
margin-bottom: 1.125rem;
}
.c3 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.c2 {
margin-right: auto;
margin-left: auto;
padding-right: 1.875rem;
padding-left: 1.875rem;
}
.c5 {
margin-right: auto;
margin-left: auto;
}
.c6 {
box-sizing: border-box;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -0.625rem;
margin-left: -0.625rem;
}
.c7 {
box-sizing: border-box;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.625rem;
padding-left: 0.625rem;
display: block;
}
.c1 {
box-sizing: border-box;
width: 100%;
padding-left: 0;
padding-right: 0;
}
.c4 {
box-sizing: border-box;
width: 100%;
max-width: 78.75rem;
}
.c10 {
margin: 0;
color: rgb(73,73,73);
font-weight: 400;
line-height: 1.5rem;
font-size: 0.9375rem;
}
.c9 {
font-weight: normal;
margin: 0;
}
.c8 {
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c8:first-child a {
color: inherit;
-webkit-text-decoration: none;
text-decoration: none;
}
.c8:last-child svg {
display: none;
}
.c0 {
border-bottom: 0.0625rem solid rgb(216,216,216);
}
@media only screen and (min-width:48em) {
.c5 {
width: 46rem;
}
}
@media only screen and (min-width:64em) {
.c5 {
width: 56rem;
}
}
@media only screen and (min-width:75em) {
.c5 {
width: 59rem;
}
}
@media only screen and (min-width:0em) {
.c7 {
-webkit-flex-basis: 100%;
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
}
}
@media only screen and (max-width:37.4375rem) {
.c4 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
@media only screen and (min-width:37.5rem) and (max-width:62.4375rem) {
.c4 {
padding-left: 1.875rem;
padding-right: 1.875rem;
}
}
@media only screen and (min-width:62.5rem) and (max-width:87.4375rem) {
.c4 {
padding-left: 4.375rem;
padding-right: 4.375rem;
}
}
<div
className="c0 c1 c2"
>
<div
className="c3"
>
<div
className="c4 c5"
>
<div
className="c6"
name="breadcrum"
>
<div
className="c7"
>
<div
className="c8"
>
<h4
className="c9 c10"
name="breadcrum-item"
>
<div
className="c11"
>
Compute
</div>
</h4>
<svg
height="6"
style={
Object {
"transform": "rotate(-90deg)",
}
}
viewBox="0 0 9.6 6"
width="9.6"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.6,1.12,8.24,0,4.8,3.5,1.36,0,0,1.12,4.8,6Z"
fill="rgb(151, 151, 151)"
/>
</svg>
</div>
<div
className="c8"
>
<h4
className="c9 c10"
name="breadcrum-item"
>
<div
className="c11"
>
Service Groups
</div>
</h4>
<svg
height="6"
style={
Object {
"transform": "rotate(-90deg)",
}
}
viewBox="0 0 9.6 6"
width="9.6"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.6,1.12,8.24,0,4.8,3.5,1.36,0,0,1.12,4.8,6Z"
fill="rgb(151, 151, 151)"
/>
</svg>
</div>
<div
className="c8"
>
<h4
className="c9 c10"
name="breadcrum-item"
>
<div
className="c11"
>
id
</div>
</h4>
<svg
height="6"
style={
Object {
"transform": "rotate(-90deg)",
}
}
viewBox="0 0 9.6 6"
width="9.6"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.6,1.12,8.24,0,4.8,3.5,1.36,0,0,1.12,4.8,6Z"
fill="rgb(151, 151, 151)"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,36 @@
import React from 'react';
import renderer from 'react-test-renderer';
import 'jest-styled-components';
import Breadcrumb from '../breadcrumb';
import Theme from '@mocks/theme';
it('renders <Breadcrumb /> without throwing', () => {
expect(
renderer
.create(
<Theme>
<Breadcrumb />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Breadcrumb match /> without throwing', () => {
const match = {
params: {
sg: 'id'
}
};
expect(
renderer
.create(
<Theme>
<Breadcrumb match={match} />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});

View File

@ -0,0 +1,40 @@
import React from 'react';
import paramCase from 'param-case';
import get from 'lodash.get';
import { Link } from 'react-router-dom';
import { Margin } from 'styled-components-spacing';
import { Breadcrumb, BreadcrumbItem } from 'joyent-ui-toolkit';
export default ({ match }) => {
const serviceGroupId = get(match, 'params.sg');
const links = [
{
name: 'Compute',
pathname: '/'
},
{
name: 'Service Groups',
pathname: '/service-groups'
}
]
.concat(
serviceGroupId && [
{
name: paramCase(serviceGroupId),
pathname: `/service-groups/${serviceGroupId}`
}
]
)
.filter(Boolean)
.map(({ name, pathname }) => (
<BreadcrumbItem key={name} to={pathname} component={Link}>
<Margin horizontal="1" vertical="3">
{name}
</Margin>
</BreadcrumbItem>
));
return <Breadcrumb>{links}</Breadcrumb>;
};

View File

@ -0,0 +1,237 @@
import React, { Component, Fragment } from 'react';
import { If, Then } from 'react-if';
import ReduxForm from 'declarative-redux-form';
import { SubmissionError, destroy } from 'redux-form';
import { Margin, Padding } from 'styled-components-spacing';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import { set, destroyAll } from 'react-redux-values';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import {
ViewContainer,
Message,
MessageTitle,
MessageDescription,
Button
} from 'joyent-ui-toolkit';
import {
PostCreation,
PostCreationContent,
PostCreationTitle
} from 'joyent-ui-resource-widgets';
import { Provider as ResourceSteps } from 'joyent-ui-resource-step';
import parseError from '@state/parse-error';
import { Forms, Values } from '@root/constants';
import ListServiceGroups from '@graphql/list-service-groups.gql';
import CreateServiceGroup from '@graphql/create-service-group.gql';
import GetServiceGroup from '@graphql/get-service-group.gql';
import Template from './steps/template';
import Name from './steps/name';
const { SGC_F } = Forms;
const { SGC_N_V, SGC_T_V } = Values;
class CreateTemplate extends Component {
constructor(...args) {
super(...args);
this.isValids = {};
}
setIsValid = name => ref => {
if (!ref) {
return;
}
const { isValid } = ref;
if (!isValid) {
return;
}
this.isValids = Object.assign({}, this.isValids, {
[name]: isValid
});
};
isFormValid = () => {
const { steps } = this.props;
return Boolean(
Object.keys(this.isValids).filter(
name => !this.isValids[name](steps[name] || {})
).length
);
};
isStepValid = step => {
const { steps } = this.props;
const fn = this.isValids[step];
const values = steps[step];
if (!fn || !values) {
return true;
}
return fn(values);
};
render() {
const { match, steps, handleDefocus, handleSubmit } = this.props;
const { params } = match;
const { step } = params;
const { template, name } = steps;
const disabled = !(
template &&
template.id &&
name &&
name.name &&
name.capacity
);
return (
<ViewContainer main>
<Padding top="5">
<ResourceSteps namespace="service-groups/~create">
<Margin bottom="4">
<Template
ref={this.setIsValid('template')}
expanded={step === 'template'}
next="name"
saved={get(steps, 'template.id', false)}
onDefocus={handleDefocus(SGC_T_V)}
preview={template}
isValid={this.isStepValid('template')}
/>
</Margin>
<Margin bottom="4">
<Name
ref={this.setIsValid('name')}
expanded={step === 'name'}
saved={get(steps, 'name.name', false)}
onDefocus={handleDefocus(SGC_N_V)}
preview={name}
isValid={this.isStepValid('name')}
/>
</Margin>
</ResourceSteps>
<Margin top="5" bottom="3">
<ReduxForm form={SGC_F} onSubmit={handleSubmit}>
{({ handleSubmit, submitting, error }) => (
<Fragment>
<If condition={error}>
<Then>
<Margin bottom="4">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{error}</MessageDescription>
</Message>
</Margin>
</Then>
</If>
<form onSubmit={handleSubmit}>
<Button loading={submitting} disabled={disabled}>
Deploy
</Button>
</form>
</Fragment>
)}
</ReduxForm>
</Margin>
</Padding>
</ViewContainer>
);
}
}
export const Success = ({ match }) => {
const id = match.params.sg;
return (
<ViewContainer main>
<Margin top="5">
<PostCreation id={id} object="service group">
<PostCreationTitle>
You have successfully created a service group
</PostCreationTitle>
<PostCreationContent>
Your service group has been created and is currently being
processed. It should only take a few minutes and will then appear in
your console.
</PostCreationContent>
</PostCreation>
</Margin>
</ViewContainer>
);
};
export default compose(
graphql(CreateServiceGroup, { name: 'createServiceGroup' }),
connect(({ form, values = {} }) => ({
forms: Object.keys(form),
steps: {
name: get(values, SGC_N_V),
template: get(values, SGC_T_V)
}
})),
connect(null, (dispatch, { forms, steps, history, createServiceGroup }) => ({
handleDefocus: name => value => {
return dispatch(set({ name, value }));
},
handleSubmit: async () => {
const [err, res] = await intercept(
createServiceGroup({
variables: {
name: steps.name.name,
template: steps.template.id,
capacity: steps.name.capacity
},
update: (proxy, { data: { createGroup: group } }) => {
try {
proxy.writeQuery({
query: ListServiceGroups,
data: {
groups: proxy
.readQuery({ query: ListServiceGroups })
.groups.concat([group])
}
});
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
try {
proxy.writeQuery({
query: GetServiceGroup,
variables: { id: group.id },
data: { group }
});
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}
})
);
if (err) {
throw new SubmissionError({
_error: parseError(err)
});
}
const { data } = res;
const { createGroup: cg } = data;
const { id } = cg;
dispatch([destroyAll(), forms.map(name => destroy(name))]);
history.push(`/service-groups/~create/${id}/success`);
}
}))
)(CreateTemplate);

View File

@ -0,0 +1,215 @@
import React, { Fragment } from 'react';
import { If, Then, Else } from 'react-if';
import ReduxForm from 'declarative-redux-form';
import { SubmissionError, destroy } from 'redux-form';
import { Margin, Padding } from 'styled-components-spacing';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import { set, destroyAll } from 'react-redux-values';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import {
ViewContainer,
Message,
MessageTitle,
MessageDescription,
Button,
StatusLoader
} from 'joyent-ui-toolkit';
import {
PostCreation,
PostCreationContent,
PostCreationTitle
} from 'joyent-ui-resource-widgets';
import { Provider as ResourceSteps } from 'joyent-ui-resource-step';
import parseError from '@state/parse-error';
import { Forms, Values } from '@root/constants';
import ListServiceGroups from '@graphql/list-service-groups.gql';
import UpdateServiceGroup from '@graphql/update-service-group.gql';
import GetServiceGroup from '@graphql/get-service-group.gql';
import Template from './steps/template';
import Name from './steps/name';
const { SGE_F } = Forms;
const { SGE_N_V } = Values;
const EditTemplate = ({
match,
steps,
loading,
initialCapacity,
handleDefocus,
handleSubmit
}) => {
const { params } = match;
const { step, sg } = params;
const { template, name } = steps;
const disabled = name && name.capacity && initialCapacity === name.capacity;
return (
<ViewContainer main>
<Padding top="5">
<If condition={loading}>
<Then>
<StatusLoader />
</Then>
<Else>
<Fragment>
<ResourceSteps namespace={`service-groups/~edit/${sg}`}>
<Margin bottom="4">
<Template next="name" preview={template} readOnly />
</Margin>
<Margin bottom="4">
<Name
expanded={step === 'name'}
saved={get(steps, 'name.name', false)}
onDefocus={handleDefocus(SGE_N_V)}
preview={name}
readOnlyName
/>
</Margin>
</ResourceSteps>
<Margin top="5" bottom="3">
<ReduxForm form={SGE_F} onSubmit={handleSubmit}>
{({ handleSubmit, submitting, error }) => (
<Fragment>
<If condition={error}>
<Then>
<Margin bottom="4">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{error}</MessageDescription>
</Message>
</Margin>
</Then>
</If>
<form onSubmit={handleSubmit}>
<Button loading={submitting} disabled={disabled}>
Update
</Button>
</form>
</Fragment>
)}
</ReduxForm>
</Margin>
</Fragment>
</Else>
</If>
</Padding>
</ViewContainer>
);
};
export const Success = ({ match }) => {
const id = match.params.sg;
return (
<ViewContainer main>
<Margin top="5">
<PostCreation id={id} object="service group">
<PostCreationTitle>
You have successfully updated a service group
</PostCreationTitle>
<PostCreationContent>
Your service group has been updated and is currently being
processed. It should only take a few minutes and the changes will
reflected in your console.
</PostCreationContent>
</PostCreation>
</Margin>
</ViewContainer>
);
};
export default compose(
graphql(UpdateServiceGroup, { name: 'updateServiceGroup' }),
graphql(GetServiceGroup, {
options: ({ match }) => ({
ssr: true,
variables: {
id: get(match, 'params.sg')
}
}),
props: ({ data: { networkStatus, error, group } }) => ({
loading: networkStatus === 1,
error,
group
})
}),
connect(({ form, values = {} }, { group = {} }) => {
const { template, capacity, name } = group;
return {
forms: Object.keys(form),
initialCapacity: capacity,
steps: {
name: get(values, SGE_N_V) || { name, capacity },
template
}
};
}),
connect(
null,
(dispatch, { forms, group, steps, history, updateServiceGroup }) => ({
handleDefocus: name => value => {
return dispatch(set({ name, value }));
},
handleSubmit: async () => {
const [err, res] = await intercept(
updateServiceGroup({
variables: {
id: group.id,
name: group.name,
template: group.template.id,
capacity: steps.name.capacity
},
update: (proxy, { data: { createGroup: group } }) => {
try {
proxy.writeQuery({
query: ListServiceGroups,
data: {
groups: proxy
.readQuery({ query: ListServiceGroups })
.groups.map(g => (g.id === group.id ? group : g))
}
});
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
try {
proxy.writeQuery({
query: GetServiceGroup,
variables: { id: group.id },
data: { group }
});
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}
})
);
if (err) {
throw new SubmissionError({
_error: parseError(err)
});
}
const { data } = res;
const { updateGroup: ug } = data;
const { id } = ug;
dispatch([destroyAll(), forms.map(name => destroy(name))]);
history.push(`/service-groups/~edit/${id}/success`);
}
})
)
)(EditTemplate);

View File

@ -0,0 +1,289 @@
import React, { Fragment } from 'react';
import { If, Then, Else } from 'react-if';
import { connect } from 'react-redux';
import { set } from 'react-redux-values';
import { compose, graphql } from 'react-apollo';
import { change } from 'redux-form';
import ReduxForm from 'declarative-redux-form';
import { Margin } from 'styled-components-spacing';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import isString from 'lodash.isstring';
import sort from 'lodash.sortby';
import reverse from 'lodash.reverse';
import Fuse from 'fuse.js';
import {
ViewContainer,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import ServiceGroupsList, {
Item as ServiceGroupsItem,
EmptyCard as ServiceGroupsEmptyCard,
EmptyRow as ServiceGroupsEmptyRow,
LoadingRow,
BulkFooter
} from '@components/list';
import { Toolbar } from '@components/toolbar';
import ListServiceGroups from '@graphql/list-service-groups.gql';
import RemoveServiceGroup from '@graphql/remove-service-group.gql';
import { Forms, Values } from '@root/constants';
import parseError from '@state/parse-error';
import Confirm from '@state/confirm';
const { SGL_F_F, SGL_T_F } = Forms;
const { SGL_R_V, SGL_E_V, SGL_SB_V, SGL_SO_V } = Values;
const ServiceGroups = ({
filter,
empty,
checked = [],
groups = [],
error = false,
loading = false,
sortBy = 'name',
sortOrder = 'asc',
handleSortBy,
handleToggleCheckAll,
handleRemove
}) => (
<ViewContainer main>
<Margin top="5">
<Margin bottom="3">
<ReduxForm form={SGL_F_F}>
{() => (
<If condition={empty}>
<Else>
<form>
<Toolbar
searchable={filter || groups.length}
searchLabel="Filter service groups"
actionLabel="Create service group"
actionTo="/service-groups/~create/template"
actionable={!(!loading && !groups.length && !filter)}
/>
</form>
</Else>
</If>
)}
</ReduxForm>
</Margin>
<If condition={error}>
<Then>
<Margin bottom="3">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
<If condition={isString(error)}>
<Then>
<Fragment>{error}</Fragment>
</Then>
<Else>
<Fragment>
An error occurred while loading your service groups
</Fragment>
</Else>
</If>
</MessageDescription>
</Message>
</Margin>
</Then>
</If>
<ReduxForm form={SGL_T_F}>
{props => (
<If condition={empty}>
<Then>
<ServiceGroupsEmptyCard />
</Then>
<Else>
<ServiceGroupsList
{...props}
checked={checked.length === groups.length}
sortBy={sortBy}
sortOrder={sortOrder}
onSortBy={newSortBy =>
handleSortBy(newSortBy, { sortOrder, sortBy })
}
onToggleCheckAll={() =>
handleToggleCheckAll(checked.length !== groups.length)
}
>
<If condition={groups.length}>
<Then>
<Fragment>
{groups.map(({ id, removing, ...group }) => (
<If condition={removing}>
<Then>
<LoadingRow key={id}>Removing...</LoadingRow>
</Then>
<Else>
<ServiceGroupsItem key={id} id={id} {...group} />
</Else>
</If>
))}
</Fragment>
</Then>
<Else>
<If condition={loading}>
<Then>
<LoadingRow />
</Then>
<Else>
<ServiceGroupsEmptyRow />
</Else>
</If>
</Else>
</If>
</ServiceGroupsList>
</Else>
</If>
)}
</ReduxForm>
<If condition={checked.length}>
<Then>
<BulkFooter items={checked} onRemove={handleRemove} />
</Then>
</If>
</Margin>
</ViewContainer>
);
export default compose(
graphql(RemoveServiceGroup, { name: 'removeGroup' }),
graphql(ListServiceGroups, {
options: ({ location }) => ({
ssr: true,
pollInterval: 1000
}),
props: ({ data: { error, groups = [], networkStatus, refetch } }) => ({
refetch,
error,
loading: networkStatus === 1,
groups,
index: new Fuse(groups, {
keys: ['id', 'name', 'created']
})
})
}),
connect(
(state, ownProps) => {
const { form, values: flags } = state;
const { groups: items, index, loading, error: loadingError } = ownProps;
const filter = get(form, `${SGL_F_F}.values.filter`, '');
const mutationError = get(flags, SGL_E_V, null);
const sortBy = get(flags, SGL_SB_V, 'name');
const sortOrder = get(flags, SGL_SO_V, 'asc');
const groups = sort(
(filter.length ? index.search(filter) : items).map(
({ id, ...groups }) => ({
...groups,
removing: get(flags, SGL_R_V(id), false),
id
})
),
[sortBy]
);
const values = get(form, `${SGL_T_F}.values`, {});
const checked = groups.length && groups.filter(({ id }) => values[id]);
return {
filter,
empty: !filter && !loading && !groups.length,
error: Boolean(loadingError) || mutationError,
groups: sortOrder === 'asc' ? groups : reverse(groups),
checked,
sortBy,
sortOrder
};
},
(dispatch, { templates = [], refetch, removeGroup }) => {
return {
handleToggleCheckAll: newChecked => {
return dispatch(
templates.map(({ id }) => change(SGL_T_F, id, newChecked))
);
},
handleSortBy: (newSortBy, { sortBy: currentSortBy, sortOrder }) => {
// sort prop is the same, toggle
if (currentSortBy === newSortBy) {
return dispatch(
set({
name: SGL_SO_V,
value: sortOrder === 'desc' ? 'asc' : 'desc'
})
);
}
dispatch([
set({
name: SGL_SO_V,
value: 'desc'
}),
set({
name: SGL_SB_V,
value: newSortBy
})
]);
},
handleRemove: async (ev, checked = []) => {
// eslint-disable-next-line no-alert
if (
!await Confirm(
`Do you want to remove ${
checked.length === 1
? `"${checked[0].name}"`
: `${checked.length} service groups`
}`
)
) {
return;
}
dispatch(
checked.map(({ id }) =>
set({
name: SGL_R_V(id),
value: true
})
)
);
const [err] = await intercept(
Promise.all(
checked.map(({ id }) =>
removeGroup({
variables: { id }
})
)
)
);
await refetch();
dispatch(
[
...checked.map(({ id }) =>
set({
name: SGL_R_V(id),
value: false
})
),
err
? set({
name: SGL_E_V,
value: parseError(err)
})
: undefined
].filter(Boolean)
);
}
};
}
)
)(ServiceGroups);

View File

@ -0,0 +1,171 @@
import React, { PureComponent } from 'react';
import { Margin } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component';
import { compose } from 'react-apollo';
import { Link } from 'react-router-dom';
import ReduxForm from 'declarative-redux-form';
import { connect } from 'react-redux';
import { Field, change } from 'redux-form';
import { withRouter } from 'react-router';
import get from 'lodash.get';
import plur from 'plur';
import Step, {
Header as StepHeader,
Description as StepDescription,
Preview as StepPreview,
Outlet as StepOutlet
} from 'joyent-ui-resource-step';
import {
H2,
P,
FormGroup,
FormLabel,
Input,
FormMeta,
Button,
InstanceCountIcon
} from 'joyent-ui-toolkit';
import { Forms } from '@root/constants';
import { name as validateName } from '@state/validators';
const { SGC_N_F } = Forms;
const Name = ({
history,
handleGetValue,
preview = {},
readOnlyName,
initialValues,
handleValidate,
handlePlusClick,
handleMinusClick,
...props
}) => (
<Step name="name" getValue={handleGetValue} {...props}>
<StepHeader icon={<InstanceCountIcon />}>
Name and instance count
</StepHeader>
<StepDescription>
Input the name of your Service Group and the desired number of instances
this group will aim to maintain. You can scale up or down your Service
Group anytime after commisioning.
</StepDescription>
<StepPreview>
<Margin top="3">
<Margin bottom="2">
<H2>{preview.name}</H2>
</Margin>
<P>
{preview.capacity} {plur('instance', preview.capacity)}
</P>
</Margin>
</StepPreview>
<StepOutlet>
{({ next }) => (
<Margin top="3">
<ReduxForm
form={SGC_N_F}
validate={handleValidate}
initialValues={initialValues}
destroyOnUnmount={false}
forceUnregisterOnUnmount={false}
enableReinitialize
keepDirtyOnReinitialize
>
{({ pristine, invalid }) => (
<form onSubmit={() => history.push(next)}>
<Margin bottom="5">
<FormGroup name="name" fluid field={Field}>
<FormLabel>Service group name</FormLabel>
<Margin top="0.5">
<Flex>
<FlexItem>
<Input
onBlur={null}
type="text"
disabled={readOnlyName}
/>
</FlexItem>
</Flex>
</Margin>
<FormMeta />
</FormGroup>
</Margin>
<Margin bottom="5">
<FormGroup name="capacity" fluid field={Field}>
<FormLabel>Desired number of instances</FormLabel>
<Margin top="0.5">
<Flex>
<FlexItem>
<Input
type="number"
onBlur={null}
onPlusClick={handlePlusClick}
onMinusClick={handleMinusClick}
xSmall
/>
</FlexItem>
</Flex>
</Margin>
<FormMeta />
</FormGroup>
</Margin>
<Button
type="button"
component={Link}
to={next}
disabled={pristine || invalid}
>
Save
</Button>
</form>
)}
</ReduxForm>
</Margin>
)}
</StepOutlet>
</Step>
);
const Container = compose(
withRouter,
connect((store, { preview = {} }) => {
const { form } = store;
return {
handleGetValue: () => get(form, `${SGC_N_F}.values`, {}),
initialValues: {
capacity: 1,
...preview
}
};
}),
connect(null, (dispatch, { handleGetValue }) => ({
handleValidate: validateName,
handlePlusClick: ({ shiftKey, metaKey }) => {
const count = metaKey ? 100 : shiftKey ? 10 : 1;
const capacity = handleGetValue().capacity || 0;
return dispatch(change(SGC_N_F, 'capacity', capacity + count));
},
handleMinusClick: ({ shiftKey, metaKey }) => {
const count = metaKey ? 100 : shiftKey ? 10 : 1;
const capacity = handleGetValue().capacity || 0;
return dispatch(change(SGC_N_F, 'capacity', capacity - count));
}
}))
)(Name);
export default class extends PureComponent {
isValid(values) {
const msgs = validateName(values);
return !msgs || !Object.values(msgs).length;
}
render() {
const { children, ...props } = this.props;
return <Container {...props}>{children}</Container>;
}
}

View File

@ -0,0 +1,333 @@
import React, { Fragment, Component } from 'react';
import { If, Then, Else } from 'react-if';
import ReduxForm from 'declarative-redux-form';
import { connect } from 'react-redux';
import { set } from 'react-redux-values';
import { Field } from 'redux-form';
import { compose, graphql } from 'react-apollo';
import { Link } from 'react-router-dom';
import { Margin, Padding } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import get from 'lodash.get';
import sort from 'lodash.sortby';
import reverse from 'lodash.reverse';
import find from 'lodash.find';
import Fuse from 'fuse.js';
import Step, {
Header as StepHeader,
Description as StepDescription,
Preview as StepPreview,
Outlet as StepOutlet
} from 'joyent-ui-resource-step';
import {
H2,
Label,
Divider,
Button,
FormGroup,
FormLabel,
Input,
TemplateIcon,
StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import TemplatesList, {
Item as TemplatesItem,
EmptyCard as TemplatesEmptyCard,
EmptyRow as TemplatesEmptyRow,
LoadingRow
} from '@components/templates';
import { Global } from '@state/global.js';
import GetTemplate from '@graphql/get-template.gql';
import ListTemplates from '@graphql/list-templates.gql';
import { Forms, Values } from '@root/constants';
const { SGC_T_F, SGC_F_F } = Forms;
const { SGC_T_SB_V, SGC_T_SO_V } = Values;
class Template extends Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
const { preview: prev } = prevProps;
const { preview: next, onDefocus, readOnly } = this.props;
if (!(readOnly && prev !== next && onDefocus)) {
return;
}
onDefocus(next);
}
render() {
const {
loading = false,
empty = false,
handleGetValue,
preview = {},
initialValues,
templates = [],
filter = '',
sortBy = 'name',
sortOrder = 'asc',
expanded = false,
readOnly = false,
error = null,
handleSortBy,
...props
} = this.props;
return (
<Step
name="template"
getValue={handleGetValue}
readOnly={readOnly}
expanded={readOnly ? false : expanded}
{...props}
>
<StepHeader icon={<TemplateIcon />}>Select template</StepHeader>
<StepDescription>
Select the template youd like to deloy your instances from. Once a
Service Group is deployed with a templates, any changes to that
template will not effect the acting service group.
</StepDescription>
<StepPreview>
<Margin top="5">
<If condition={loading && !preview.name}>
<Then>
<StatusLoader />
</Then>
<Else>
<Fragment>
<Margin bottom="2">
<H2>{preview.name}</H2>
</Margin>
<Flex alignCenter>
<FlexItem>
<Padding right="3">
<Label inline>{preview.image}</Label>
</Padding>
</FlexItem>
<Divider vertical />
<FlexItem>
<Padding right="3" left="3">
<Label inline>{preview.package}</Label>
</Padding>
</FlexItem>
<Divider vertical />
<FlexItem>
<Padding right="3" left="3">
<Label inline>
{distanceInWordsToNow(preview.created)}
</Label>
</Padding>
</FlexItem>
</Flex>
</Fragment>
</Else>
</If>
</Margin>
</StepPreview>
<StepOutlet>
{({ next }) => (
<Margin top="5">
<If condition={error}>
<Then>
<Margin bottom="3">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your templates
</MessageDescription>
</Message>
</Margin>
</Then>
</If>
<ReduxForm form={SGC_F_F}>
{props => (
<If condition={empty}>
<Else>
<Margin bottom="3">
<FormGroup name="filter" field={Field}>
<FormLabel>Filter</FormLabel>
<Margin top="0.5">
<Input disabled={!(filter || templates.length)} />
</Margin>
</FormGroup>
</Margin>
</Else>
</If>
)}
</ReduxForm>
<ReduxForm form={SGC_T_F} initialValues={initialValues}>
{({ pristine }) => (
<Fragment>
<If condition={empty}>
<Then>
<TemplatesEmptyCard />
</Then>
<Else>
<TemplatesList
{...props}
sortBy={sortBy}
sortOrder={sortOrder}
onSortBy={newSortBy =>
handleSortBy(newSortBy, { sortOrder, sortBy })
}
>
<If condition={templates.length}>
<Then>
<Fragment>
{templates.map(({ id, ...template }) => (
<TemplatesItem
key={id}
id={id}
{...template}
/>
))}
</Fragment>
</Then>
<Else>
<If condition={loading}>
<Then>
<LoadingRow />
</Then>
<Else>
<TemplatesEmptyRow />
</Else>
</If>
</Else>
</If>
</TemplatesList>
</Else>
</If>
<Margin top="5">
<Button
type="button"
disabled={pristine}
component={Link}
to={next}
>
Save
</Button>
</Margin>
</Fragment>
)}
</ReduxForm>
</Margin>
)}
</StepOutlet>
</Step>
);
}
}
export default compose(
graphql(ListTemplates, {
options: ({ location }) => {
const tmpl = Global().query.template;
return {
ssr: true,
fetchPolicy: tmpl ? 'cache-only' : 'cache-and-network'
};
},
props: ({ data: { error, templates = [], networkStatus } }) => ({
error,
loading: networkStatus === 1,
templates,
index: new Fuse(templates, {
keys: ['id', 'name', 'created']
})
})
}),
graphql(GetTemplate, {
options: ({ match }) => {
const tmpl = Global().query.template;
return {
ssr: true,
fetchPolicy: tmpl ? 'cache-and-network' : 'cache-only',
variables: {
id: tmpl
}
};
},
props: ({ data }) => {
const { variables, networkStatus, error, template } = data;
if (!variables.id) {
return {};
}
return {
readOnly: Boolean(variables.id),
loading: networkStatus === 1,
error,
preview: template
};
}
}),
connect(
(state, ownProps) => {
const { form, values: flags } = state;
const { templates: items, preview = {}, loading, index } = ownProps;
const filter = get(form, `${SGC_F_F}.values.filter`, '');
const sortBy = get(flags, SGC_T_SB_V, 'name');
const sortOrder = get(flags, SGC_T_SO_V, 'asc');
const templates = sort(filter.length ? index.search(filter) : items, [
sortBy
]);
return {
filter,
empty: !filter && !loading && !templates.length,
handleGetValue: () => {
return find(templates, [
'id',
get(form, `${SGC_T_F}.values.template`)
]);
},
initialValues: {
template: preview.id
},
templates: sortOrder === 'asc' ? templates : reverse(templates),
sortBy,
sortOrder
};
},
(dispatch, { templates = [], refetch, removeGroup }) => {
return {
handleSortBy: (newSortBy, { sortBy: currentSortBy, sortOrder }) => {
// sort prop is the same, toggle
if (currentSortBy === newSortBy) {
return dispatch(
set({
name: SGC_T_SO_V,
value: sortOrder === 'desc' ? 'asc' : 'desc'
})
);
}
dispatch([
set({
name: SGC_T_SO_V,
value: 'desc'
}),
set({
name: SGC_T_SB_V,
value: newSortBy
})
]);
}
};
}
)
)(Template);

View File

@ -0,0 +1,141 @@
import React, { Fragment } from 'react';
import { If, Then, Else } from 'react-if';
import { compose, graphql } from 'react-apollo';
import { Margin } from 'styled-components-spacing';
import { connect } from 'react-redux';
import { set } from 'react-redux-values';
import intercept from 'apr-intercept';
import isString from 'lodash.isstring';
import get from 'lodash.get';
import {
ViewContainer,
StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import { Meta, EventLogContainer } from '@components/summary';
import GetServiceGroup from '@graphql/get-service-group.gql';
import RemoveServiceGroup from '@graphql/remove-service-group.gql';
import ListServiceGroups from '@graphql/list-service-groups.gql';
import { Values } from '@root/constants';
import parseError from '@state/parse-error';
import Confirm from '@state/confirm';
const { SGS_R_V, SGS_E_V } = Values;
const Summary = ({
loading = false,
error = null,
group = null,
removing = false,
handleRemove
}) => (
<ViewContainer main>
<Margin top="5">
<If condition={error}>
<Then>
<Margin bottom="3">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
<If condition={isString(error)}>
<Then>
<Fragment>{error}</Fragment>
</Then>
<Else>
<Fragment>
An error occurred while loading your service groups
</Fragment>
</Else>
</If>
</MessageDescription>
</Message>
</Margin>
</Then>
</If>
<If condition={loading && !group}>
<Then>
<StatusLoader />
</Then>
<Else>
<Fragment>
<Margin bottom="5">
<Meta {...group} removing={removing} onRemove={handleRemove} />
</Margin>
<EventLogContainer />
</Fragment>
</Else>
</If>
</Margin>
</ViewContainer>
);
export default compose(
graphql(RemoveServiceGroup, { name: 'removeGroup' }),
graphql(GetServiceGroup, {
options: ({ match }) => ({
ssr: true,
pollInterval: 1000,
variables: {
id: get(match, 'params.sg')
}
}),
props: ({ data: { loading, networkStatus, error, group } }) => ({
loading: networkStatus === 1,
error,
group
})
}),
connect(
({ values }, { group = {}, error: loadingError }) => ({
error: Boolean(loadingError) || get(values, SGS_E_V(group.id), false),
removing: get(values, SGS_R_V(group.id), false)
}),
(dispatch, { history, group, removeGroup }) => ({
handleDefocus: name => value => {
return dispatch(set({ name, value }));
},
handleRemove: async () => {
const { id, name } = group;
if (!await Confirm(`Do you want to remove ${name}?`)) {
return;
}
dispatch(set({ name: SGS_R_V(id), value: true }));
const [err] = await intercept(
removeGroup({
variables: { id },
update: proxy => {
try {
proxy.writeQuery({
query: ListServiceGroups,
data: {
groups: proxy
.readQuery({ query: ListServiceGroups })
.groups.filter(g => g.id !== id)
}
});
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}
})
);
dispatch(
[
set({ name: SGS_R_V(id), value: false }),
err ? set({ name: SGS_E_V(id), value: parseError(err) }) : undefined
].filter(Boolean)
);
history.push('/service-groups');
}
})
)
)(Summary);

View File

@ -0,0 +1,16 @@
mutation createServiceGroup($name: String!, $template: ID!, $capacity: Int!) {
createGroup(name: $name, template: $template, capacity: $capacity) {
id
name
template {
id
name
package
image
created
}
capacity
created
updated
}
}

View File

@ -0,0 +1,16 @@
query group($id: ID!) {
group(id: $id) {
id
name
template {
id
name
package
image
created
}
capacity
created
updated
}
}

View File

@ -0,0 +1,20 @@
query template($id: ID!) {
template(id: $id) {
id
name
package
image
enableFirewall
networks
userdata
metadata {
name
value
}
tags {
name
value
}
created
}
}

View File

@ -0,0 +1,16 @@
query groups {
groups {
id
name
template {
id
name
package
image
created
}
capacity
created
updated
}
}

View File

@ -0,0 +1,9 @@
query templates {
templates {
id
name
package
image
created
}
}

View File

@ -0,0 +1,5 @@
mutation removeServiceGroup($id: ID!) {
deleteGroup(id: $id) {
id
}
}

View File

@ -0,0 +1,21 @@
mutation updateServiceGroup(
$id: ID!
$name: String!
$template: ID!
$capacity: Int!
) {
updateGroup(id: $id, name: $name, template: $template, capacity: $capacity) {
id
name
template {
id
name
package
image
created
}
capacity
created
updated
}
}

View File

@ -0,0 +1,28 @@
const React = require('react');
module.exports = ({
htmlAttrs = {},
bodyAttrs = {},
head = [],
children = null
}) => (
<html {...htmlAttrs}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta name="theme-color" content="#1E313B" />
{head}
</head>
<body {...bodyAttrs}>
<div id="header" />
{children ? null : <div id="root" />}
{children}
<script src="/navigation/static/main.js" />
</body>
</html>
);

View File

@ -0,0 +1,33 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { HelmetProvider } from 'react-helmet-async';
import { ThemeProvider } from 'styled-components';
import { Provider as ReduxProvider } from 'react-redux';
import { ApolloProvider } from 'react-apollo';
import { BrowserRouter } from 'react-router-dom';
import isFunction from 'lodash.isfunction';
import isFinite from 'lodash.isfinite';
import theme from '@state/theme';
import createStore from '@state/redux-store';
import createClient from '@state/apollo-client';
import App from './app';
if (!isFunction(Number.isFinite)) {
Number.isFinite = isFinite;
}
ReactDOM.hydrate(
<ApolloProvider client={createClient()}>
<ThemeProvider theme={theme}>
<ReduxProvider store={createStore()}>
<BrowserRouter>
<HelmetProvider context={{}}>
<App />
</HelmetProvider>
</BrowserRouter>
</ReduxProvider>
</ThemeProvider>
</ApolloProvider>,
document.getElementById('root')
);

View File

@ -0,0 +1,6 @@
module.exports = {
'^redux-form$': '<rootDir>/src/mocks/redux-form',
'^react-responsive$': '<rootDir>/src/mocks/react-responsive',
'^react-router-dom$': '<rootDir>/src/mocks/react-router-dom',
'^declarative-redux-form$': '<rootDir>/src/mocks/declarative-redux-form'
};

View File

@ -0,0 +1,3 @@
import React from 'react';
export default ({ children, ...props }) => React.createElement(children, props);

View File

@ -0,0 +1,7 @@
import React from 'react';
export default ({ query, children }) => (
<span name="react-responsive-mock" query={query}>
{children}
</span>
);

View File

@ -0,0 +1,4 @@
import React from 'react';
export const Field = ({ children, ...rest }) =>
React.createElement('a', rest, children);

Some files were not shown because too many files have changed in this diff Show More