Compare commits

..

No commits in common. "master" and "nav-copy" have entirely different histories.

1053 changed files with 110357 additions and 87068 deletions

View File

@ -1,9 +1,4 @@
packages/*/**
prototypes/*/**
artifacts
reports
.nyc_output
coverage
dist
styleguide
build
consoles/*/lib/app
node_modules

View File

@ -1,10 +1,8 @@
{
"extends": "joyent-portal",
"rules": {
"no-console": 1,
"new-cap": 0,
"jsx-a11y/href-no-hash": 0,
"no-negated-condition": 1,
"camelcase": 1
"new-cap": 0,
"no-console": 0
}
}

2
.gitignore vendored
View File

@ -165,5 +165,3 @@ prototypes/*/package-lock.json
_env*
keys*
/packages/*/public/index.html
/consoles/*/public/index.html

View File

@ -15,7 +15,6 @@ yarn.lock
dist
build
packages/*/lib/app
consoles/*/lib/app
*.ico
*.html

View File

@ -1 +0,0 @@
{}

View File

@ -9,23 +9,24 @@
"build:lib": "echo 0",
"build:bundle": "echo 0",
"prepublish": "echo 0",
"lint": "echo 0",
"lint:ci": "echo 0",
"test": "echo 0",
"test:ci": "echo 0"
},
"dependencies": {
"apr-main": "^4.0.3",
"cloudapi-gql": "^8.0.0",
"brule": "^3.1.0",
"cloudapi-gql": "^7.1.4",
"execa": "^0.10.0",
"graphi": "^5.7.0",
"h2o2": "^8.1.2",
"hapi": "^17.5.0",
"hapi-triton-auth": "^3.0.0",
"hapi-webconsole-nav": "^2.1.0",
"h2o2": "^8.0.1",
"hapi": "^17.3.1",
"hapi-triton-auth": "^2.0.1",
"hapi-webconsole-nav": "^1.2.0",
"inert": "^5.1.0",
"my-joy-images": "*",
"my-joy-instances": "*",
"my-joy-navigation": "*",
"my-joy-service-groups": "*",
"my-joy-templates": "*",
"tsg-graphql": "^1.0.0"
"rollover": "^1.0.0"
}
}

View File

@ -1,12 +1,7 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-images');
const {
PORT = 4003,
@ -30,35 +25,19 @@ Main(async () => {
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
await server.register({
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl,
dcName
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
routes: {
prefix: `/${PREFIX}`
}
]);
});
await server.start();
});

View File

@ -1,12 +1,7 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-instances');
const {
PORT = 4002,
@ -30,35 +25,19 @@ Main(async () => {
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
await server.register({
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl,
dcName
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
routes: {
prefix: `/${PREFIX}`
}
]);
});
await server.start();
});

View File

@ -1,12 +1,7 @@
require('../.env.js');
const Main = require('apr-main');
const Nav = require('hapi-webconsole-nav');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-navigation');
const Regions = require('../data/regions');
const Categories = require('../data/categories');
@ -35,38 +30,22 @@ Main(async () => {
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
await server.register({
plugin: Nav,
options: {
keyPath,
keyId,
apiBaseUrl,
dcName,
baseUrl,
regions: Regions,
accountServices: Account,
categories: Categories
},
{
plugin: Nav,
options: {
keyPath,
keyId,
apiBaseUrl,
dcName,
baseUrl,
regions: Regions,
accountServices: Account,
categories: Categories
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
routes: {
prefix: `/${PREFIX}`
}
]);
});
await server.start();
});

View File

