build(my-joy-beta): initial ssr support

fixes #1247
This commit is contained in:
Sérgio Ramos 2018-02-20 00:35:31 +00:00 committed by Sérgio Ramos
parent 05550dd570
commit 318a510ee8
93 changed files with 1440 additions and 626 deletions

0
bundle/.yarnclean Normal file
View File

View File

@ -4,7 +4,7 @@
"private": true,
"license": "MPL-2.0",
"scripts": {
"start": "NODE_ENV=development PORT=3069 REACT_APP_GQL_PORT=3069 REACT_APP_GQL_PROTOCOL=http node src/index.js",
"start": "UMD=1 NODE_ENV=development PORT=3069 REACT_APP_GQL_PORT=3069 REACT_APP_GQL_HOSTNAME=localhost REACT_APP_GQL_PROTOCOL=http node src/index.js",
"lint-ci": "echo 0",
"lint": "echo 0",
"test-ci": "echo 0",
@ -12,12 +12,14 @@
"prepublish": "echo 0"
},
"dependencies": {
"brok": "^2.0.0",
"brule": "^3.1.0",
"cloudapi-gql": "^4.6.0",
"hapi": "^17.2.0",
"hapi-triton-auth": "^1.0.0",
"inert": "^5.1.0",
"joyent-navigation": "*",
"my-images-console": "*",
"my-joy-beta": "*",
"rollover": "^1.0.0"
}

View File

