joyent-portal/consoles/my-joy-instances/src/containers/instances/summary.js

314 lines
7.9 KiB
JavaScript

import React from 'react';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import { set } from 'react-redux-values';
import { stopSubmit } from 'redux-form';
import { Margin } from 'styled-components-spacing';
import intercept from 'apr-intercept';
import isArray from 'lodash.isarray';
import some from 'lodash.some';
import isInteger from 'lodash.isinteger';
import get from 'lodash.get';
import ReduxForm from 'declarative-redux-form';
import {
ViewContainer,
StatusLoader,
Message,
MessageDescription,
MessageTitle
} from 'joyent-ui-toolkit';
import GetInstance from '@graphql/get-instance.gql';
import StartInstance from '@graphql/start-instance.gql';
import StopInstance from '@graphql/stop-instance.gql';
import RebootInstance from '@graphql/reboot-instance.gql';
import RemoveInstance from '@graphql/remove-instance.gql';
import RenameMachine from '@graphql/rename-machine.gql';
import SummaryScreen from '@components/instances/summary';
import parseError from '@state/parse-error';
import Confirm from '@state/confirm';
import { instanceName as validateName } from '@state/validators';
const FORM = 'change-name';
export const Summary = ({
instance,
loading,
loadingError,
mutationError,
handleAction,
starting,
stopping,
rebooting,
removing,
editName,
editingName,
handleChangeName,
handleAsyncValidate,
shouldAsyncValidate
}) => {
const _loading =
loading || (!instance && !loadingError) ? <StatusLoader /> : null;
const _summary = !_loading &&
instance && (
<ReduxForm
form={FORM}
onSubmit={handleChangeName}
initialValues={{ name: instance.name }}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
>
{props => (
<SummaryScreen
{...props}
instance={instance}
starting={starting}
stopping={stopping}
rebooting={rebooting}
removing={removing}
onAction={handleAction}
editName={editName}
editingName={editingName}
/>
)}
</ReduxForm>
);
const _error = loadingError &&
!_loading &&
!instance && (
<Margin bottom="5">
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your instance summary
</MessageDescription>
</Message>
</Margin>
);
const _mutationError = mutationError && (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{mutationError}</MessageDescription>
</Message>
);
return (
<ViewContainer center={Boolean(_loading)} main>
{_loading}
{_error}
{_mutationError}
{_summary}
</ViewContainer>
);
};
// from https://github.com/natesilva/is-in-subnet
const ipv4ToLong = ip => {
const octets = ip.split('.');
return (
((parseInt(octets[0], 10) << 24) +
(parseInt(octets[1], 10) << 16) +
(parseInt(octets[2], 10) << 8) +
parseInt(octets[3], 10)) >>>
0
);
};
// from https://github.com/natesilva/is-in-subnet
const isInSubnet = (address, subnetOrSubnets) => {
if (isArray(subnetOrSubnets)) {
return some(subnetOrSubnets, subnet => isInSubnet(address, subnet));
}
const subnet = subnetOrSubnets;
const [subnetAddress, prefixLengthString] = subnet.split('/');
const prefixLength = parseInt(prefixLengthString, 10);
if (!subnetAddress || !isInteger(prefixLength)) {
return;
}
if (prefixLength < 0 || prefixLength > 32) {
return;
}
// the next two lines throw if the addresses are not valid IPv4 addresses
const subnetLong = ipv4ToLong(subnetAddress);
const addressLong = ipv4ToLong(address);
if (prefixLength === 0) {
return true;
}
const subnetPrefix = subnetLong >> (32 - prefixLength);
const addressPrefix = addressLong >> (32 - prefixLength);
return subnetPrefix === addressPrefix;
};
// from https://github.com/natesilva/is-in-subnet
const isPrivate = address => {
return isInSubnet(address, ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']);
};
export default compose(
graphql(StopInstance, { name: 'stop' }),
graphql(RenameMachine, { name: 'rename' }),
graphql(StartInstance, { name: 'start' }),
graphql(RebootInstance, { name: 'reboot' }),
graphql(RemoveInstance, { name: 'remove' }),
graphql(GetInstance, {
options: ({ match }) => ({
ssr: false,
pollInterval: 1000,
variables: {
id: get(match, 'params.instance')
}
}),
props: ({ data: { loading, error, machine, ...rest } }) => {
if (machine) {
const { ips } = machine;
const grupedIps = ips
.map(ip => ({ ip, openness: isPrivate(ip) ? 'private' : 'public' }))
.reduce(
(sum, { ip, openness }) =>
Object.assign(sum, {
[openness]: (sum[openness] || []).concat([ip])
}),
{}
);
machine = Object.assign({}, machine, { ips: grupedIps });
}
return {
instance: machine,
loading,
loadingError: error
};
}
}),
connect(
({ values }, ownProps) => {
const { instance = {} } = ownProps;
const { id } = instance;
if (!id) {
return ownProps;
}
return {
...ownProps,
editingName: get(values, 'editing-name', false),
starting: values[`${id}-summary-starting`],
stopping: values[`${id}-summary-stoping`],
rebooting: values[`${id}-summary-rebooting`],
removing: values[`${id}-summary-removeing`],
mutationError: values[`${id}-summary-mutation-error`]
};
},
(dispatch, ownProps) => ({
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'change';
},
handleAsyncValidate: validateName,
editName: () =>
dispatch(
set({
name: `editing-name`,
value: true
})
),
handleChangeName: async ({ name, id }) => {
const { instance } = ownProps;
if (name === instance.name) {
return dispatch(
set({
name: `editing-name`,
value: false
})
);
}
const [err] = await intercept(
ownProps.rename({
variables: {
name,
id: get(ownProps, 'match.params.instance')
}
})
);
if (err) {
return dispatch(
stopSubmit(FORM, {
_error: parseError(err)
})
);
}
dispatch(
set({
name: `editing-name`,
value: false
})
);
},
handleAction: async action => {
const { instance } = ownProps;
const { id } = instance;
if (!await Confirm(`Do you want to ${action} "${instance.name}"?`)) {
return;
}
const gerund = `${action}ing`;
const name = `${id}-summary-${gerund}`;
// sets loading to true
dispatch(
set({
name,
value: true
})
);
// calls mutation and waits while loading is still true
const [err] = await intercept(
ownProps[action]({
variables: { id }
})
);
if (!err && action === 'remove') {
const { history } = ownProps;
return history.push('/instances');
}
// after mutation, sets loading back to false
const setLoadingFalse = set({
name,
value: false
});
// if error, sets error value
const mutationError =
err &&
set({
name: `${id}-summary-mutation-error`,
value: parseError(err)
});
return dispatch([mutationError, setLoadingFalse].filter(Boolean));
}
})
)
)(Summary);