@ -1,7 +1,6 @@
require('../.env.js');
const Hapi = require('hapi');
const Sso = require('hapi-triton-auth');
const Url = require('url');
const {
COOKIE_PASSWORD,
@ -9,10 +8,12 @@ const {
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID,
SDC_URL
SDC_URL,
DC_NAME
} = process.env;
module.exports = async ({ PORT, BASE_URL }) => {
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
const apiBaseUrl = SDC_URL;
@ -49,7 +50,6 @@ module.exports = async ({ PORT, BASE_URL }) => {
server.events.on('log', (event, tags) => {
if (tags.error) {
// eslint-disable-next-line no-console
console.log(event);
}
});
@ -58,7 +58,6 @@ module.exports = async ({ PORT, BASE_URL }) => {
const { tags } = event;
if (tags.includes('error') && event.data && event.data.errors) {
event.data.errors.forEach(error => {
// eslint-disable-next-line no-console
console.log(error);
});
}

View File

@ -1,78 +0,0 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Tsg = require('tsg-graphql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-service-groups');
const {
PORT = 4004,
BASE_URL = `http://0.0.0.0:${PORT}`,
PREFIX = 'service-groups',
DC_NAME,
TSG_URL = 'http://0.0.0.0:3000',
SDC_URL,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
Main(async () => {
const server = await Server({
PORT,
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Tsg,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: TSG_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: SDC_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);
await server.start();
});

View File

@ -1,78 +0,0 @@
require('../.env.js');
const Main = require('apr-main');
const CloudApiGql = require('cloudapi-gql');
const Tsg = require('tsg-graphql');
const Graphi = require('graphi');
const Url = require('url');
const Server = require('./server');
const Ui = require('my-joy-templates');
const {
PORT = 4005,
BASE_URL = `http://0.0.0.0:${PORT}`,
PREFIX = 'templates',
DC_NAME,
TSG_URL = 'http://0.0.0.0:3000',
SDC_URL,
SDC_KEY_PATH,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const dcName = DC_NAME || Url.parse(SDC_URL).host.split('.')[0];
const keyPath = SDC_KEY_PATH;
const keyId = `/${SDC_ACCOUNT}/keys/${SDC_KEY_ID}`;
Main(async () => {
const server = await Server({
PORT,
BASE_URL
});
await server.register([
{
plugin: Graphi,
options: {
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
authStrategy: 'sso'
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Tsg,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: TSG_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: CloudApiGql,
options: {
authStrategy: 'sso',
keyPath,
keyId,
apiBaseUrl: SDC_URL,
dcName
},
routes: {
prefix: `/${PREFIX}`
}
},
{
plugin: Ui
}
]);
await server.start();
});

View File

@ -4,16 +4,7 @@ module.exports = {
'scope-enum': [
2,
'always',
[
'ui-toolkit',
'icons',
'instances',
'navigation',
'bundle',
'images',
'sg',
'templates'
]
['ui-toolkit', 'icons', 'instances', 'navigation', 'bundle', 'images']
]
}
};

View File

@ -1,38 +0,0 @@
import React from 'react';
import { Margin } from 'styled-components-spacing';
import { NavLink } from 'react-router-dom';
import forceArray from 'force-array';
import {
SectionList,
SectionListItem,
SectionListAnchor,
ViewContainer
} from 'joyent-ui-toolkit';
const getMenuItems = (links = []) =>
links.map(({ pathname, name }) => (
<SectionListItem key={pathname}>
<SectionListAnchor to={pathname} component={NavLink}>
{name}
</SectionListAnchor>
</SectionListItem>
));
const Menu = ({ links = [] }) => {
const _links = forceArray(links);
if (!_links.length) {
return null;
}
return (
<ViewContainer plain>
<Margin bottom="5" top="1">
<SectionList>{getMenuItems(_links)}</SectionList>
</Margin>
</ViewContainer>
);
};
export default Menu;

View File

@ -1,28 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,42 +0,0 @@
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';
const {
REACT_APP_GQL_PORT = global.port,
REACT_APP_GQL_PROTOCOL = global.protocol,
REACT_APP_GQL_HOSTNAME = global.hostname
} = process.env;
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 = {}, request = {}) => {
const host = get(request, 'raw.req.headers.host', '');
let cache = new InMemoryCache();
if (global.__APOLLO_STATE__) {
cache = cache.restore(global.__APOLLO_STATE__);
}
return new ApolloClient({
cache,
link: new HttpLink({
uri: host ? `${REACT_APP_GQL_PROTOCOL}//${host}/images/graphql` : URI,
credentials: 'same-origin',
fetch,
headers: {
'X-CSRF-Token': global.cookie.replace(
/(?:(?:^|.*;\s*)crumb\s*=\s*([^;]*).*$)|^.*$/,
'$1'
)
}
}),
...opts
});
};

View File

@ -1,29 +0,0 @@
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: ''
};
}
return {
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

@ -1,133 +0,0 @@
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 = 'instances', 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

@ -1,54 +0,0 @@
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: 'instances'
})
)
}),
monoFont: Object.assign({}, theme.monoFont, {
href: () =>
_mono(
Object.assign(_addr, {
namespace: 'instances'
})
)
})
});
return {
theme: _theme,
createClient,
createStore
};
};
module.exports = Document({
namespace: 'instances/',
assets,
Html: require('./html'),
getState
});