@ -3,6 +3,7 @@
const Brule = require('brule');
const Hapi = require('hapi');
const Rollover = require('rollover');
const Brok = require('brok');
const { homedir } = require('os');
const { join } = require('path');
@ -10,7 +11,6 @@ process.env.SDC_KEY_PATH =
process.env.SDC_KEY_PATH || join(homedir(), '.ssh/id_rsa');
const Sso = require('hapi-triton-auth');
const Ui = require('my-joy-beta');
const Nav = require('joyent-navigation');
const Api = require('cloudapi-gql');
@ -24,16 +24,28 @@ const {
SDC_URL,
BASE_URL = `http://0.0.0.0:${PORT}`,
ROLLBAR_SERVER_TOKEN,
NODE_ENV = 'development'
NODE_ENV = 'development',
CONSOLE = 'my-joy-beta'
} = process.env;
const Ui = require(CONSOLE);
const server = Hapi.server({
compression: {
minBytes: 1
},
debug: {
request: ['error']
},
port: PORT,
host: '127.0.0.1'
});
async function main() {
await server.register([
{
plugin: Brok
},
{
plugin: Rollover,
options: {

View File

@ -20,16 +20,16 @@
"dependencies": {
"remcalc": "^1.0.10",
"rnd-id": "^2.0.2",
"styled-components": "^3.1.4"
"styled-components": "^3.1.6"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-joyent-portal": "^6.0.3",
"eslint": "^4.16.0",
"babel-preset-joyent-portal": "^7.0.1",
"eslint": "^4.18.1",
"eslint-config-joyent-portal": "^3.3.1",
"joyent-react-scripts": "^7.3.0",
"react": "^16.2.0",
"redrun": "^5.10.0"
"redrun": "^5.10.5"
},
"peerDependencies": {
"react": "^16.2.0"

View File

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

View File

@ -1,4 +1,5 @@
.nyc_output
coverage
dist
build
build
lib/app

View File

@ -21,3 +21,5 @@ yarn-error.log*
**/__diff_output__
lib/app

View File

@ -1,22 +1,104 @@
const Inert = require('inert');
const Path = require('path');
const Execa = require('execa');
const { readFile } = require('mz/fs');
const RenderReact = require('hapi-render-react');
const Wreck = require('wreck');
const Url = require('url');
exports.register = async server => {
await Execa('npm', ['run', 'build'], {
cwd: Path.join(__dirname, '..'),
stdio: 'inherit'
});
const indexFile = await readFile(
Path.join(__dirname, '../build/index.html'),
'utf-8'
);
await server.register(Inert);
await server.register([
{
plugin: Inert
},
{
plugin: RenderReact,
options: {
relativeTo: Path.join(__dirname, 'app')
}
}
]);
server.route([
{
method: 'GET',
path: '/service-worker.js',
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/service-worker.js')
}
}
}
},
{
method: 'GET',
path: '/favicon.ico',
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/favicon.ico')
}
}
}
},
{
method: 'GET',
path: '/font/{pathname*}',
config: {
auth: false,
handler: async (request, h) => {
const { params } = request;
const { pathname } = params;
const location = Url.format({
protocol: 'https:',
slashes: true,
host: 'fonts.gstatic.com',
pathname
});
const res = await Wreck.request('GET', location);
return h.response(res);
}
}
},
{
method: 'GET',
path: '/fonts/css',
config: {
auth: false,
handler: async (request, h) => {
const { query, headers } = request;
const { family } = query;
const { host } = headers;
const url = Url.parse(`http://${host}`);
const location = Url.format({
protocol: 'https:',
slashes: true,
host: 'fonts.googleapis.com',
pathname: '/css',
query: { family }
});
const res = await Wreck.request('GET', location);
const body = await Wreck.read(res);
const _body = body.toString().replace(
/https:\/\/fonts\.gstatic\.com/g,
`http://${url.host}/font`
);
return h
.response(_body)
.header('content-type', res.headers['content-type'])
.header('expires', res.headers.expires)
.header('date', res.headers.date)
.header('cache-control', res.headers['cache-control']);
}
}
},
{
method: 'GET',
path: '/static/{path*}',
@ -31,12 +113,21 @@ exports.register = async server => {
}
}
},
{
method: '*',
path: '/~server-error',
handler: {
view: {
name: 'server-error'
}
}
},
{
method: '*',
path: '/{path*}',
config: {
handler: (request, h) => {
return h.response(indexFile).type('text/html');
handler: {
view: {
name: 'app'
}
}
}

View File

@ -8,23 +8,28 @@
"scripts": {
"dev": "REACT_APP_GQL_PORT=4000 PORT=3070 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start",
"start": "PORT=3069 joyent-react-scripts start",
"build": "NODE_ENV=production joyent-react-scripts build",
"build:app": "NODE_ENV=production joyent-react-scripts build",
"build:lib": "NODE_ENV=production SSR=1 UMD=1 babel src --out-dir lib/app --copy-files",
"lint-ci": "eslint . --ext .js --ext .md",
"lint": "eslint . --fix --ext .js --ext .md",
"test-ci": "NODE_ENV=test joyent-react-scripts test --env=jsdom --testPathIgnorePatterns='.ui.js'",
"test": "DEFAULT_TIMEOUT_INTERVAL=100000 NODE_ENV=test joyent-react-scripts test --env=jsdom",
"prepublish": "echo 0"
"postinstall": "npm run build:app",
"prepublish": "npm run build:lib"
},
"dependencies": {
"@manaflair/redux-batch": "^0.1.0",
"apollo": "^0.2.2",
"apollo-cache-inmemory": "^1.1.7",
"apollo-client": "^2.2.3",
"apollo-link-http": "^1.3.3",
"apollo-cache-inmemory": "^1.1.9",
"apollo-client": "^2.2.5",
"apollo-link-http": "^1.5.1",
"apr-intercept": "^3.0.3",
"babel-preset-joyent-portal": "^7.0.1",
"date-fns": "^1.29.0",
"declarative-redux-form": "^2.0.8",
"force-array": "^3.1.0",
"hapi-render-react": "^2.1.0",
"hapi-render-react-joyent-document": "^4.2.3",
"joyent-logo-assets": "^1.0.0",
"joyent-react-styled-flexboxgrid": "^2.2.3",
"joyent-ui-toolkit": "^5.0.0",
@ -41,7 +46,7 @@
"react": "^16.2.0",
"react-apollo": "^2.0.4",
"react-dom": "^16.2.0",
"react-redux": "^5.0.6",
"react-redux": "^5.0.7",
"react-redux-values": "^1.1.2",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
@ -51,17 +56,17 @@
"scroll-to-element": "^2.0.0",
"styled-components": "^3.1.6",
"styled-components-spacing": "^2.1.3",
"styled-flex-component": "^2.2.0",
"styled-flex-component": "^2.2.1",
"styled-is": "^1.1.2",
"title-case": "^2.1.1"
},
"devDependencies": {
"babel-preset-joyent-portal": "^6.0.3",
"eslint": "^4.16.0",
"babel-cli": "^6.26.0",
"eslint": "^4.18.1",
"eslint-config-joyent-portal": "^3.3.1",
"jest-image-snapshot": "^2.3.0",
"jest-styled-components": "^4.11.0-0",
"joyent-react-scripts": "^7.2.2",
"joyent-react-scripts": "^7.3.0",
"react-screenshot-renderer": "^1.1.2",
"react-test-renderer": "^16.2.0"
}

View File

@ -6,16 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#1E313B">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> -->
<!-- <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> -->
<title>My Joyent Images &beta;</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="header"></div>
<div id="root"></div>
<script src="/nav-static/main.js"></script>
</body>
</html>

View File

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

View File

@ -0,0 +1,34 @@
const get = require('lodash.get');
const Document = require('hapi-render-react-joyent-document');
const path = require('path');
const url = require('url');
const { default: theme } = require('./state/theme');
const { default: createClient } = require('./state/apollo-client');
const { default: createStore } = require('./state/redux-store');
const indexFile = path.join(__dirname, '../../build/index.html');
const getState = request => {
const { req, res } = request.raw;
const _font = get(theme, 'font.href', () => '');
const _mono = get(theme, 'monoFont.href', () => '');
const _addr = url.parse(`http://${req.headers.host}`);
const _theme = Object.assign({}, theme, {
font: Object.assign({}, theme.font, {
href: () => _font(_addr)
}),
monoFont: Object.assign({}, theme.monoFont, {
href: () => _mono(_addr)
})
});
return {
theme: _theme,
createClient,
createStore
};
};
module.exports = Document({ indexFile, getState });

View File

@ -1,21 +1,10 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { Provider as ReduxProvider } from 'react-redux';
import { ApolloProvider } from 'react-apollo';
import { theme, RootContainer } from 'joyent-ui-toolkit';
import { client, store } from '@state/store';
import Router from '@root/router';
import { RootContainer } from 'joyent-ui-toolkit';
import Routes from '@root/routes';
export default () => (
<ApolloProvider client={client}>
<ThemeProvider theme={theme}>
<ReduxProvider store={store}>
<RootContainer>
<Router />
</RootContainer>
</ReduxProvider>
</ThemeProvider>
</ApolloProvider>
<RootContainer>
<Routes />
</RootContainer>
);

View File

@ -95,7 +95,7 @@ export const Image = ({
height: '24'
})}
</Margin>
<A to={`/${name}`} component={Link}>
<A to={`/${name}/summary`} component={Link}>
{name}
</A>
</Flex>

View File

@ -17,7 +17,7 @@ import Animated from '@containers/create-image/animated';
import Details from '@components/create-image/details';
import Description from '@components/description';
import GetRandomName from '@graphql/get-random-name.gql';
import { client } from '@state/store';
import createStore from '@state/apollo-client';
import { Forms } from '@root/constants';
const NameContainer = ({
@ -111,7 +111,10 @@ const NameContainer = ({
export default compose(
Animated,
graphql(GetRandomName, {
fetchPolicy: 'network-only',
options: () => ({
fetchPolicy: 'network-only',
ssr: false
}),
props: ({ data }) => ({
placeholderName: data.rndName || ''
})
@ -168,7 +171,7 @@ export default compose(
dispatch(set({ name: 'create-image-name-randomizing', value: true }));
const [err, res] = await intercept(
client.query({
createStore().query({
fetchPolicy: 'network-only',
query: GetRandomName
})

View File

@ -95,6 +95,7 @@ export default compose(
graphql(CreateImage, { name: 'createImage' }),
graphql(GetInstance, {
options: ({ match }) => ({
ssr: false,
variables: {
name: get(match, 'params.instance')
}

View File

@ -99,8 +99,10 @@ export default compose(
name: 'removeImage'
}),
graphql(ListImages, {
fetchPolicy: 'network-only',
pollInterval: 1000,
options: () => ({
ssr: false,
pollInterval: 1000
}),
props: ({ data: { images, loading, error, refetch } }) => ({
images,
loading,

View File

@ -67,6 +67,7 @@ export default compose(
graphql(RemoveImage, { name: 'removeImage' }),
graphql(GetImage, {
options: ({ match }) => ({
ssr: false,
variables: {
name: get(match, 'params.image')
}

View File

@ -116,6 +116,7 @@ export default compose(
}),
graphql(GetTags, {
options: ({ match }) => ({
ssr: false,
fetchPolicy: 'network-only',
pollInterval: 1000,
variables: {

View File

@ -1,8 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider, consolidateStreamedStyles } 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 'joyent-ui-toolkit';
import createStore from '@state/redux-store';
import createClient from '@state/apollo-client';
import { register } from './sw';
import App from './app';
@ -10,6 +18,19 @@ if (!isFunction(Number.isFinite)) {
Number.isFinite = isFinite;
}
ReactDOM.render(<App />, document.getElementById('root'));
consolidateStreamedStyles();
ReactDOM.hydrate(
<ApolloProvider client={createClient()}>
<ThemeProvider theme={theme}>
<ReduxProvider store={createStore()}>
<BrowserRouter>
<App />
</BrowserRouter>
</ReduxProvider>
</ThemeProvider>
</ApolloProvider>,
document.getElementById('root')
);
register();

View File

@ -1 +1,8 @@
module.exports = {};
module.exports = {
'^joyent-ui-toolkit/dist/es/editor$': '<rootDir>/src/mocks/editor',
'^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',
'^scroll-to-element': '<rootDir>/src/mocks/scroll-to-element'
};

View File

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

View File

@ -0,0 +1,3 @@
import React from 'react';
export default () => <span>joyent-maifest-editor</span>;

View File

@ -0,0 +1,3 @@
export { default as Router } from './router';
export { default as Store } from './store';
export { default as Theme } from './theme';

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);

View File

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

View File

@ -0,0 +1,4 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
export default ({ children }) => <MemoryRouter>{children}</MemoryRouter>;

View File

@ -0,0 +1 @@
export default () => null;

View File

@ -0,0 +1,8 @@
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import createClient from '@state/apollo-client';
export default ({ children }) => (
<ApolloProvider client={createClient()}>{children}</ApolloProvider>
);

View File

@ -0,0 +1,22 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import {
theme,
RootContainer,
PageContainer,
ViewContainer
} from 'joyent-ui-toolkit';
export default ({ children, ss }) => (
<ThemeProvider theme={theme}>
{ss ? (
<RootContainer>
<PageContainer>
<ViewContainer>{children}</ViewContainer>
</PageContainer>
</RootContainer>
) : (
children
)}
</ThemeProvider>
);

View File

@ -1,54 +0,0 @@
import React from 'react';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import get from 'lodash.get';
import { PageContainer } from 'joyent-ui-toolkit';
import Breadcrumb from '@containers/breadcrumb';
import Menu from '@containers/menu';
import List from '@containers/list';
import Summary from '@containers/summary';
import Create from '@containers/create';
import Tags from '@containers/tags';
import Footer from '@components/footer';
export default () => (
<BrowserRouter>
<PageContainer>
{/* Breadcrumb */}
<Switch>
<Route path="/~create/:instance/:step?" exact component={Breadcrumb} />
<Route path="/:image?" component={Breadcrumb} />
</Switch>
{/* Menu */}
<Switch>
<Route path="/:image/:section?" component={Menu} />
<Route path="/~create/:instance/:step?" component={() => {}} />
</Switch>
{/* Images */}
<Switch>
<Route path="/" exact component={List} />
<Route path="/:image/summary" exact component={Summary} />
<Route path="/:image/tags" exact component={Tags} />
<Route
path="/:image"
exact
component={({ match }) => (
<Redirect to={`/${get(match, 'params.image')}/summary`} />
)}
/>
</Switch>
{/* Create Image */}
<Switch>
<Route
path="/~create/:instance?"
exact
component={({ match }) => (
<Redirect to={`/~create/${match.params.instance}/name`} />
)}
/>
<Route path="/~create/:instance/:step" component={Create} />
</Switch>
<Footer />
</PageContainer>
</BrowserRouter>
);

View File

@ -0,0 +1,80 @@
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import get from 'lodash.get';
import {
PageContainer,
RootContainer,
ViewContainer,
Message,
MessageDescription,
MessageTitle
} from 'joyent-ui-toolkit';
import Breadcrumb from '@containers/breadcrumb';
import Menu from '@containers/menu';
import List from '@containers/list';
import Summary from '@containers/summary';
import Create from '@containers/create';
import Tags from '@containers/tags';
import Footer from '@components/footer';
import { Route as ServerError } from '@root/server-error';
export default () => (
<PageContainer>
{/* Breadcrumb */}
<Switch>
<Route path="/~server-error" component={Breadcrumb} />
<Route path="/~create/:instance/:step?" exact component={Breadcrumb} />
<Route path="/:image?" component={Breadcrumb} />
</Switch>
{/* Menu */}
<Switch>
<Route path="/~server-error" component={() => null} />
<Route path="/:image/:section?" component={Menu} />
<Route path="/~create/:instance/:step?" component={() => {}} />
</Switch>
{/* Images */}
<Switch>
<Route path="/~server-error" component={() => null} />
<Route path="/" exact component={List} />
<Route path="/:image/summary" exact component={Summary} />
<Route path="/:image/tags" exact component={Tags} />
<Route
path="/:image"
exact
component={({ match }) => (
<Redirect to={`/${get(match, 'params.image')}/summary`} />
)}
/>
</Switch>
{/* Create Image */}
<Switch>
<Route
path="/~create/:instance?"
exact
component={({ match }) => (
<Redirect to={`/~create/${match.params.instance}/name`} />
)}
/>
<Route path="/~create/:instance/:step" component={Create} />
</Switch>
<Route path="/~server-error" component={ServerError} />
<noscript>
<ViewContainer main>
<Message warning>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
You need to enable JavaScript to run this app.
</MessageDescription>
</Message>
</ViewContainer>
</noscript>
<Footer />
</PageContainer>
);

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Margin } from 'styled-components-spacing';
import remcalc from 'remcalc';
import {
RootContainer,
PageContainer,
ViewContainer,
Message,
MessageDescription,
MessageTitle,
Divider
} from 'joyent-ui-toolkit';
import Breadcrumb from '@containers/breadcrumb';
export const Route = () => (
<ViewContainer main>
<Divider height={remcalc(30)} transparent />
<Margin bottom={4}>
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your page
</MessageDescription>
</Message>
</Margin>
</ViewContainer>
);
export default () => (
<RootContainer>
<PageContainer>
<Breadcrumb />
<Route />
</PageContainer>
</RootContainer>
);

View File

@ -0,0 +1,33 @@
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import fetch from 'cross-fetch';
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}/graphql`;
export default (opts = {}) => {
let cache = new InMemoryCache();
if (global.__APOLLO_STATE__) {
cache = cache.restore(global.__APOLLO_STATE__);
}
return new ApolloClient({
cache,
link: new HttpLink({
uri: URI,
credentials: 'same-origin',
fetch
}),
...opts
});
};

View File

@ -0,0 +1,16 @@
import { canUseDOM } from 'exenv';
export default (() => {
if (!canUseDOM) {
return {};
}
return {
port: window.location.port,
protocol: window.location.protocol.replace(/:$/, ''),
hostname: window.location.hostname,
__REDUX_DEVTOOLS_EXTENSION__: window.__REDUX_DEVTOOLS_EXTENSION__,
__APOLLO_STATE__: window.__APOLLO_STATE__,
__REDUX_STATE__: window.__REDUX_STATE__
};
})();

View File

@ -0,0 +1,27 @@
import { reduxBatch } from '@manaflair/redux-batch';
import { createStore, combineReducers, compose } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { reducer as valuesReducer } from 'react-redux-values';
import paramCase from 'param-case';
import global from './global';
const initialState = {};
export default () => {
return createStore(
combineReducers({
values: valuesReducer,
form: formReducer,
ui: (state = {}) => state
}),
global.__REDUX_STATE__ || initialState,
compose(
reduxBatch,
// If you are using the devToolsExtension, you can add it here also
// eslint-disable-next-line no-negated-condition
typeof global.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined'
? global.__REDUX_DEVTOOLS_EXTENSION__()
: f => f
)
);
};

View File

@ -1,39 +0,0 @@
import { reduxBatch } from '@manaflair/redux-batch';
import { createStore, combineReducers, compose } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { reducer as valuesReducer } from 'react-redux-values';
const {
REACT_APP_GQL_PORT = 443,
REACT_APP_GQL_PROTOCOL = 'https',
REACT_APP_GQL_HOSTNAME = window.location.hostname
} = process.env;
export const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: `${REACT_APP_GQL_PROTOCOL}://${REACT_APP_GQL_HOSTNAME}:${REACT_APP_GQL_PORT}/graphql`
})
});
const initialState = {};
export const store = createStore(
combineReducers({
values: valuesReducer,
form: formReducer,
ui: (state = {}) => state
}),
initialState,
compose(
reduxBatch,
// If you are using the devToolsExtension, you can add it here also
// eslint-disable-next-line no-negated-condition
typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined'
? window.__REDUX_DEVTOOLS_EXTENSION__()
: f => f
)
);

View File

@ -19,17 +19,17 @@
"apr-for-each": "^3.0.3",
"apr-main": "^4.0.3",
"babel-cli": "^6.26.0",
"babel-preset-joyent-portal": "^6.0.3",
"eslint": "^4.16.0",
"babel-preset-joyent-portal": "^7.0.1",
"eslint": "^4.18.1",
"eslint-config-joyent-portal": "^3.3.1",
"execa": "^0.9.0",
"globby": "^7.1.1",
"globby": "^8.0.1",
"htmltojsx": "^0.3.0",
"joyent-react-scripts": "^7.2.2",
"joyent-react-scripts": "^7.3.0",
"mz": "^2.7.0",
"prettier": "^1.10.2",
"react": "^16.2.0",
"redrun": "^5.10.0"
"redrun": "^5.10.5"
},
"peerDependencies": {
"react": "^16.2.0"

View File

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

View File

@ -1,4 +1,5 @@
.nyc_output
coverage
dist
build
build
lib/app

View File

@ -21,3 +21,5 @@ yarn-error.log*
**/__diff_output__
lib/app

View File

@ -1,16 +1,104 @@
const Inert = require('inert');
const Path = require('path');
const { readFile } = require('mz/fs');
const RenderReact = require('hapi-render-react');
const Wreck = require('wreck');
const Url = require('url');
exports.register = async server => {
const indexFile = await readFile(
Path.join(__dirname, '../build/index.html'),
'utf-8'
);
await server.register(Inert);
await server.register([
{
plugin: Inert
},
{
plugin: RenderReact,
options: {
relativeTo: Path.join(__dirname, 'app')
}
}
]);
server.route([
{
method: 'GET',
path: '/service-worker.js',
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/service-worker.js')
}
}
}
},
{
method: 'GET',
path: '/favicon.ico',
config: {
auth: false,
handler: {
file: {
path: Path.join(__dirname, '../build/favicon.ico')
}
}
}
},
{
method: 'GET',
path: '/font/{pathname*}',
config: {
auth: false,
handler: async (request, h) => {
const { params } = request;
const { pathname } = params;
const location = Url.format({
protocol: 'https:',
slashes: true,
host: 'fonts.gstatic.com',
pathname
});
const res = await Wreck.request('GET', location);
return h.response(res);
}
}
},
{
method: 'GET',
path: '/fonts/css',
config: {
auth: false,
handler: async (request, h) => {
const { query, headers } = request;
const { family } = query;
const { host } = headers;
const url = Url.parse(`http://${host}`);
const location = Url.format({
protocol: 'https:',
slashes: true,
host: 'fonts.googleapis.com',
pathname: '/css',
query: { family }
});
const res = await Wreck.request('GET', location);
const body = await Wreck.read(res);
const _body = body.toString().replace(
/https:\/\/fonts\.gstatic\.com/g,
`http://${url.host}/font`
);
return h
.response(_body)
.header('content-type', res.headers['content-type'])
.header('expires', res.headers.expires)
.header('date', res.headers.date)
.header('cache-control', res.headers['cache-control']);
}
}
},
{
method: 'GET',
path: '/static/{path*}',
@ -25,12 +113,21 @@ exports.register = async server => {
}
}
},
{
method: '*',
path: '/~server-error',
handler: {
view: {
name: 'server-error'
}
}
},
{
method: '*',
path: '/{path*}',
config: {
handler: (request, h) => {
return h.response(indexFile).type('text/html');
handler: {
view: {
name: 'app'
}
}
}

View File

@ -8,29 +8,34 @@
"scripts": {
"dev": "REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start",
"start": "PORT=3069 joyent-react-scripts start",
"build": "NODE_ENV=production joyent-react-scripts build",
"build:app": "NODE_ENV=production joyent-react-scripts build",
"build:lib": "NODE_ENV=production SSR=1 UMD=1 babel src --out-dir lib/app --copy-files",
"lint-ci": "eslint . --ext .js --ext .md",
"lint": "eslint . --fix --ext .js --ext .md",
"test-ci": "NODE_ENV=test joyent-react-scripts test --env=jsdom --testPathIgnorePatterns='.ui.js'",
"test": "DEFAULT_TIMEOUT_INTERVAL=100000 NODE_ENV=test joyent-react-scripts test --env=jsdom",
"postinstall": "npm run build",
"prepublish": "echo 0"
"postinstall": "npm run build:app",
"prepublish": "npm run build:lib"
},
"dependencies": {
"@manaflair/redux-batch": "^0.1.0",
"apollo": "0.2.2",
"apollo": "^0.2.2",
"apr-intercept": "^3.0.3",
"babel-preset-joyent-portal": "^6.0.3",
"babel-preset-joyent-portal": "^7.0.1",
"bytes": "^3.0.0",
"clipboard-copy": "^1.2.1",
"clipboard-copy": "^1.4.2",
"constant-case": "^2.0.0",
"cross-fetch": "^1.1.1",
"date-fns": "^1.29.0",
"declarative-redux-form": "^2.0.8",
"execa": "^0.9.0",
"exenv": "^1.2.2",
"fuse.js": "^3.2.0",
"hapi-render-react": "^2.1.0",
"hapi-render-react-joyent-document": "^4.2.3",
"inert": "^5.1.0",
"joyent-logo-assets": "^1.0.0",
"joyent-manifest-editor": "^1.4.0",
"joyent-manifest-editor": "^3.0.1",
"joyent-react-scripts": "^7.3.0",
"joyent-react-styled-flexboxgrid": "^2.2.3",
"joyent-ui-toolkit": "^5.0.0",
@ -41,7 +46,7 @@
"lodash.isarray": "^4.0.0",
"lodash.isboolean": "^3.0.3",
"lodash.isfinite": "^3.3.2",
"lodash.isfunction": "^3.0.8",
"lodash.isfunction": "^3.0.9",
"lodash.isstring": "^4.0.1",
"lodash.omit": "^4.5.0",
"lodash.reverse": "^4.0.1",
@ -56,22 +61,23 @@
"react": "^16.2.0",
"react-apollo": "^2.0.4",
"react-dom": "^16.2.0",
"react-redux": "^5.0.6",
"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-actions": "^2.2.1",
"redux-form": "^7.2.1",
"redux-form": "^7.2.3",
"remcalc": "^1.0.10",
"scroll-to-element": "^2.0.0",
"styled-components": "^3.1.4",
"styled-components": "^3.1.6",
"styled-components-spacing": "^2.1.3",
"styled-flex-component": "^2.2.0",
"styled-flex-component": "^2.2.1",
"title-case": "^2.1.1"
},
"devDependencies": {
"eslint": "^4.16.0",
"babel-cli": "^6.26.0",
"eslint": "^4.18.1",
"eslint-config-joyent-portal": "^3.3.1",
"jest-image-snapshot": "^2.3.0",
"jest-styled-components": "^4.11.0-0",

View File

@ -6,16 +6,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#1E313B">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> -->
<!-- <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> -->
<title>My Joyent &beta;</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="header"></div>
<div id="root"></div>
<script src="/nav-static/main.js"></script>
</body>
</html>

View File

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

View File

@ -0,0 +1,34 @@
const get = require('lodash.get');
const Document = require('hapi-render-react-joyent-document');
const path = require('path');
const url = require('url');
const { default: theme } = require('./state/theme');
const { default: createClient } = require('./state/apollo-client');
const { default: createStore } = require('./state/redux-store');
const indexFile = path.join(__dirname, '../../build/index.html');
const getState = request => {
const { req, res } = request.raw;
const _font = get(theme, 'font.href', () => '');
const _mono = get(theme, 'monoFont.href', () => '');
const _addr = url.parse(`http://${req.headers.host}`);
const _theme = Object.assign({}, theme, {
font: Object.assign({}, theme.font, {
href: () => _font(_addr)
}),
monoFont: Object.assign({}, theme.monoFont, {
href: () => _mono(_addr)
})
});
return {
theme: _theme,
createClient,
createStore
};
};
module.exports = Document({ indexFile, getState });

View File

@ -1,35 +1,10 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { Provider as ReduxProvider } from 'react-redux';
import { ApolloProvider } from 'react-apollo';
import { theme, RootContainer } from 'joyent-ui-toolkit';
import { client, store } from '@state/store';
import Router from '@root/router';
const { NODE_ENV } = process.env;
const IS_PRODUCTION = NODE_ENV === 'production';
const fullTheme = {
...theme,
font: {
...theme.font,
href: !IS_PRODUCTION
? theme.font.href
: () =>
'https://fonts.googleapis.com/css?family=Libre+Franklin:400,500,600,700'
}
};
import { RootContainer } from 'joyent-ui-toolkit';
import Routes from '@root/routes';
export default () => (
<ApolloProvider client={client}>
<ThemeProvider theme={fullTheme}>
<ReduxProvider store={store}>
<RootContainer>
<Router />
</RootContainer>
</ReduxProvider>
</ThemeProvider>
</ApolloProvider>
<RootContainer>
<Routes />
</RootContainer>
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -107,6 +107,9 @@ const CNSContainer = ({
export default compose(
graphql(GetAccount, {
options: () => ({
ssr: false
}),
props: ({ data: { account: { id = '<account-id>' } = [] } }) => ({
id
})

View File

@ -172,6 +172,7 @@ export default compose(
),
graphql(ListFwRules, {
options: ({ tags, expanded, enabled }) => ({
ssr: false,
fetchPolicy: expanded && enabled ? 'cache-first' : 'cache-only',
variables: {
tags: tags.map(({ name, value }) => ({ name, value }))
@ -201,86 +202,3 @@ export default compose(
}
})
)(Firewall);
// <ReduxForm
// form="fw-enabled"
// destroyOnUnmount={false}
// forceUnregisterOnUnmount={true}
// initialValues={{ enabled }}
// onSubmit={handleEnabledToggle}
// >
// {props =>
// loading ? null : (
// <Fragment>
// <Margin bottom={7}>
// <ToggleFirewallForm {...props} />
// </Margin>
// <Divider height={remcalc(1)} />
// </Fragment>
// )
// }
// </ReduxForm>
// <ReduxForm
// form="fw-inactive"
// destroyOnUnmount={false}
// forceUnregisterOnUnmount={true}
// initialValues={{ inactive }}
// >
// {props =>
// !enabled || loading ? null : (
// <Margin top={4}>
// <ToggleInactiveForm {...props} />
// </Margin>
// )
// }
// </ReduxForm>
// {!loading && !defaultRules.length && !tagRules.length ? (
// <Margin top={5}>
// <Empty>Sorry, but we werent able to find any firewall rules.</Empty>
// </Margin>
// ) : null}
// {!loading && enabled && defaultRules.length ? (
// <Margin top={5}>
// <DefaultRules rules={defaultRules} />
// </Margin>
// ) : null}
// {!loading && enabled && tagRules.length ? (
// <Margin top={8}>
// <TagRules rules={tagRules} />
// </Margin>
// ) : null}
//
//
//
//
//
//
// <ReduxForm
// form={FORM_NAME}
// destroyOnUnmount={false}
// forceUnregisterOnUnmount={true}
// >
// {props =>
// expanded && !loading ? (
// <FirewallForm
// {...props}
// defaultRules={defaultRules}
// tagRules={tagRules}
// enabled={enabled}
// />
// ) : null
// }
// </ReduxForm>
// {expanded ? (
// <Margin bottom={4}>
// <Button type="button" onClick={handleNext}>
// Next
// </Button>
// </Margin>
// ) : proceeded ? (
// <Margin bottom={4}>
// <Button type="button" onClick={handleEdit} secondary>
// Edit
// </Button>
// </Margin>
// ) : null}

View File

@ -149,6 +149,9 @@ export default compose(
})
),
graphql(GetImages, {
options: () => ({
ssr: false
}),
props: ({ ownProps, data }) => {
const { image = '', query } = ownProps;
const { loading = false, images = [] } = data;

View File

@ -16,7 +16,7 @@ import Name from '@components/create-instance/name';
import Description from '@components/description';
import GetInstance from '@graphql/get-instance-small.gql';
import GetRandomName from '@graphql/get-random-name.gql';
import { client } from '@state/store';
import createClient from '@state/apollo-client';
import parseError from '@state/parse-error';
import { fieldError } from '@root/constants';
@ -90,7 +90,9 @@ const NameContainer = ({
export default compose(
graphql(GetRandomName, {
fetchPolicy: 'network-only',
options: () => ({
ssr: false
}),
props: ({ data }) => ({
placeholderName: data.rndName || ''
})
@ -133,7 +135,7 @@ export default compose(
}
const [err, res] = await intercept(
client.query({
createClient().query({
fetchPolicy: 'network-only',
query: GetInstance,
variables: { name }
@ -172,7 +174,7 @@ export default compose(
);
const [err, res] = await intercept(
client.query({
createClient().query({
fetchPolicy: 'network-only',
query: GetRandomName
})

View File

@ -112,6 +112,9 @@ export const Networks = ({
export default compose(
graphql(ListNetworks, {
options: () => ({
ssr: false
}),
props: ({ data }) => {
const { networks = [], loading = false, error = null, refetch } = data;

View File

@ -136,6 +136,9 @@ const PackageContainer = ({
export default compose(
graphql(getPackages, {
options: () => ({
ssr: false
}),
props: ({ data: { loading, packages = [] } }) => ({
loading,
packages: packages.map(pkg => {
@ -253,4 +256,4 @@ export default compose(
}
})
)
)(PackageContainer);
)(PackageContainer);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -129,6 +129,9 @@ export default compose(
graphql(UpdateTags, { name: 'updateTags' }),
graphql(DeleteTag, { name: 'deleteTag' }),
graphql(GetAccount, {
options: () => ({
ssr: false
}),
props: ({ data }) => {
const { account = {} } = data;
const { id = '<account-id>' } = account;
@ -137,6 +140,9 @@ export default compose(
}
}),
graphql(GetTags, {
options: () => ({
ssr: false
}),
options: ({ match }) => ({
variables: {
fetchPolicy: 'network-only',

View File

@ -133,6 +133,7 @@ export default compose(
graphql(DisableFirewall, { name: 'disableFirewall' }),
graphql(GetFirewallRules, {
options: ({ match }) => ({
ssr: false,
variables: {
fetchPolicy: 'network-only',
name: get(match, 'params.instance')

View File

@ -181,6 +181,7 @@ export default compose(
graphql(RemoveInstance, { name: 'remove' }),
graphql(ListInstances, {
options: () => ({
ssr: false,
pollInterval: 1000
}),
props: ({ data: { machines, loading, error, refetch } }) => {

View File

@ -156,6 +156,7 @@ export default compose(
graphql(DeleteMetadata, { name: 'deleteMetadata' }),
graphql(GetMetadata, {
options: ({ match }) => ({
ssr: false,
pollInterval: 1000,
variables: {
name: get(match, 'params.instance')

View File

@ -170,6 +170,7 @@ export default compose(
graphql(CreateSnapshotMutation, { name: 'createSnapshot' }),
graphql(GetSnapshots, {
options: ({ match }) => ({
ssr: false,
pollInterval: 1000,
variables: {
name: get(match, 'params.instance')

View File

@ -84,6 +84,7 @@ export default compose(
graphql(RemoveInstance, { name: 'remove' }),
graphql(GetInstance, {
options: ({ match }) => ({
ssr: false,
pollInterval: 1000,
variables: {
name: get(match, 'params.instance')

View File

@ -149,6 +149,7 @@ export default compose(
graphql(DeleteTag, { name: 'deleteTag' }),
graphql(GetTags, {
options: ({ match }) => ({
ssr: false,
pollInterval: 1000,
variables: {
name: get(match, 'params.instance')

View File

@ -47,6 +47,7 @@ export const UserScript = ({ metadata, loading = false, error = null }) => (
export default compose(
graphql(GetMetadata, {
options: ({ match }) => ({
ssr: false,
variables: {
fetchPolicy: 'network-only',
name: get(match, 'params.instance')

View File

@ -1,8 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider, consolidateStreamedStyles } 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 'joyent-ui-toolkit';
import createStore from '@state/redux-store';
import createClient from '@state/apollo-client';
import { register } from './sw';
import App from './app';
@ -10,6 +18,19 @@ if (!isFunction(Number.isFinite)) {
Number.isFinite = isFinite;
}
ReactDOM.render(<App />, document.getElementById('root'));
consolidateStreamedStyles();
ReactDOM.hydrate(
<ApolloProvider client={createClient()}>
<ThemeProvider theme={theme}>
<ReduxProvider store={createStore()}>
<BrowserRouter>
<App />
</BrowserRouter>
</ReduxProvider>
</ThemeProvider>
</ApolloProvider>,
document.getElementById('root')
);
register();

View File

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

View File

@ -1,87 +0,0 @@
import React from 'react';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
import get from 'lodash.get';
import { PageContainer } from 'joyent-ui-toolkit';
import { Breadcrumb, Menu } from '@containers/navigation';
import Footer from '@components/navigation/footer';
import CreateInstance from '@containers/create-instance';
import {
List as Instances,
Summary as InstanceSummary,
Tags as InstanceTags,
Metadata as InstanceMetadata,
Networks as InstanceNetworks,
Firewall as InstanceFirewall,
Cns as InstanceCns,
Snapshots as InstanceSnapshots,
UserScript as InstanceUserScript
} from '@containers/instances';
export default () => (
<BrowserRouter>
<PageContainer>
{/* Breadcrumb */}
<Switch>
<Route path="/~create/:section?" exact component={Breadcrumb} />
<Route path="/~:action/:instance?" exact component={Breadcrumb} />
<Route path="/:instance?" component={Breadcrumb} />
</Switch>
{/* Menu */}
<Switch>
<Route path="/~:action/:id?" exact component={Menu} />
<Route path="/:instance?/:section?" component={Menu} />
</Switch>
{/* Instances List */}
<Switch>
<Route path="/" exact component={Instances} />
</Switch>
{/* Instance Sections */}
<Switch>
<Route path="/~:action" component={() => null} />
<Route path="/:instance/summary" exact component={InstanceSummary} />
<Route path="/:instance/tags" exact component={InstanceTags} />
<Route path="/:instance/metadata" exact component={InstanceMetadata} />
<Route path="/:instance/networks" exact component={InstanceNetworks} />
<Route path="/:instance/firewall" exact component={InstanceFirewall} />
<Route
path="/:instance/snapshots"
exact
component={InstanceSnapshots}
/>
<Route path="/:instance/cns" exact component={InstanceCns} />
<Route
path="/:instance/user-script"
exact
component={InstanceUserScript}
/>
<Route
path="/:instance"
exact
component={({ match }) => (
<Redirect to={`/${get(match, 'params.instance')}/summary`} />
)}
/>
</Switch>
{/* Actions */}
<Switch>
{/* Create Instance */}
<Route
path="/~create/"
exact
component={({ match, location }) => (
<Redirect to={`/~create/name${location.search}`} />
)}
/>
<Route path="/~create/:step" component={CreateInstance} />
</Switch>
<Footer />
</PageContainer>
</BrowserRouter>
);

View File

@ -0,0 +1,103 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import get from 'lodash.get';
import {
PageContainer,
RootContainer,
ViewContainer,
Message,
MessageDescription,
MessageTitle
} from 'joyent-ui-toolkit';
import { Breadcrumb, Menu } from '@containers/navigation';
import Footer from '@components/navigation/footer';
import CreateInstance from '@containers/create-instance';
import { Route as ServerError } from '@root/server-error';
import {
List as Instances,
Summary as InstanceSummary,
Tags as InstanceTags,
Metadata as InstanceMetadata,
Networks as InstanceNetworks,
Firewall as InstanceFirewall,
Cns as InstanceCns,
Snapshots as InstanceSnapshots,
UserScript as InstanceUserScript
} from '@containers/instances';
export default () => (
<PageContainer>
{/* Breadcrumb */}
<Switch>
<Route path="/~create/:section?" exact component={Breadcrumb} />
<Route path="/~:action/:instance?" exact component={Breadcrumb} />
<Route path="/:instance?" component={Breadcrumb} />
</Switch>
{/* Menu */}
<Switch>
<Route path="/~server-error" component={() => null} />
<Route path="/~:action/:id?" exact component={Menu} />
<Route path="/:instance?/:section?" component={Menu} />
</Switch>
{/* Instances List */}
<Switch>
<Route path="/" exact component={Instances} />
</Switch>
{/* Instance Sections */}
<Switch>
<Route path="/~:action" component={() => null} />
<Route path="/:instance/summary" exact component={InstanceSummary} />
<Route path="/:instance/tags" exact component={InstanceTags} />
<Route path="/:instance/metadata" exact component={InstanceMetadata} />
<Route path="/:instance/networks" exact component={InstanceNetworks} />
<Route path="/:instance/firewall" exact component={InstanceFirewall} />
<Route path="/:instance/snapshots" exact component={InstanceSnapshots} />
<Route path="/:instance/cns" exact component={InstanceCns} />
<Route
path="/:instance/user-script"
exact
component={InstanceUserScript}
/>
<Route
path="/:instance"
exact
component={({ match }) => (
<Redirect to={`/${get(match, 'params.instance')}/summary`} />
)}
/>
</Switch>
{/* Actions */}
<Switch>
{/* Create Instance */}
<Route
path="/~create/"
exact
component={({ match, location }) => (
<Redirect to={`/~create/name${location.search}`} />
)}
/>
<Route path="/~create/:step" component={CreateInstance} />
</Switch>
<Route path="/~server-error" exact component={ServerError} />
<noscript>
<ViewContainer main>
<Message warning>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
You need to enable JavaScript to run this app.
</MessageDescription>
</Message>
</ViewContainer>
</noscript>
<Footer />
</PageContainer>
);

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Margin } from 'styled-components-spacing';
import remcalc from 'remcalc';
import {
RootContainer,
PageContainer,
ViewContainer,
Message,
MessageDescription,
MessageTitle,
Divider
} from 'joyent-ui-toolkit';
import Breadcrumb from '@containers/navigation/breadcrumb';
export const Route = () => (
<ViewContainer main>
<Divider height={remcalc(30)} transparent />
<Margin bottom={4}>
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your page
</MessageDescription>
</Message>
</Margin>
</ViewContainer>
);
export default () => (
<RootContainer>
<PageContainer>
<Breadcrumb />
<Route />
</PageContainer>
</RootContainer>
);

View File

@ -0,0 +1,33 @@
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import fetch from 'cross-fetch';
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}/graphql`;
export default (opts = {}) => {
let cache = new InMemoryCache();
if (global.__APOLLO_STATE__) {
cache = cache.restore(global.__APOLLO_STATE__);
}
return new ApolloClient({
cache,
link: new HttpLink({
uri: URI,
credentials: 'same-origin',
fetch
}),
...opts
});
};

View File

@ -0,0 +1,16 @@
import { canUseDOM } from 'exenv';
export default (() => {
if (!canUseDOM) {
return {};
}
return {
port: window.location.port,
protocol: window.location.protocol.replace(/:$/, ''),
hostname: window.location.hostname,
__REDUX_DEVTOOLS_EXTENSION__: window.__REDUX_DEVTOOLS_EXTENSION__,
__APOLLO_STATE__: window.__APOLLO_STATE__,
__REDUX_STATE__: window.__REDUX_STATE__
};
})();

View File

@ -0,0 +1,45 @@
import { reduxBatch } from '@manaflair/redux-batch';
import { createStore, combineReducers, compose } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { reducer as valuesReducer } from 'react-redux-values';
import paramCase from 'param-case';
import global from './global';
const initialState = {
ui: {
sections: {
instances: [
'Summary',
'CNS',
'Snapshots',
'Tags',
'Metadata',
'User Script',
'Networks',
'Firewall'
].map(name => ({
pathname: paramCase(name),
name
}))
}
}
};
export default () => {
return createStore(
combineReducers({
values: valuesReducer,
form: formReducer,
ui: (state = {}) => state
}),
global.__REDUX_STATE__ || initialState,
compose(
reduxBatch,
// If you are using the devToolsExtension, you can add it here also
// eslint-disable-next-line no-negated-condition
typeof global.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined'
? global.__REDUX_DEVTOOLS_EXTENSION__()
: f => f
)
);
};

View File

@ -1,62 +0,0 @@
import { reduxBatch } from '@manaflair/redux-batch';
import { createStore, combineReducers, compose } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { reducer as valuesReducer } from 'react-redux-values';
import paramCase from 'param-case';
const {
REACT_APP_GQL_PORT = window.location.port,
REACT_APP_GQL_PROTOCOL = window.location.protocol.replace(/:$/, ''),
REACT_APP_GQL_HOSTNAME = window.location.hostname
} = process.env;
const PORT = REACT_APP_GQL_PORT ? `:${REACT_APP_GQL_PORT}` : '';
const URI = `${REACT_APP_GQL_PROTOCOL}://${REACT_APP_GQL_HOSTNAME}${PORT}/graphql`;
export const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
credentials: 'same-origin',
uri: URI
})
});
const initialState = {
ui: {
sections: {
instances: [
'Summary',
'CNS',
'Snapshots',
'Tags',
'Metadata',
'User Script',
'Networks',
'Firewall'
].map(name => ({
pathname: paramCase(name),
name
}))
}
}
};
export const store = createStore(
combineReducers({
values: valuesReducer,
form: formReducer,
ui: (state = {}) => state
}),
initialState,
compose(
reduxBatch,
// If you are using the devToolsExtension, you can add it here also
// eslint-disable-next-line no-negated-condition
typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined'
? window.__REDUX_DEVTOOLS_EXTENSION__()
: f => f
)
);

View File

@ -14,15 +14,15 @@
"prepublish": "echo 0"
},
"dependencies": {
"apollo-cache-inmemory": "^1.1.5",
"apollo-client": "^2.2.0",
"apollo-link": "^1.0.7",
"apollo-link-http": "^1.3.2",
"apollo-link-state": "^0.3.1",
"babel-preset-joyent-portal": "^6.0.3",
"apollo-cache-inmemory": "^1.1.9",
"apollo-client": "^2.2.5",
"apollo-link": "^1.2.1",
"apollo-link-http": "^1.5.1",
"apollo-link-state": "^0.4.0",
"babel-preset-joyent-portal": "^7.0.1",
"emotion": "^8.0.12",
"emotion-theming": "^8.0.12",
"graphql-tag": "^2.6.1",
"graphql-tag": "^2.8.0",
"inert": "^5.1.0",
"joyent-icons": "^5.0.0",
"joyent-ui-toolkit": "^5.0.0",
@ -40,8 +40,8 @@
"remcalc": "^1.0.10"
},
"devDependencies": {
"babel-eslint": "^8.2.1",
"eslint": "^4.16.0",
"babel-eslint": "^8.2.2",
"eslint": "^4.18.1",
"eslint-config-joyent-portal": "^3.3.1"
}
}

View File

@ -25,7 +25,7 @@ const RegionContainer = emotion('div')`
padding-bottom: ${remcalc(24)};
`;
const getDatacenters = gql`
const GetDatacenters = gql`
{
regions @client {
name
@ -80,7 +80,10 @@ const Datacenters = ({ expanded, regions = [] }) =>
) : null;
export default compose(
graphql(getDatacenters, {
graphql(GetDatacenters, {
options: () => ({
ssr: false
}),
props: ({ data }) => {
const {
regions = [],

View File

@ -74,12 +74,18 @@ const Services = ({ expanded = false, categories = [], products = [] }) =>
export default compose(
graphql(GetCategories, {
options: () => ({
ssr: false
}),
props: ({ data }) => {
const { categories = [] } = data;
return { categories };
}
}),
graphql(GetProducts, {
options: () => ({
ssr: false
}),
props: ({ data }) => {
const { products = [] } = data;
return { products };

View File

@ -20,20 +20,20 @@ import {
HeaderSpace
} from './components';
const updateHeaderMutation = gql`
const UpdateHeaderMutation = gql`
mutation updateHeader($isOpen: Boolean!, $activePanel: String!) {
updateHeader(isOpen: $isOpen, activePanel: $activePanel) @client
}
`;
const getHeader = gql`
const GetHeader = gql`
{
isOpen @client
activePanel @client
}
`;
const getAccount = gql`
const GetAccount = gql`
{
account {
login
@ -96,7 +96,10 @@ const Navigation = ({ login, toggleSectionOpen, isOpen, activePanel }) => (
);
export default compose(
graphql(getAccount, {
graphql(GetAccount, {
options: () => ({
ssr: false
}),
props: ({ data }) => {
const { account = {}, loading = false, error = null } = data;
const { login } = account;
@ -104,7 +107,10 @@ export default compose(
return { login, loading, error };
}
}),
graphql(getHeader, {
graphql(GetHeader, {
options: () => ({
ssr: false
}),
props: ({ data }) => {
const {
isOpen = false,
@ -116,7 +122,7 @@ export default compose(
return { isOpen, activePanel, loading, error };
}
}),
graphql(updateHeaderMutation, {
graphql(UpdateHeaderMutation, {
props: ({ mutate, ownProps }) => ({
toggleSectionOpen: (name = '') => {
const { isOpen, activePanel } = ownProps;

View File

@ -21,7 +21,8 @@
},
"dependencies": {
"camel-case": "^3.0.0",
"clipboard-copy": "^1.4.1",
"clipboard-copy": "^1.4.2",
"exenv": "^1.2.2",
"joy-react-broadcast": "^0.6.9",
"joyent-icons": "^5.0.0",
"joyent-react-styled-flexboxgrid": "^2.2.3",
@ -35,19 +36,19 @@
"pascal-case": "^2.0.1",
"prop-types": "^15.6.0",
"react-bundle": "^1.1.0",
"react-popper": "^0.7.5",
"react-responsive": "^4.0.3",
"react-popper": "^0.8.2",
"react-responsive": "^4.0.4",
"remcalc": "^1.0.10",
"rnd-id": "^2.0.2",
"styled-components": "^3.1.4",
"styled-components": "^3.1.6",
"styled-is": "^1.1.2",
"unitcalc": "^1.1.2"
"unitcalc": "^1.2.3"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-joyent-portal": "^6.0.3",
"babel-preset-joyent-portal": "^7.0.1",
"classnames": "^2.2.5",
"eslint": "^4.16.0",
"eslint": "^4.18.1",
"eslint-config-joyent-portal": "^3.3.1",
"jest-styled-components": "^4.11.0-0",
"joyent-react-scripts": "^7.3.0",
@ -56,12 +57,12 @@
"react-docgen": "^3.0.0-beta8",
"react-docgen-displayname-handler": "^1.0.1",
"react-dom": "^16.2.0",
"react-styleguidist": "^6.2.0",
"react-styleguidist": "^6.2.5",
"react-test-renderer": "^16.2.0",
"redrun": "^5.10.0",
"redrun": "^5.10.5",
"styled-components-spacing": "^2.1.3",
"styled-flex-component": "^2.2.0",
"webpack": "^3.10.0"
"styled-flex-component": "^2.2.1",
"webpack": "^3.11.0"
},
"peerDependencies": {
"codemirror": "^5.32.0",

View File

@ -1,4 +1,16 @@
import remcalc from 'remcalc';
import { canUseDOM } from 'exenv';
const globals = (() => {
if (!canUseDOM) {
return {};
}
return {
protocol: window.location.protocol,
host: window.location.host
};
})();
const flexboxgrid = {
gridSize: 12, // rem
@ -138,10 +150,12 @@ export const font = {
family: '"Libre Franklin"',
families:
'"Libre Franklin", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica, sans-serif',
href: () =>
`${document.location.protocol}//${
document.location.host
}/fonts/css?family=Libre+Franklin:400,500,600,700`,
href: ({ protocol, host } = {}) => {
const _protocol = protocol || globals.protocol;
const _host = host || globals.host;
return `${_protocol}//${_host}/fonts/css?family=Libre+Franklin:400,500,600,700`;
},
weight: {
bold: 700,
semibold: 600,
@ -157,10 +171,12 @@ export const monoFont = {
textMuted: base.secondary,
family: '"Roboto Mono"',
families: '"Roboto Mono", monospace',
href: () =>
`${document.location.protocol}//${
document.location.host
}/fonts/css?family=Roboto+Mono:700,400`,
href: ({ protocol, host } = {}) => {
const _protocol = protocol || globals.protocol;
const _host = host || globals.host;
return `${_protocol}//${_host}/fonts/css?family=Roboto+Mono:700,400`;
},
weight: {
bold: 700,
normal: 400

View File

@ -31,7 +31,7 @@
"styled-components": "^3.1.4"
},
"devDependencies": {
"babel-preset-joyent-portal": "^6.0.1",
"babel-preset-joyent-portal": "^7.0.0",
"eslint": "^4.11.0",
"eslint-config-joyent-portal": "^3.2.0",
"joyent-react-scripts": "^7.3.0",

505
yarn.lock

File diff suppressed because it is too large Load Diff