View File

@ -1,14 +0,0 @@
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>Instances</title>
</Helmet>
<Routes />
</RootContainer>
);

View File

@ -1,23 +0,0 @@
import React from 'react';
import { P as BaseP } from 'joyent-ui-toolkit';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import styled from 'styled-components';
const P = styled(BaseP)`
font-weight: 200;
`;
export default ({ href = '', children }) => (
<Row>
<Col xs="12" sm="7">
<P>
{children}{' '}
{href ? (
<a target="__blank" href={href} rel="noopener noreferrer">
Read the docs
</a>
) : null}
</P>
</Col>
</Row>
);

View File

@ -1,173 +0,0 @@
import React from 'react';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { Margin } from 'styled-components-spacing';
import Flex from 'styled-flex-component';
import {
Button,
StickyFooter,
QueryBreakpoints,
StartIcon,
StopIcon,
ResetIcon,
DeleteIcon
} from 'joyent-ui-toolkit';
const { SmallOnly, Medium } = QueryBreakpoints;
export default ({
submitting = false,
statuses = {},
allowedActions = {},
onStart,
onStop,
onReboot,
onRemove
}) => (
<StickyFooter fill="disabled" fixed bottom>
<Row between="xs" middle="xs">
<Col xs="7">
<Flex>
{onStart && [
<SmallOnly key="small-only">
<Button
type="button"
onClick={onStart}
disabled={submitting || !allowedActions.start}
loading={submitting && statuses.starting}
secondary
small
icon
>
<StartIcon disabled={submitting || !allowedActions.start} />
</Button>
</SmallOnly>,
<Margin right="1">
<Medium key="medium">
<Button
type="button"
onClick={onStart}
disabled={submitting || !allowedActions.start}
loading={submitting && statuses.starting}
secondary
icon
>
<Margin right="1">
<StartIcon disabled={submitting || !allowedActions.start} />
</Margin>
<span>Start</span>
</Button>
</Medium>
</Margin>
]}
{onStop && [
<SmallOnly key="small-only">
<Button
type="button"
onClick={onStop}
disabled={submitting || !allowedActions.stop}
loading={submitting && statuses.stopping}
secondary
small
icon
>
<StopIcon disabled={submitting || !allowedActions.stop} />
</Button>
</SmallOnly>,
<Margin right="1">
<Medium key="medium">
<Button
type="button"
onClick={onStop}
disabled={submitting || !allowedActions.stop}
loading={submitting && statuses.stopping}
secondary
icon
>
<Margin right="1">
<StopIcon disabled={submitting || !allowedActions.stop} />
</Margin>
<span>Stop</span>
</Button>
</Medium>
</Margin>
]}
{onReboot && [
<SmallOnly key="small-only">
<Button
type="button"
onClick={onReboot}
disabled={submitting || !allowedActions.reboot}
loading={submitting && statuses.rebooting}
secondary
small
icon
>
<ResetIcon disabled={submitting || !allowedActions.reboot} />
</Button>
</SmallOnly>,
<Margin right="1">
<Medium key="medium">
<Button
type="button"
onClick={onReboot}
disabled={submitting || !allowedActions.reboot}
loading={submitting && statuses.rebooting}
secondary
icon
>
<ResetIcon disabled={submitting || !allowedActions.reboot} />
<span>Reboot</span>
</Button>
</Medium>
</Margin>
]}
</Flex>
</Col>
{onRemove && (
<Col xs="5">
<SmallOnly key="small-only">
<Button
type="button"
onClick={onRemove}
disabled={submitting || !allowedActions.remove}
loading={submitting && statuses.removing}
secondary
error
right
small
icon
>
<DeleteIcon
disabled={submitting}
fill={submitting || !allowedActions.remove ? undefined : 'red'}
/>
</Button>
</SmallOnly>
<Medium key="medium">
<Button
type="button"
onClick={onRemove}
disabled={submitting || !allowedActions.remove}
loading={submitting && statuses.removing}
error
secondary
right
icon
>
<Margin right="1">
<DeleteIcon
disabled={submitting || !allowedActions.remove}
fill={
submitting || !allowedActions.remove ? undefined : 'red'
}
/>
</Margin>
<span>Remove</span>
</Button>
</Medium>
</Col>
)}
</Row>
</StickyFooter>
);

View File

@ -1,394 +0,0 @@
import React from 'react';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import Flex, { FlexItem } from 'styled-flex-component';
import styled from 'styled-components';
import { Margin, Padding } from 'styled-components-spacing';
import titleCase from 'title-case';
import get from 'lodash.get';
import remcalc from 'remcalc';
import { Field } from 'redux-form';
import { ValueBreakpoints as breakpoints } from 'joyent-ui-toolkit';
import {
Card,
CardOutlet,
Divider,
Button,
ButtonGroup,
PopoverButton,
PopoverItem as BasePopoverItem,
H2 as BaseH2,
Label as BaseLabel,
CopiableField,
QueryBreakpoints,
DotIcon,
DeleteIcon,
StartIcon,
StopIcon,
EditIcon,
Input,
FormMeta,
FormGroup
} from 'joyent-ui-toolkit';
import GLOBAL from '@state/global';
const { SmallOnly, Medium } = QueryBreakpoints;
const stateColor = {
PROVISIONING: 'primary',
RUNNING: 'green',
STOPPING: 'grey',
STOPPED: 'grey',
DELETED: 'secondaryActive',
FAILED: 'red'
};
const Label = styled(BaseLabel)`
font-weight: 200;
`;
const GreyLabel = styled(Label)`
opacity: 0.5;
padding-right: ${remcalc(3)};
`;
const TrimedLabel = styled(Label)`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
const Actionable = styled(Margin)`
cursor: pointer;
`;
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)};
@media (max-width: ${remcalc(breakpoints.small.upper)}) {
display: none;
}
`;
const H2 = styled(BaseH2)`
margin: 0;
`;
const PopoverItem = styled(BasePopoverItem)`
padding-bottom: ${remcalc(11)};
`;
export const Meta = ({
created,
updated,
state,
brand,
image,
editingName,
handleSubmit,
editName,
disabled,
submitting,
...instance
}) => [
<Row middle="xs">
<Col xs="12">
<Margin bottom="1">
<H2>
{editingName ? (
<form onSubmit={handleSubmit}>
<Flex alignStart>
<FormGroup name="name" field={Field}>
<Input
onBlur={null}
type="text"
placeholder={instance.name}
disabled={disabled || submitting}
/>
<FormMeta />
</FormGroup>
<Margin left="1">
<Button
type="submit"
disabled={submitting}
loading={submitting}
inline
>
Save
</Button>
</Margin>
</Flex>
</form>
) : (
<Flex>
{instance.name}
<Actionable left="2" onClick={editName}>
<EditIcon />
</Actionable>
</Flex>
)}
</H2>
</Margin>
</Col>
</Row>,
<Margin vertical="1">
<Flex>
<TrimedLabel>
{image && image.name ? titleCase(image.name) : 'Custom Image'}
</TrimedLabel>
<VerticalDivider />
<TrimedLabel>
{brand === 'LX'
? 'Infrastructure container'
: 'Hardware virtual machine'}
</TrimedLabel>
<VerticalDivider />
<TrimedLabel>{(instance.package || {}).name}</TrimedLabel>
<VerticalDivider />
<Flex>
<DotIcon
right={remcalc(6)}
size={remcalc(15)}
color={stateColor[state]}
/>
{titleCase(state)}
</Flex>
</Flex>
<Margin top="1">
<Flex>
<Flex>
<GreyLabel>Created: </GreyLabel>
<Label> {distanceInWordsToNow(created)} ago</Label>
</Flex>
<VerticalDivider />
<Flex>
<GreyLabel>Updated: </GreyLabel>
<Label> {distanceInWordsToNow(updated)} ago</Label>
</Flex>
</Flex>
</Margin>
</Margin>
];
export default ({
instance = {},
starting = false,
stopping = false,
rebooting = false,
removing = false,
onAction,
...props
}) => (
<Row>
<Col xs="12" sm="12" md="9">
<Card>
<CardOutlet>
<Padding all="5">
<Meta {...instance} {...props} />
<Margin top="3">
<Row between="xs">
<Col xs="9">
<Flex>
<FlexItem>
<Margin right="1">
<ButtonGroup>
{instance.state === 'STOPPED' ? (
<Button
type="button"
loading={starting}
disabled={instance.state !== 'STOPPED'}
onClick={() => onAction('start')}
secondary
bold
icon
>
<Margin right="2">
<StartIcon
disabled={instance.state !== 'STOPPED'}
/>
</Margin>
<span>Start</span>
</Button>
) : (
<Button
type="button"
loading={stopping}
disabled={instance.state !== 'RUNNING'}
onClick={() => onAction('stop')}
secondary
bold
icon
>
<Margin right="2">
<StopIcon
disabled={instance.state !== 'RUNNING'}
/>
</Margin>
<span>Stop</span>
</Button>
)}
<PopoverButton secondary>
<PopoverItem
disabled={instance.state === 'RUNNING'}
onClick={() =>
instance.state === 'RUNNING'
? null
: onAction('start')
}
>
Start
</PopoverItem>
<PopoverItem
disabled={instance.state === 'STOPPED'}
onClick={() =>
instance.state === 'STOPPED'
? null
: onAction('reboot')
}
>
Restart
</PopoverItem>
<PopoverItem
disabled={instance.state === 'STOPPED'}
onClick={() =>
instance.state === 'STOPPED'
? null
: onAction('stop')
}
>
Stop
</PopoverItem>
</PopoverButton>
</ButtonGroup>
</Margin>
</FlexItem>
<FlexItem>
<Button
href={`${GLOBAL.origin}/images/~create/${instance.id}`}
target="__blank"
rel="noopener noreferrer"
secondary
bold
icon
>
Create Image
</Button>
</FlexItem>
</Flex>
</Col>
<Col xs="3">
<SmallOnly>
<Button
type="button"
loading={removing}
disabled={
['RUNNING', 'STOPPED'].indexOf(instance.state) < 0
}
onClick={() => onAction('remove')}
secondary
small
right
icon
error
>
<Margin right="2">
<DeleteIcon
fill="red"
disabled={
['RUNNING', 'STOPPED'].indexOf(instance.state) < 0
}
/>
</Margin>
</Button>
</SmallOnly>
<Medium>
<Button
type="button"
loading={removing}
disabled={
['RUNNING', 'STOPPED'].indexOf(instance.state) < 0
}
onClick={() => onAction('remove')}
secondary
bold
right
icon
error
>
<Margin right="2">
<DeleteIcon
fill={
['RUNNING', 'STOPPED'].indexOf(instance.state) >= 0
? 'red'
: undefined
}
disabled={
['RUNNING', 'STOPPED'].indexOf(instance.state) < 0
}
/>
</Margin>
<span>Delete</span>
</Button>
</Medium>
</Col>
</Row>
</Margin>
<Margin bottom="5" top="3">
<Divider height={1} />
</Margin>
<Margin bottom="3">
<CopiableField
text={(instance.id || '').split('-')[0]}
label="Short ID"
/>
</Margin>
<Margin bottom="3">
<CopiableField text={instance.id} label="ID" />
</Margin>
<Margin bottom="3">
<CopiableField text={instance.compute_node} label="CN UUID" />
</Margin>
{instance.image &&
instance.image.id && (
<Margin bottom="3">
<CopiableField text={instance.image.id} label="Image UUID" />
</Margin>
)}
<Margin bottom="3">
<CopiableField
text={`ssh root@${instance.primary_ip}`}
label="Login"
/>
</Margin>
{get(instance, 'ips.public', []).map((ip, i, ips) => (
<Margin bottom="3">
<CopiableField
key={`public-${i}`}
label={`Public IP address ${ips.length > 1 ? i + 1 : ''}`}
text={ip}
/>
</Margin>
))}
{get(instance, 'ips.private', []).map((ip, i, ips) => (
<Margin bottom="3">
<CopiableField
key={`private-${i}`}
noMargin={i === ips.length - 1}
label={`Private IP address ${ips.length > 1 ? i + 1 : ''}`}
text={ip}
/>
</Margin>
))}
</Padding>
</CardOutlet>
</Card>
</Col>
</Row>
);

View File

@ -1,353 +0,0 @@
import React, { Component, Fragment } from 'react';
import { Margin, Padding } from 'styled-components-spacing';
import { graphql, compose } from 'react-apollo';
import ReduxForm from 'declarative-redux-form';
import { connect } from 'react-redux';
import { SubmissionError, destroy } from 'redux-form';
import { set, destroyAll } from 'react-redux-values';
import Flex, { FlexItem } from 'styled-flex-component';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import styled from 'styled-components';
import {
Name,
Image,
Package,
Networks,
Tags,
Metadata,
UserScript,
Firewall,
CNS,
Affinity,
generatePayload,
Footer
} from 'joyent-ui-instance-steps';
import { Provider as ResourceSteps } from 'joyent-ui-resource-step';
import { Anchor, H3, ViewContainer, Button } from 'joyent-ui-toolkit';
import { Forms, Values } from '@root/constants';
import parseError from '@state/parse-error';
import CreateInstanceMutation from '@graphql/create-instance.gql';
const { IC_F } = Forms;
const { IC_SHOW_CLI } = Values;
const names = {
name: 'IC_NAME',
image: 'IC_IMAGE',
package: 'IC_PACKAGE',
networks: 'IC_NETWORKS',
tags: 'IC_TAGS',
metadata: 'IC_METADATA',
'user-script': 'IC_USERSCRIPT',
firewall: 'IC_FIREWALL',
cns: 'IC_CNS',
affinity: 'IC_AFFINITY'
};
class CreateInstance 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 Object.keys(this.isValids).every(name =>
this.isValids[name](steps[name] || {})
);
};
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,
showCli = false,
handleDefocus,
handleToggleShowCli,
handleSubmit,
disabled
} = this.props;
const { params } = match;
const { step } = params;
const {
name,
image,
package: pkg,
networks,
tags,
metadata,
firewall,
cns,
affinity
} = steps;
const Mask = styled.div`
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.25);
position: absolute;
display: ${showCli ? 'block' : 'none'};
`;
return (
<ViewContainer main>
<Margin top="5">
<H3>Create Instance</H3>
</Margin>
<Padding top="5">
<ResourceSteps namespace="instances/~create">
<Margin bottom="5">
<Name
ref={this.setIsValid('name')}
expanded={step === 'name'}
next="image"
saved={get(steps, 'name.name', false)}
onDefocus={handleDefocus('name')}
preview={name}
isValid={this.isStepValid('name')}
/>
</Margin>
<Margin bottom="5">
<Image
ref={this.setIsValid('image')}
expanded={step === 'image'}
next="package"
saved={steps.image && steps.image.id}
onDefocus={handleDefocus('image')}
preview={image}
/>
</Margin>
<Margin bottom="5">
<Package
ref={this.setIsValid('package')}
expanded={step === 'package'}
next="networks"
saved={steps.package}
onDefocus={handleDefocus('package')}
preview={pkg}
/>
</Margin>
<Margin bottom="5">
<Networks
ref={this.setIsValid('networks')}
expanded={step === 'networks'}
next="tags"
saved={steps.networks}
onDefocus={handleDefocus('networks')}
preview={networks}
/>
</Margin>
<Margin bottom="5">
<Tags
ref={this.setIsValid('tags')}
expanded={step === 'tags'}
next="metadata"
saved={steps.tags && steps.tags.length}
onDefocus={handleDefocus('tags')}
preview={tags}
optional
/>
</Margin>
<Margin bottom="5">
<Metadata
ref={this.setIsValid('metadata')}
expanded={step === 'metadata'}
next="user-script"
saved={steps.metadata && steps.metadata.length}
onDefocus={handleDefocus('metadata')}
preview={metadata}
optional
/>
</Margin>
<Margin bottom="5">
<UserScript
ref={this.setIsValid('user-script')}
expanded={step === 'user-script'}
next="firewall"
saved={get(steps, 'user-script.lines', false)}
onDefocus={handleDefocus('user-script')}
preview={steps['user-script']}
optional
/>
</Margin>
<Margin bottom="5">
<Firewall
ref={this.setIsValid('firewall')}
expanded={step === 'firewall'}
next="cns"
saved={steps.firewall}
onDefocus={handleDefocus('firewall')}
preview={firewall}
optional
/>
</Margin>
<Margin bottom="5">
<CNS
ref={this.setIsValid('cns')}
expanded={step === 'cns'}
next="affinity"
saved={steps.cns}
onDefocus={handleDefocus('cns')}
preview={cns}
optional
/>
</Margin>
<Margin bottom="5">
<Affinity
ref={this.setIsValid('affinity')}
expanded={step === 'affinity'}
next=""
saved={steps.affinity}
onDefocus={handleDefocus('affinity')}
preview={affinity}
optional
/>
</Margin>
</ResourceSteps>
<Margin bottom="5">
<Flex alignCenter>
<FlexItem>
<ReduxForm form={IC_F} onSubmit={handleSubmit}>
{({ handleSubmit, submitting }) => (
<form onSubmit={handleSubmit}>
<Button
disabled={disabled || !this.isFormValid()}
loading={submitting}
>
Deploy
</Button>
</form>
)}
</ReduxForm>
</FlexItem>
<FlexItem>
<Margin left={3}>
<Anchor
disabled={disabled || !this.isFormValid()}
onClick={() => handleToggleShowCli(!showCli)}
>
View CLI Details
</Anchor>
</Margin>
</FlexItem>
</Flex>
</Margin>
</Padding>
{showCli ? (
<Fragment>
<Footer
getData={() => generatePayload(steps)}
onCloseCli={() => handleToggleShowCli(!showCli)}
/>
<Mask onClick={() => handleToggleShowCli(!showCli)} />
</Fragment>
) : null}
</ViewContainer>
);
}
}
export default compose(
graphql(CreateInstanceMutation, { name: 'createInstance' }),
connect(({ form, values = {} }, { match, location }) => {
const steps = {
name: values[names.name],
image: values[names.image],
package: values[names.package],
networks: values[names.networks],
tags: values[names.tags],
metadata: values[names.metadata],
'user-script': values[names['user-script']],
firewall: values[names.firewall],
cns: values[names.cns],
affinity: values[names.affinity]
};
const error = get(form, `${IC_F}.error`, null);
const showCli = Boolean(values[IC_SHOW_CLI]);
// Maybe re-use saved to only write the rule once
const disabled = !(
!error &&
steps.name &&
steps.name.name &&
steps.image &&
steps.image.id &&
steps.package &&
steps.package.id &&
steps.networks &&
Array.isArray(steps.networks)
);
return {
disabled,
showCli,
forms: Object.keys(form), // improve this
error,
steps
};
}),
connect(null, (dispatch, { steps = {}, forms, history, createInstance }) => {
return {
handleToggleShowCli: value => {
return dispatch(set({ name: IC_SHOW_CLI, value }));
},
handleDefocus: name => value => {
return dispatch(set({ name: names[name], value }));
},
handleSubmit: async () => {
const [err, res] = await intercept(
createInstance({
variables: generatePayload(steps)
})
);
if (err) {
throw new SubmissionError({
_error: parseError(err)
});
}
dispatch([destroyAll(), forms.map(name => destroy(name))]);
history.push(`/instances/${res.data.createMachine.id}`);
}
};
})
)(CreateInstance);

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